gioser

tawasuyu/brahman

Rust 582TOML 222Markdown 73JSON 198635 Definitions

feat(charka): nivel 88 + modelo de datos compartido en charka-ir

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 <noreply@anthropic.com>

28ee1ae2602662b216549ba4db0d34a9b55b8deb · by sergio · (5 months ago) · parent 4df7478b7 · browse the tree at this commit

Semantic delta · 45

crates/modules/charka/charka-codegen/src/expr.rs

crates/modules/charka/charka-codegen/src/lib.rs

crates/modules/charka/charka-codegen/src/sym.rs

crates/modules/charka/charka-ir/src/ast.rs

  • signature changed struct Ir pub

crates/modules/charka/charka-ir/src/lib.rs

crates/modules/charka/charka-ir/src/model.rs

crates/modules/charka/charka-shadow/src/field.rs

crates/modules/charka/charka-shadow/src/interp.rs

17 files · +472 −264

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<DataItem>, procedures: Vec<Procedure> }`.
-  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> }`. `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<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 },
+    /// Valor inicial normalizado (de la cláusula `VALUE`).
+    pub init: String,
 }
 
-/// 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<Field>,
     by_name: HashMap<String, usize>,
+    conditions: HashMap<String, ConditionName>,
 }
 
 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<Field> = 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<Field>) {
-    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<FieldKind> {
-    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<char> = 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::<String>().parse::<usize>() {
-                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<DataItem>,
+    /// 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<Procedure>,
 }
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 <name>` 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<Field>,
+    /// Los nombres de condición (nivel 88).
+    pub conditions: Vec<ConditionName>,
+}
+
+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<FieldKind> {
+    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<char> = 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::<String>().parse::<usize>() {
+                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<String, Cell> {
+/// Materializa los campos del modelo en un mapa `nombre → campo`.
+pub(crate) fn build_fields(model: &DataModel) -> HashMap<String, Cell> {
     let mut map = HashMap::new();
-    collect(data, &mut map);
-    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<String, Cell>) {
-    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<Cell> {
-    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<char> = 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::<String>().parse::<usize>() {
-                count = n;
+    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))
             }
-            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()
+            FieldKind::Text { len } => Cell::Text(Text::with_value(len, &f.init)),
+        };
+        map.entry(f.name.clone()).or_insert(cell);
     }
+    map
 }
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<String, Cell>,
     para_index: HashMap<String, usize>,
+    conditions: HashMap<String, ConditionName>,
     pub output: Vec<String>,
     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