tawasuyu

tawasuyu/brahman

Rust 506TOML 198Markdown 56JSON 197212 Definitions

⎇ fa65f202065abca26e0fb28581427117b84af85c crates/modules/charka/charka-ir/src/model.rs Rust · 327 lines · 10.3 KB History Who touched what raw

feat(charka): INITIALIZE — resetear datos y grupos fa65f2020 · sergio ·

1//! El modelo de datos resuelto: el árbol de `DataItem` aplanado a una
2//! lista de campos elementales y a los nombres de condición (nivel 88).
3//!
4//! Es la fuente única de verdad sobre «qué tipo de campo describe una
5//! PICTURE» — `charka-codegen` y `charka-shadow` la consumen en vez de
6//! reimplementar cada uno la clasificación.
7
8use charka_bcd::{Decimal, Picture};
9use charka_parser::DataItem;
10
11use crate::ast::Operand;
12
13/// El tipo resuelto de un dato elemental.
14#[derive(Debug, Clone, Copy, PartialEq, Eq)]
15pub enum FieldKind {
16    /// Numérico: dígitos enteros, fraccionarios y si lleva signo.
17    Num { int: u8, frac: u8, signed: bool },
18    /// Alfanumérico de longitud fija.
19    Text { len: usize },
20}
21
22/// Un dato elemental del programa, listo para materializarse.
23#[derive(Debug, Clone, PartialEq, Eq)]
24pub struct Field {
25    /// Nombre COBOL, en mayúsculas.
26    pub name: String,
27    /// Numérico o alfanumérico.
28    pub kind: FieldKind,
29    /// Valor inicial ya normalizado (de la cláusula `VALUE`).
30    pub init: String,
31    /// Si es una tabla (`OCCURS n`), su número de elementos; `None`
32    /// para un dato escalar.
33    pub occurs: Option<u32>,
34}
35
36/// Un nombre de condición — un dato de nivel 88. `IF <name>` equivale
37/// a comparar `parent` con `value`.
38#[derive(Debug, Clone, PartialEq)]
39pub struct ConditionName {
40    /// Nombre del 88, en mayúsculas.
41    pub name: String,
42    /// El dato sobre el que se prueba la condición.
43    pub parent: String,
44    /// El valor que hace verdadera la condición.
45    pub value: Operand,
46}
47
48/// Un grupo de datos: su nombre y los datos elementales que contiene
49/// (recursivamente).
50#[derive(Debug, Clone, PartialEq, Eq)]
51pub struct GroupInfo {
52    /// Nombre COBOL del grupo, en mayúsculas.
53    pub name: String,
54    /// Nombres COBOL de los datos elementales que agrupa.
55    pub members: Vec<String>,
56}
57
58/// El modelo de datos resuelto de un programa.
59#[derive(Debug, Clone, PartialEq, Default)]
60pub struct DataModel {
61    /// Los datos elementales, en orden de declaración.
62    pub fields: Vec<Field>,
63    /// Los nombres de condición (nivel 88).
64    pub conditions: Vec<ConditionName>,
65    /// Los grupos y sus datos elementales.
66    pub groups: Vec<GroupInfo>,
67}
68
69impl DataModel {
70    /// Busca un campo por su nombre COBOL (sin distinguir mayúsculas).
71    pub fn field(&self, name: &str) -> Option<&Field> {
72        let up = name.to_uppercase();
73        self.fields.iter().find(|f| f.name == up)
74    }
75
76    /// Busca un nombre de condición.
77    pub fn condition(&self, name: &str) -> Option<&ConditionName> {
78        let up = name.to_uppercase();
79        self.conditions.iter().find(|c| c.name == up)
80    }
81
82    /// Busca un grupo por su nombre COBOL.
83    pub fn group(&self, name: &str) -> Option<&GroupInfo> {
84        let up = name.to_uppercase();
85        self.groups.iter().find(|g| g.name == up)
86    }
87}
88
89/// Aplana el árbol de datos en un [`DataModel`].
90pub fn resolve_data(data: &[DataItem]) -> DataModel {
91    let mut model = DataModel::default();
92    walk(data, &mut model);
93    model
94}
95
96/// Recorre el árbol: registra los 88 como condiciones, los grupos con
97/// sus miembros, y emite los datos elementales. Devuelve los nombres
98/// de los datos elementales producidos (para que el grupo que llama
99/// los reúna como sus miembros).
100fn walk(items: &[DataItem], model: &mut DataModel) -> Vec<String> {
101    let mut produced = Vec::new();
102    for it in items {
103        if it.level == 66 || it.level == 88 {
104            // Los 88 los registra su dato padre; los 66 se omiten.
105            continue;
106        }
107        // Los hijos de nivel 88 son condiciones sobre este dato.
108        for child in &it.children {
109            if child.level == 88 {
110                model.conditions.push(ConditionName {
111                    name: child.name.to_uppercase(),
112                    parent: it.name.to_uppercase(),
113                    value: condition_value(child.value.as_deref()),
114                });
115            }
116        }
117        // Un dato con hijos «reales» (no 88/66) es un grupo.
118        let is_group = it.children.iter().any(|c| c.level != 88 && c.level != 66);
119        if is_group {
120            let members = walk(&it.children, model);
121            if it.name != "FILLER" {
122                model.groups.push(GroupInfo {
123                    name: it.name.to_uppercase(),
124                    members: members.clone(),
125                });
126            }
127            produced.extend(members);
128        } else if it.name != "FILLER" {
129            if let Some(kind) = classify(it.picture.as_deref()) {
130                let init = match kind {
131                    FieldKind::Num { .. } => numeric_value(it.value.as_deref()),
132                    FieldKind::Text { .. } => text_value(it.value.as_deref()),
133                };
134                produced.push(it.name.to_uppercase());
135                model.fields.push(Field {
136                    name: it.name.to_uppercase(),
137                    kind,
138                    init,
139                    occurs: it.occurs,
140                });
141            }
142        }
143    }
144    produced
145}
146
147/// Clasifica una cláusula PICTURE: alfanumérica si tiene `X`/`A`,
148/// numérica si `charka-bcd` la parsea; una PICTURE de edición se trata
149/// como texto de presentación.
150fn classify(pic: Option<&str>) -> Option<FieldKind> {
151    let up = pic?.to_uppercase();
152    if up.contains('X') || up.contains('A') {
153        return Some(FieldKind::Text {
154            len: pic_width(&up).max(1),
155        });
156    }
157    if let Ok(p) = Picture::parse(&up) {
158        return Some(FieldKind::Num {
159            int: p.integer_digits,
160            frac: p.fraction_digits,
161            signed: p.signed,
162        });
163    }
164    Some(FieldKind::Text {
165        len: pic_width(&up).max(1),
166    })
167}
168
169/// Cuenta las posiciones de presentación de una PICTURE, expandiendo
170/// la repetición `C(n)`. `S` y `V` no ocupan posición.
171fn pic_width(up: &str) -> usize {
172    let chars: Vec<char> = up.chars().collect();
173    let mut i = 0;
174    let mut total = 0usize;
175    while i < chars.len() {
176        let c = chars[i];
177        i += 1;
178        if c == 'S' || c == 'V' {
179            continue;
180        }
181        let mut count = 1usize;
182        if chars.get(i) == Some(&'(') {
183            i += 1;
184            let start = i;
185            while i < chars.len() && chars[i].is_ascii_digit() {
186                i += 1;
187            }
188            if let Ok(n) = chars[start..i].iter().collect::<String>().parse::<usize>() {
189                count = n;
190            }
191            if chars.get(i) == Some(&')') {
192                i += 1;
193            }
194        }
195        total += count;
196    }
197    total
198}
199
200/// Normaliza el `VALUE` de un campo numérico a un literal parseable.
201fn numeric_value(v: Option<&str>) -> String {
202    let Some(raw) = v else {
203        return "0".to_string();
204    };
205    if matches!(raw.to_uppercase().as_str(), "ZERO" | "ZEROS" | "ZEROES") {
206        return "0".to_string();
207    }
208    if Decimal::parse(raw).is_ok() {
209        raw.to_string()
210    } else {
211        "0".to_string()
212    }
213}
214
215/// Normaliza el `VALUE` de un campo de texto. El parser envuelve los
216/// literales de texto en comillas simples; aquí se desenvuelven.
217fn text_value(v: Option<&str>) -> String {
218    let Some(raw) = v else {
219        return String::new();
220    };
221    let up = raw.to_uppercase();
222    if matches!(up.as_str(), "SPACE" | "SPACES") {
223        return String::new();
224    }
225    if matches!(up.as_str(), "ZERO" | "ZEROS" | "ZEROES") {
226        return "0".to_string();
227    }
228    if raw.len() >= 2 && raw.starts_with('\'') && raw.ends_with('\'') {
229        raw[1..raw.len() - 1].to_string()
230    } else {
231        raw.to_string()
232    }
233}
234
235/// El valor de un nivel 88 como [`Operand`]: literal de texto entre
236/// comillas, número, o (si no es ninguno) texto crudo.
237fn condition_value(value: Option<&str>) -> Operand {
238    let Some(raw) = value else {
239        return Operand::Num("0".to_string());
240    };
241    if raw.len() >= 2 && raw.starts_with('\'') && raw.ends_with('\'') {
242        return Operand::Str(raw[1..raw.len() - 1].to_string());
243    }
244    if Decimal::parse(raw).is_ok() {
245        Operand::Num(raw.to_string())
246    } else {
247        Operand::Str(raw.to_string())
248    }
249}
250
251#[cfg(test)]
252mod tests {
253    use super::*;
254    use charka_lexer::{lex, SourceFormat};
255
256    fn model_of(src: &str) -> DataModel {
257        let toks = lex(src, SourceFormat::Free).unwrap();
258        let program = charka_parser::parse(&toks).unwrap();
259        resolve_data(&program.data)
260    }
261
262    #[test]
263    fn flattens_elementary_fields() {
264        let m = model_of(
265            "DATA DIVISION.\n\
266             01 WS-N PIC 9(3) VALUE 7.\n\
267             01 WS-T PIC X(4) VALUE 'AB'.\n",
268        );
269        assert_eq!(m.fields.len(), 2);
270        assert_eq!(
271            m.field("WS-N").unwrap().kind,
272            FieldKind::Num {
273                int: 3,
274                frac: 0,
275                signed: false
276            }
277        );
278        assert_eq!(m.field("WS-N").unwrap().init, "7");
279        assert_eq!(m.field("WS-T").unwrap().kind, FieldKind::Text { len: 4 });
280        assert_eq!(m.field("WS-T").unwrap().init, "AB");
281    }
282
283    #[test]
284    fn group_items_are_not_fields_but_their_children_are() {
285        let m = model_of(
286            "DATA DIVISION.\n\
287             01 WS-REC.\n\
288                05 WS-A PIC 9(2).\n\
289                05 WS-B PIC X(3).\n",
290        );
291        assert!(m.field("WS-REC").is_none());
292        assert!(m.field("WS-A").is_some());
293        assert!(m.field("WS-B").is_some());
294    }
295
296    #[test]
297    fn level_88_becomes_a_condition_on_its_parent() {
298        let m = model_of(
299            "DATA DIVISION.\n\
300             01 WS-FLAG PIC X VALUE 'N'.\n\
301                88 ES-SI VALUE 'Y'.\n\
302                88 ES-NO VALUE 'N'.\n",
303        );
304        // El dato con hijos 88 sigue siendo un campo.
305        assert!(m.field("WS-FLAG").is_some());
306        let si = m.condition("ES-SI").unwrap();
307        assert_eq!(si.parent, "WS-FLAG");
308        assert_eq!(si.value, Operand::Str("Y".into()));
309        assert_eq!(
310            m.condition("ES-NO").unwrap().value,
311            Operand::Str("N".into())
312        );
313    }
314
315    #[test]
316    fn numeric_level_88_value() {
317        let m = model_of(
318            "DATA DIVISION.\n\
319             01 WS-COD PIC 9(2) VALUE 0.\n\
320                88 ES-OK VALUE 0.\n",
321        );
322        assert_eq!(
323            m.condition("ES-OK").unwrap().value,
324            Operand::Num("0".into())
325        );
326    }
327}