From 28ee1ae2602662b216549ba4db0d34a9b55b8deb Mon Sep 17 00:00:00 2001 From: sergio Date: Thu, 21 May 2026 21:50:06 +0000 Subject: [PATCH] feat(charka): nivel 88 + modelo de datos compartido en charka-ir MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Los nombres de condición de COBOL (IF ES-VALIDO), que antes el transpilador evaluaba siempre como false. Y, de paso, se elimina la duplicación de la resolución del modelo de datos. - charka-ir gana un módulo `model`: resolve_data(&[DataItem]) -> DataModel aplana el árbol de datos a campos elementales (Field con FieldKind) y a nombres de condición (ConditionName). El Ir lleva ahora un campo `model` — la fuente única de verdad sobre la clasificación de PICTURE. - charka-codegen y charka-shadow consumen ir.model en vez de reimplementar cada uno la clasificación, el ancho de PICTURE y la normalización de VALUE. charka-codegen ya no depende de charka-bcd. - Cond::Named (un nivel 88) se resuelve a `padre = valor`: el codegen emite la comparación, el intérprete sombra la evalúa. - Corregido: un dato con hijos de nivel 88 antes se perdía como si fuera un grupo; ahora se reconoce como campo elemental. - Corpus: programa nuevo 10-condicion (semáforo con 88 de texto y de número). Verificado: intérprete y crate compilado dan igual salida. Tests: charka-ir 23, charka-codegen 17, charka-shadow 15. fmt + clippy limpios. Co-Authored-By: Claude Opus 4.7 --- Cargo.lock | 2 +- crates/modules/charka/SDD.md | 13 +- .../modules/charka/charka-codegen/Cargo.toml | 1 - .../modules/charka/charka-codegen/src/expr.rs | 16 +- .../modules/charka/charka-codegen/src/lib.rs | 63 ++-- .../modules/charka/charka-codegen/src/sym.rs | 132 +++----- crates/modules/charka/charka-ir/Cargo.toml | 1 + crates/modules/charka/charka-ir/src/ast.rs | 7 +- crates/modules/charka/charka-ir/src/lib.rs | 3 + crates/modules/charka/charka-ir/src/model.rs | 293 ++++++++++++++++++ .../modules/charka/charka-shadow/src/field.rs | 128 +------- .../charka/charka-shadow/src/interp.rs | 20 +- .../modules/charka/charka-shadow/src/lib.rs | 1 + crates/modules/charka/corpus/10-condicion.cob | 29 ++ .../charka/corpus/10-condicion.expected | 4 + crates/modules/charka/corpus/README.md | 1 + docs/changelog/charka.md | 24 ++ 17 files changed, 473 insertions(+), 265 deletions(-) create mode 100644 crates/modules/charka/charka-ir/src/model.rs create mode 100644 crates/modules/charka/corpus/10-condicion.cob create mode 100644 crates/modules/charka/corpus/10-condicion.expected diff --git a/Cargo.lock b/Cargo.lock index 5ed4ea3..df4b287 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2332,7 +2332,6 @@ dependencies = [ name = "charka-codegen" version = "0.1.0" dependencies = [ - "charka-bcd", "charka-ir", "charka-lexer", "charka-parser", @@ -2342,6 +2341,7 @@ dependencies = [ name = "charka-ir" version = "0.1.0" dependencies = [ + "charka-bcd", "charka-lexer", "charka-parser", ] diff --git a/crates/modules/charka/SDD.md b/crates/modules/charka/SDD.md index d45adff..5f35400 100644 --- a/crates/modules/charka/SDD.md +++ b/crates/modules/charka/SDD.md @@ -75,15 +75,22 @@ Tercera etapa: `Program` → `Ir`. Aquí se parsea cada `Sentence` cruda árbol de instrucciones. - `lower(&Program) -> Ir` — **total y tolerante**, nunca falla. -- `Ir { program_id, data: Vec, procedures: Vec }`. - El modelo de datos pasa tal cual (sirve de tabla de símbolos). +- `Ir { program_id, data, model, procedures }`. `data` es el árbol de + `DataItem` con su estructura de grupos; `model` es el **modelo de + datos resuelto** (módulo `model`): el árbol aplanado a campos + elementales (`Field` con `FieldKind` numérico/alfanumérico y el + `VALUE` normalizado) más los nombres de condición (nivel 88). Es la + fuente única de verdad sobre «qué tipo de campo describe una + PICTURE» — `charka-codegen` y `charka-shadow` lo consumen en vez de + reimplementar la clasificación. - `Procedure { name, body: Vec }`. `Stmt` cubre `Move`, `Display`, `Accept`, `Compute`, `Add`/`Subtract`/`Multiply`/`Divide`, `If`, `Evaluate`, `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 (88). + palabra) unidas por `AND`/`OR`/`NOT`, más nombres de condición + (nivel 88), que se resuelven contra el `model` a `padre = valor`. - Un verbo no soportado se conserva como `Stmt::Unknown { verb, tokens }` — el lowering jamás aborta. - COBOL no separa statements con un símbolo: cada uno corta donde diff --git a/crates/modules/charka/charka-codegen/Cargo.toml b/crates/modules/charka/charka-codegen/Cargo.toml index 53c919b..0bfc378 100644 --- a/crates/modules/charka/charka-codegen/Cargo.toml +++ b/crates/modules/charka/charka-codegen/Cargo.toml @@ -10,7 +10,6 @@ description = "charka-codegen — emisión de Rust desde el IR de charka: el mod [dependencies] charka-ir = { path = "../charka-ir" } -charka-bcd = { path = "../charka-bcd" } [dev-dependencies] charka-parser = { path = "../charka-parser" } diff --git a/crates/modules/charka/charka-codegen/src/expr.rs b/crates/modules/charka/charka-codegen/src/expr.rs index 4d99c83..f5cc87b 100644 --- a/crates/modules/charka/charka-codegen/src/expr.rs +++ b/crates/modules/charka/charka-codegen/src/expr.rs @@ -114,9 +114,19 @@ pub(crate) fn emit_expr(sym: &Symbols, e: &Expr) -> String { pub(crate) fn emit_cond(sym: &Symbols, c: &Cond) -> String { match c { Cond::Compare { lhs, op, rhs } => emit_compare(sym, lhs, *op, rhs), - Cond::Named(name) => { - format!("false /* charka: condición 88 no soportada: {name} */") - } + Cond::Named(name) => match sym.condition(name) { + // Un nombre de condición (88) equivale a comparar su dato + // padre con el valor que la hace verdadera. + Some(cn) => emit_cond( + sym, + &Cond::Compare { + lhs: Operand::Data(cn.parent.clone()), + op: CmpOp::Eq, + rhs: cn.value.clone(), + }, + ), + None => format!("false /* charka: condición 88 no resuelta: {name} */"), + }, Cond::Not(inner) => format!("!({})", emit_cond(sym, inner)), Cond::And(a, b) => format!("({}) && ({})", emit_cond(sym, a), emit_cond(sym, b)), Cond::Or(a, b) => format!("({}) || ({})", emit_cond(sym, a), emit_cond(sym, b)), diff --git a/crates/modules/charka/charka-codegen/src/lib.rs b/crates/modules/charka/charka-codegen/src/lib.rs index 1432b2b..dfbd509 100644 --- a/crates/modules/charka/charka-codegen/src/lib.rs +++ b/crates/modules/charka/charka-codegen/src/lib.rs @@ -39,7 +39,7 @@ use sym::{paragraph_method, 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.data); + let sym = Symbols::build(&ir.model); let mut em = Emitter::new(); emit_header(&mut em); emit_struct(&mut em, &sym); @@ -142,53 +142,17 @@ fn emit_main(em: &mut Emitter) { em.line("}"); } -/// El inicializador de un campo, a partir de su cláusula `VALUE`. +/// El inicializador de un campo, a partir de su `VALUE` ya +/// normalizado por `charka-ir`. fn field_init(f: &Field) -> String { match &f.kind { FieldKind::Num { int, frac, signed } => format!( "Num::with_value(Picture::new({int}, {frac}, {signed}), {})", - rust_str(&numeric_value(f.value.as_deref())) + rust_str(&f.init) ), - FieldKind::Text { len } => format!( - "Text::with_value({len}, {})", - rust_str(&text_value(f.value.as_deref())) - ), - } -} - -/// Normaliza el `VALUE` de un campo numérico a un literal parseable. -fn numeric_value(v: Option<&str>) -> String { - let Some(raw) = v else { - return "0".to_string(); - }; - let up = raw.to_uppercase(); - if matches!(up.as_str(), "ZERO" | "ZEROS" | "ZEROES") { - return "0".to_string(); - } - if charka_bcd::Decimal::parse(raw).is_ok() { - raw.to_string() - } else { - "0".to_string() - } -} - -/// Normaliza el `VALUE` de un campo de texto. El parser envuelve los -/// literales de texto en comillas simples; aquí se desenvuelven. -fn text_value(v: Option<&str>) -> String { - let Some(raw) = v else { - return String::new(); - }; - let up = raw.to_uppercase(); - if matches!(up.as_str(), "SPACE" | "SPACES") { - return String::new(); - } - if matches!(up.as_str(), "ZERO" | "ZEROS" | "ZEROES") { - return "0".to_string(); - } - if raw.len() >= 2 && raw.starts_with('\'') && raw.ends_with('\'') { - raw[1..raw.len() - 1].to_string() - } else { - raw.to_string() + FieldKind::Text { len } => { + format!("Text::with_value({len}, {})", rust_str(&f.init)) + } } } @@ -365,6 +329,19 @@ mod tests { assert!(out.contains("} else {")); } + #[test] + fn level_88_condition_resolves_to_a_comparison() { + let out = gen("DATA DIVISION.\n\ + WORKING-STORAGE SECTION.\n\ + 01 WS-FLAG PIC X VALUE 'N'.\n\ + 88 ES-SI VALUE 'Y'.\n\ + PROCEDURE DIVISION.\n\ + MAIN.\n\ + IF ES-SI DISPLAY 'SI' END-IF.\n"); + // ES-SI equivale a `WS-FLAG = 'Y'` (comparación de texto). + assert!(out.contains("cobol_text_cmp(self.ws_flag.display().as_str(), \"Y\").is_eq()")); + } + #[test] fn empty_program_still_compiles_shape() { let out = gen(""); diff --git a/crates/modules/charka/charka-codegen/src/sym.rs b/crates/modules/charka/charka-codegen/src/sym.rs index cf530b3..92dc143 100644 --- a/crates/modules/charka/charka-codegen/src/sym.rs +++ b/crates/modules/charka/charka-codegen/src/sym.rs @@ -1,8 +1,14 @@ -//! Tabla de símbolos: el modelo de datos COBOL traducido a campos Rust. +//! Tabla de símbolos del código generado: los campos del `struct +//! Program` y los nombres de condición, derivados del modelo de datos +//! resuelto que entrega `charka-ir`. use std::collections::HashMap; -use charka_ir::DataItem; +use charka_ir::{ConditionName, DataModel}; + +/// El tipo de campo lo aporta `charka-ir`; se reexporta para que el +/// resto del crate lo nombre como `crate::sym::FieldKind`. +pub(crate) use charka_ir::FieldKind; /// Un campo del struct `Program` generado. pub(crate) struct Field { @@ -12,36 +18,46 @@ pub(crate) struct Field { pub ident: String, /// Numérico o alfanumérico. pub kind: FieldKind, - /// La cláusula `VALUE`, si la hay. - pub value: Option, + /// Valor inicial normalizado (de la cláusula `VALUE`). + pub init: String, } -/// El tipo de un campo elemental. -pub(crate) enum FieldKind { - /// Campo numérico — se emite como `Num`. - Num { int: u8, frac: u8, signed: bool }, - /// Campo alfanumérico — se emite como `Text`. - Text { len: usize }, -} - -/// El conjunto de campos del programa, indexado por nombre COBOL. +/// Los campos del programa y sus nombres de condición, indexados. pub(crate) struct Symbols { pub fields: Vec, by_name: HashMap, + conditions: HashMap, } impl Symbols { - /// Construye la tabla recorriendo el árbol de datos. - pub(crate) fn build(data: &[DataItem]) -> Self { - let mut fields = Vec::new(); - collect(data, &mut fields); + /// Construye la tabla desde el modelo de datos resuelto. + pub(crate) fn build(model: &DataModel) -> Self { + let mut fields: Vec = model + .fields + .iter() + .map(|f| Field { + cobol: f.name.clone(), + ident: sanitize_ident(&f.name), + kind: f.kind, + init: f.init.clone(), + }) + .collect(); dedup_idents(&mut fields); let by_name = fields .iter() .enumerate() .map(|(i, f)| (f.cobol.clone(), i)) .collect(); - Self { fields, by_name } + let conditions = model + .conditions + .iter() + .map(|c| (c.name.clone(), c.clone())) + .collect(); + Self { + fields, + by_name, + conditions, + } } /// Busca un campo por su nombre COBOL (sin distinguir mayúsculas). @@ -50,31 +66,10 @@ impl Symbols { .get(&cobol.to_uppercase()) .map(|&i| &self.fields[i]) } -} -/// Recoge los datos elementales del árbol. Los grupos no son campos — -/// se recurre en sus hijos. Se saltan niveles 88/66 y los `FILLER`. -fn collect(items: &[DataItem], out: &mut Vec) { - for it in items { - if it.level == 88 || it.level == 66 { - continue; - } - if !it.children.is_empty() { - collect(&it.children, out); - continue; - } - if it.name == "FILLER" { - continue; - } - let Some(kind) = classify(it.picture.as_deref()) else { - continue; - }; - out.push(Field { - cobol: it.name.clone(), - ident: sanitize_ident(&it.name), - kind, - value: it.value.clone(), - }); + /// Busca un nombre de condición (un dato de nivel 88). + pub(crate) fn condition(&self, name: &str) -> Option<&ConditionName> { + self.conditions.get(&name.to_uppercase()) } } @@ -91,59 +86,6 @@ fn dedup_idents(fields: &mut [Field]) { } } -/// Clasifica una cláusula PICTURE: alfanumérica si tiene `X`/`A`, -/// numérica si `charka-bcd` la parsea; una PICTURE de edición se trata -/// como texto de presentación. -fn classify(pic: Option<&str>) -> Option { - let up = pic?.to_uppercase(); - if up.contains('X') || up.contains('A') { - return Some(FieldKind::Text { - len: pic_width(&up).max(1), - }); - } - if let Ok(p) = charka_bcd::Picture::parse(&up) { - return Some(FieldKind::Num { - int: p.integer_digits, - frac: p.fraction_digits, - signed: p.signed, - }); - } - Some(FieldKind::Text { - len: pic_width(&up).max(1), - }) -} - -/// Cuenta las posiciones de presentación de una PICTURE, expandiendo -/// la repetición `C(n)`. `S` y `V` no ocupan posición. -fn pic_width(up: &str) -> usize { - let chars: Vec = up.chars().collect(); - let mut i = 0; - let mut total = 0usize; - while i < chars.len() { - let c = chars[i]; - i += 1; - if c == 'S' || c == 'V' { - continue; - } - let mut count = 1usize; - if chars.get(i) == Some(&'(') { - i += 1; - let start = i; - while i < chars.len() && chars[i].is_ascii_digit() { - i += 1; - } - if let Ok(n) = chars[start..i].iter().collect::().parse::() { - count = n; - } - if chars.get(i) == Some(&')') { - i += 1; - } - } - total += count; - } - total -} - /// Convierte un nombre COBOL en un identificador Rust válido. fn sanitize_ident(name: &str) -> String { let mut s: String = name diff --git a/crates/modules/charka/charka-ir/Cargo.toml b/crates/modules/charka/charka-ir/Cargo.toml index fabdade..b411a34 100644 --- a/crates/modules/charka/charka-ir/Cargo.toml +++ b/crates/modules/charka/charka-ir/Cargo.toml @@ -10,6 +10,7 @@ description = "charka-ir — representación intermedia: el AST de charka-parser [dependencies] charka-parser = { path = "../charka-parser" } +charka-bcd = { path = "../charka-bcd" } [dev-dependencies] charka-lexer = { path = "../charka-lexer" } diff --git a/crates/modules/charka/charka-ir/src/ast.rs b/crates/modules/charka/charka-ir/src/ast.rs index 81ee689..820d8a2 100644 --- a/crates/modules/charka/charka-ir/src/ast.rs +++ b/crates/modules/charka/charka-ir/src/ast.rs @@ -8,9 +8,12 @@ pub use charka_parser::{DataItem, Token}; pub struct Ir { /// El `PROGRAM-ID` ("" si el programa no lo declara). pub program_id: String, - /// El modelo de datos — el árbol de [`DataItem`] tal cual lo - /// produjo `charka-parser`. Sirve de tabla de símbolos. + /// El árbol de [`DataItem`] tal cual lo produjo `charka-parser`, + /// con su estructura de grupos. pub data: Vec, + /// El modelo de datos resuelto: los datos elementales aplanados y + /// los nombres de condición (nivel 88). + pub model: crate::model::DataModel, /// Los párrafos del PROCEDURE, con sus statements ya tipados. pub procedures: Vec, } diff --git a/crates/modules/charka/charka-ir/src/lib.rs b/crates/modules/charka/charka-ir/src/lib.rs index 3950f15..53aace2 100644 --- a/crates/modules/charka/charka-ir/src/lib.rs +++ b/crates/modules/charka/charka-ir/src/lib.rs @@ -27,10 +27,12 @@ mod ast; mod cursor; mod expr; mod kw; +mod model; mod stmt; pub use ast::*; pub use charka_parser::Program; +pub use model::{resolve_data, ConditionName, DataModel, Field, FieldKind}; use cursor::Cursor; @@ -55,6 +57,7 @@ pub fn lower(program: &Program) -> Ir { Ir { program_id: program.program_id.clone().unwrap_or_default(), data: program.data.clone(), + model: model::resolve_data(&program.data), procedures, } } diff --git a/crates/modules/charka/charka-ir/src/model.rs b/crates/modules/charka/charka-ir/src/model.rs new file mode 100644 index 0000000..f92a130 --- /dev/null +++ b/crates/modules/charka/charka-ir/src/model.rs @@ -0,0 +1,293 @@ +//! El modelo de datos resuelto: el árbol de `DataItem` aplanado a una +//! lista de campos elementales y a los nombres de condición (nivel 88). +//! +//! Es la fuente única de verdad sobre «qué tipo de campo describe una +//! PICTURE» — `charka-codegen` y `charka-shadow` la consumen en vez de +//! reimplementar cada uno la clasificación. + +use charka_bcd::{Decimal, Picture}; +use charka_parser::DataItem; + +use crate::ast::Operand; + +/// El tipo resuelto de un dato elemental. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum FieldKind { + /// Numérico: dígitos enteros, fraccionarios y si lleva signo. + Num { int: u8, frac: u8, signed: bool }, + /// Alfanumérico de longitud fija. + Text { len: usize }, +} + +/// Un dato elemental del programa, listo para materializarse. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct Field { + /// Nombre COBOL, en mayúsculas. + pub name: String, + /// Numérico o alfanumérico. + pub kind: FieldKind, + /// Valor inicial ya normalizado (de la cláusula `VALUE`). + pub init: String, +} + +/// Un nombre de condición — un dato de nivel 88. `IF ` equivale +/// a comparar `parent` con `value`. +#[derive(Debug, Clone, PartialEq)] +pub struct ConditionName { + /// Nombre del 88, en mayúsculas. + pub name: String, + /// El dato sobre el que se prueba la condición. + pub parent: String, + /// El valor que hace verdadera la condición. + pub value: Operand, +} + +/// El modelo de datos resuelto de un programa. +#[derive(Debug, Clone, PartialEq, Default)] +pub struct DataModel { + /// Los datos elementales, en orden de declaración. + pub fields: Vec, + /// Los nombres de condición (nivel 88). + pub conditions: Vec, +} + +impl DataModel { + /// Busca un campo por su nombre COBOL (sin distinguir mayúsculas). + pub fn field(&self, name: &str) -> Option<&Field> { + let up = name.to_uppercase(); + self.fields.iter().find(|f| f.name == up) + } + + /// Busca un nombre de condición. + pub fn condition(&self, name: &str) -> Option<&ConditionName> { + let up = name.to_uppercase(); + self.conditions.iter().find(|c| c.name == up) + } +} + +/// Aplana el árbol de datos en un [`DataModel`]. +pub fn resolve_data(data: &[DataItem]) -> DataModel { + let mut model = DataModel::default(); + walk(data, &mut model); + 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) { + for it in items { + if it.level == 66 || it.level == 88 { + // Los 88 los registra su dato padre; los 66 se omiten. + continue; + } + // Los hijos de nivel 88 son condiciones sobre este dato. + for child in &it.children { + if child.level == 88 { + model.conditions.push(ConditionName { + name: child.name.to_uppercase(), + parent: it.name.to_uppercase(), + value: condition_value(child.value.as_deref()), + }); + } + } + // 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); + } 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()), + }; + model.fields.push(Field { + name: it.name.to_uppercase(), + kind, + init, + }); + } + } + } +} + +/// Clasifica una cláusula PICTURE: alfanumérica si tiene `X`/`A`, +/// numérica si `charka-bcd` la parsea; una PICTURE de edición se trata +/// como texto de presentación. +fn classify(pic: Option<&str>) -> Option { + let up = pic?.to_uppercase(); + if up.contains('X') || up.contains('A') { + return Some(FieldKind::Text { + len: pic_width(&up).max(1), + }); + } + if let Ok(p) = Picture::parse(&up) { + return Some(FieldKind::Num { + int: p.integer_digits, + frac: p.fraction_digits, + signed: p.signed, + }); + } + Some(FieldKind::Text { + len: pic_width(&up).max(1), + }) +} + +/// Cuenta las posiciones de presentación de una PICTURE, expandiendo +/// la repetición `C(n)`. `S` y `V` no ocupan posición. +fn pic_width(up: &str) -> usize { + let chars: Vec = up.chars().collect(); + let mut i = 0; + let mut total = 0usize; + while i < chars.len() { + let c = chars[i]; + i += 1; + if c == 'S' || c == 'V' { + continue; + } + let mut count = 1usize; + if chars.get(i) == Some(&'(') { + i += 1; + let start = i; + while i < chars.len() && chars[i].is_ascii_digit() { + i += 1; + } + if let Ok(n) = chars[start..i].iter().collect::().parse::() { + count = n; + } + if chars.get(i) == Some(&')') { + i += 1; + } + } + total += count; + } + total +} + +/// Normaliza el `VALUE` de un campo numérico a un literal parseable. +fn numeric_value(v: Option<&str>) -> String { + let Some(raw) = v else { + return "0".to_string(); + }; + if matches!(raw.to_uppercase().as_str(), "ZERO" | "ZEROS" | "ZEROES") { + return "0".to_string(); + } + if Decimal::parse(raw).is_ok() { + raw.to_string() + } else { + "0".to_string() + } +} + +/// Normaliza el `VALUE` de un campo de texto. El parser envuelve los +/// literales de texto en comillas simples; aquí se desenvuelven. +fn text_value(v: Option<&str>) -> String { + let Some(raw) = v else { + return String::new(); + }; + let up = raw.to_uppercase(); + if matches!(up.as_str(), "SPACE" | "SPACES") { + return String::new(); + } + if matches!(up.as_str(), "ZERO" | "ZEROS" | "ZEROES") { + return "0".to_string(); + } + if raw.len() >= 2 && raw.starts_with('\'') && raw.ends_with('\'') { + raw[1..raw.len() - 1].to_string() + } else { + raw.to_string() + } +} + +/// El valor de un nivel 88 como [`Operand`]: literal de texto entre +/// comillas, número, o (si no es ninguno) texto crudo. +fn condition_value(value: Option<&str>) -> Operand { + let Some(raw) = value else { + return Operand::Num("0".to_string()); + }; + if raw.len() >= 2 && raw.starts_with('\'') && raw.ends_with('\'') { + return Operand::Str(raw[1..raw.len() - 1].to_string()); + } + if Decimal::parse(raw).is_ok() { + Operand::Num(raw.to_string()) + } else { + Operand::Str(raw.to_string()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use charka_lexer::{lex, SourceFormat}; + + fn model_of(src: &str) -> DataModel { + let toks = lex(src, SourceFormat::Free).unwrap(); + let program = charka_parser::parse(&toks).unwrap(); + resolve_data(&program.data) + } + + #[test] + fn flattens_elementary_fields() { + let m = model_of( + "DATA DIVISION.\n\ + 01 WS-N PIC 9(3) VALUE 7.\n\ + 01 WS-T PIC X(4) VALUE 'AB'.\n", + ); + assert_eq!(m.fields.len(), 2); + assert_eq!( + m.field("WS-N").unwrap().kind, + FieldKind::Num { + int: 3, + frac: 0, + signed: false + } + ); + assert_eq!(m.field("WS-N").unwrap().init, "7"); + assert_eq!(m.field("WS-T").unwrap().kind, FieldKind::Text { len: 4 }); + assert_eq!(m.field("WS-T").unwrap().init, "AB"); + } + + #[test] + fn group_items_are_not_fields_but_their_children_are() { + let m = model_of( + "DATA DIVISION.\n\ + 01 WS-REC.\n\ + 05 WS-A PIC 9(2).\n\ + 05 WS-B PIC X(3).\n", + ); + assert!(m.field("WS-REC").is_none()); + assert!(m.field("WS-A").is_some()); + assert!(m.field("WS-B").is_some()); + } + + #[test] + fn level_88_becomes_a_condition_on_its_parent() { + let m = model_of( + "DATA DIVISION.\n\ + 01 WS-FLAG PIC X VALUE 'N'.\n\ + 88 ES-SI VALUE 'Y'.\n\ + 88 ES-NO VALUE 'N'.\n", + ); + // El dato con hijos 88 sigue siendo un campo. + assert!(m.field("WS-FLAG").is_some()); + let si = m.condition("ES-SI").unwrap(); + assert_eq!(si.parent, "WS-FLAG"); + assert_eq!(si.value, Operand::Str("Y".into())); + assert_eq!( + m.condition("ES-NO").unwrap().value, + Operand::Str("N".into()) + ); + } + + #[test] + fn numeric_level_88_value() { + let m = model_of( + "DATA DIVISION.\n\ + 01 WS-COD PIC 9(2) VALUE 0.\n\ + 88 ES-OK VALUE 0.\n", + ); + assert_eq!( + m.condition("ES-OK").unwrap().value, + Operand::Num("0".into()) + ); + } +} diff --git a/crates/modules/charka/charka-shadow/src/field.rs b/crates/modules/charka/charka-shadow/src/field.rs index f7be2c6..745bbd5 100644 --- a/crates/modules/charka/charka-shadow/src/field.rs +++ b/crates/modules/charka/charka-shadow/src/field.rs @@ -1,12 +1,9 @@ -//! El estado de los datos durante la ejecución sombra: el árbol de -//! `DataItem` del IR se aplana a un mapa de campos vivos. -//! -//! La clasificación de PICTURE refleja la de `charka-codegen` — un -//! futuro refactor la unificaría en `charka-runtime`. +//! El estado de los datos durante la ejecución sombra: el modelo de +//! datos resuelto de `charka-ir` se materializa en campos vivos. use std::collections::HashMap; -use charka_ir::DataItem; +use charka_ir::{DataModel, FieldKind}; use charka_runtime::{Num, Picture, Text}; /// Un campo vivo: numérico o alfanumérico. @@ -15,114 +12,17 @@ pub(crate) enum Cell { Text(Text), } -/// Aplana el árbol de datos en un mapa `nombre COBOL → campo`. -pub(crate) fn build_fields(data: &[DataItem]) -> HashMap { +/// Materializa los campos del modelo en un mapa `nombre → campo`. +pub(crate) fn build_fields(model: &DataModel) -> HashMap { let mut map = HashMap::new(); - collect(data, &mut map); + for f in &model.fields { + let cell = match f.kind { + FieldKind::Num { int, frac, signed } => { + Cell::Num(Num::with_value(Picture::new(int, frac, signed), &f.init)) + } + FieldKind::Text { len } => Cell::Text(Text::with_value(len, &f.init)), + }; + map.entry(f.name.clone()).or_insert(cell); + } map } - -/// Recorre el árbol: los grupos no son campos (se recurre en sus -/// hijos); se saltan los niveles 88/66 y los `FILLER`. -fn collect(items: &[DataItem], map: &mut HashMap) { - for it in items { - if it.level == 88 || it.level == 66 { - continue; - } - if !it.children.is_empty() { - collect(&it.children, map); - continue; - } - if it.name == "FILLER" { - continue; - } - if let Some(cell) = make_cell(it.picture.as_deref(), it.value.as_deref()) { - map.entry(it.name.to_uppercase()).or_insert(cell); - } - } -} - -/// Construye un campo desde su PICTURE y su cláusula `VALUE`. -fn make_cell(pic: Option<&str>, value: Option<&str>) -> Option { - let up = pic?.to_uppercase(); - if up.contains('X') || up.contains('A') { - return Some(Cell::Text(Text::with_value( - pic_width(&up).max(1), - &text_value(value), - ))); - } - if let Ok(p) = Picture::parse(&up) { - return Some(Cell::Num(Num::with_value(p, &numeric_value(value)))); - } - // PICTURE de edición → campo de texto de presentación. - Some(Cell::Text(Text::with_value( - pic_width(&up).max(1), - &text_value(value), - ))) -} - -/// Cuenta las posiciones de presentación de una PICTURE, expandiendo -/// la repetición `C(n)`. `S` y `V` no ocupan posición. -fn pic_width(up: &str) -> usize { - let chars: Vec = up.chars().collect(); - let mut i = 0; - let mut total = 0usize; - while i < chars.len() { - let c = chars[i]; - i += 1; - if c == 'S' || c == 'V' { - continue; - } - let mut count = 1usize; - if chars.get(i) == Some(&'(') { - i += 1; - let start = i; - while i < chars.len() && chars[i].is_ascii_digit() { - i += 1; - } - if let Ok(n) = chars[start..i].iter().collect::().parse::() { - count = n; - } - if chars.get(i) == Some(&')') { - i += 1; - } - } - total += count; - } - total -} - -/// Normaliza el `VALUE` de un campo numérico a un literal parseable. -fn numeric_value(v: Option<&str>) -> String { - let Some(raw) = v else { - return "0".to_string(); - }; - if matches!(raw.to_uppercase().as_str(), "ZERO" | "ZEROS" | "ZEROES") { - return "0".to_string(); - } - if charka_runtime::Decimal::parse(raw).is_ok() { - raw.to_string() - } else { - "0".to_string() - } -} - -/// Normaliza el `VALUE` de un campo de texto. El parser envuelve los -/// literales de texto en comillas simples; aquí se desenvuelven. -fn text_value(v: Option<&str>) -> String { - let Some(raw) = v else { - return String::new(); - }; - let up = raw.to_uppercase(); - if matches!(up.as_str(), "SPACE" | "SPACES") { - return String::new(); - } - if matches!(up.as_str(), "ZERO" | "ZEROS" | "ZEROES") { - return "0".to_string(); - } - if raw.len() >= 2 && raw.starts_with('\'') && raw.ends_with('\'') { - raw[1..raw.len() - 1].to_string() - } else { - raw.to_string() - } -} diff --git a/crates/modules/charka/charka-shadow/src/interp.rs b/crates/modules/charka/charka-shadow/src/interp.rs index c6a70a9..9927624 100644 --- a/crates/modules/charka/charka-shadow/src/interp.rs +++ b/crates/modules/charka/charka-shadow/src/interp.rs @@ -8,7 +8,8 @@ use std::collections::HashMap; use charka_ir::{ - BinOp, CmpOp, Cond, Expr, Figurative, Ir, Operand, Perform, PerformControl, PerformTarget, Stmt, + BinOp, CmpOp, Cond, ConditionName, Expr, Figurative, Ir, Operand, Perform, PerformControl, + PerformTarget, Stmt, }; use charka_runtime::{cobol_text_cmp, Decimal, Rounding}; @@ -37,6 +38,7 @@ pub(crate) struct Machine<'a> { ir: &'a Ir, fields: HashMap, para_index: HashMap, + conditions: HashMap, pub output: Vec, budget: u64, pub step_limit_hit: bool, @@ -50,10 +52,17 @@ impl<'a> Machine<'a> { for (i, proc) in ir.procedures.iter().enumerate() { para_index.entry(proc.name.to_uppercase()).or_insert(i); } + let conditions = ir + .model + .conditions + .iter() + .map(|c| (c.name.clone(), c.clone())) + .collect(); Self { ir, - fields: build_fields(&ir.data), + fields: build_fields(&ir.model), para_index, + conditions, output: Vec::new(), budget: STEP_BUDGET, step_limit_hit: false, @@ -437,7 +446,12 @@ impl<'a> Machine<'a> { CmpOp::Ge => ord.is_ge(), } } - Cond::Named(_) => false, // nombres de condición (88): no soportado + Cond::Named(name) => match self.conditions.get(&name.to_uppercase()) { + // Un nombre de condición (88): el dato padre igual al + // valor que la hace verdadera. + Some(cn) => self.operands_equal(&Operand::Data(cn.parent.clone()), &cn.value), + None => false, + }, Cond::Not(inner) => !self.eval_cond(inner), Cond::And(a, b) => self.eval_cond(a) && self.eval_cond(b), Cond::Or(a, b) => self.eval_cond(a) || self.eval_cond(b), diff --git a/crates/modules/charka/charka-shadow/src/lib.rs b/crates/modules/charka/charka-shadow/src/lib.rs index 0fb2ac1..4f520ff 100644 --- a/crates/modules/charka/charka-shadow/src/lib.rs +++ b/crates/modules/charka/charka-shadow/src/lib.rs @@ -118,6 +118,7 @@ mod tests { corpus_test!(corpus_07_clasificar, "07-clasificar"); corpus_test!(corpus_08_varying, "08-varying"); corpus_test!(corpus_09_evaluar, "09-evaluar"); + corpus_test!(corpus_10_condicion, "10-condicion"); #[test] fn empty_source_runs_clean() { diff --git a/crates/modules/charka/corpus/10-condicion.cob b/crates/modules/charka/corpus/10-condicion.cob new file mode 100644 index 0000000..bf50b59 --- /dev/null +++ b/crates/modules/charka/corpus/10-condicion.cob @@ -0,0 +1,29 @@ +* corpus charka — nivel 5: nombres de condición (nivel 88) +IDENTIFICATION DIVISION. +PROGRAM-ID. SEMAFORO. +DATA DIVISION. +WORKING-STORAGE SECTION. +01 WS-LUZ PIC X(5) VALUE 'ROJO'. + 88 ES-PARE VALUE 'ROJO'. + 88 ES-SIGA VALUE 'VERDE'. +01 WS-CODIGO PIC 9(1) VALUE 0. + 88 ES-EXITO VALUE 0. +PROCEDURE DIVISION. +MAIN. + IF ES-PARE + DISPLAY 'LUZ ROJA: DETENERSE' + END-IF. + IF ES-EXITO + DISPLAY 'CODIGO OK' + END-IF. + MOVE 'VERDE' TO WS-LUZ. + MOVE 9 TO WS-CODIGO. + IF ES-SIGA + DISPLAY 'LUZ VERDE: AVANZAR' + END-IF. + IF ES-EXITO + DISPLAY 'CODIGO OK' + ELSE + DISPLAY 'CODIGO DE ERROR' + END-IF. + STOP RUN. diff --git a/crates/modules/charka/corpus/10-condicion.expected b/crates/modules/charka/corpus/10-condicion.expected new file mode 100644 index 0000000..679ad89 --- /dev/null +++ b/crates/modules/charka/corpus/10-condicion.expected @@ -0,0 +1,4 @@ +LUZ ROJA: DETENERSE +CODIGO OK +LUZ VERDE: AVANZAR +CODIGO DE ERROR diff --git a/crates/modules/charka/corpus/README.md b/crates/modules/charka/corpus/README.md index bdf8443..b2fed23 100644 --- a/crates/modules/charka/corpus/README.md +++ b/crates/modules/charka/corpus/README.md @@ -18,6 +18,7 @@ salida correcta, una línea por `DISPLAY`. | `07-clasificar` | 5 | `IF` anidado, condiciones con `AND` | | `08-varying` | 4 | `PERFORM VARYING` — el bucle con variable de control| | `09-evaluar` | 5 | `EVALUATE` — el `case` de COBOL, `WHEN` / `OTHER` | +| `10-condicion` | 5 | nombres de condición (nivel 88) en `IF` | ## Formato diff --git a/docs/changelog/charka.md b/docs/changelog/charka.md index 2653df4..fa8fb0a 100644 --- a/docs/changelog/charka.md +++ b/docs/changelog/charka.md @@ -3,6 +3,30 @@ 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): nombres de condición (nivel 88) + modelo de datos compartido + +`IF ES-VALIDO` — los nombres de condición de COBOL, que antes el +transpilador evaluaba siempre como `false`. Y, de paso, se elimina una +duplicación: la resolución del modelo de datos. + +- `charka-ir` gana un módulo `model`: `resolve_data(&[DataItem]) -> + DataModel` aplana el árbol de datos a campos elementales (`Field` con + `FieldKind`) y a nombres de condición (`ConditionName`). El `Ir` + ahora lleva un campo `model`. Es la fuente única de verdad sobre la + clasificación de PICTURE. +- `charka-codegen` y `charka-shadow` consumen `ir.model` en vez de + reimplementar cada uno la clasificación numérico/alfanumérico, el + ancho de PICTURE y la normalización de `VALUE`. `charka-codegen` ya + no depende de `charka-bcd`. +- `Cond::Named` (un nivel 88) se resuelve a `padre = valor`: el codegen + emite la comparación, el intérprete sombra la evalúa. +- Corregido de paso: un dato con hijos de nivel 88 (p. ej. + `01 WS-FLAG PIC X. 88 ES-SI VALUE 'Y'.`) antes se perdía como si + fuera un grupo; ahora se reconoce como campo elemental. +- Corpus: programa nuevo `10-condicion` (un semáforo con 88 de texto y + de número, en `IF` e `IF/ELSE`). Verificado: intérprete y crate + compilado dan la misma salida. + ### feat(charka): EVALUATE — el case de COBOL `EVALUATE` atraviesa el pipeline entero — antes el parser lo guardaba