⎇ 4df7478b717654172bb53fc60898fc24f6e80b28
crates/modules/charka/charka-codegen/src/sym.rs
Historia
Quién tocó qué
crudo
feat(charka): charka-codegen — emisión de Rust desde el IR
1//! Tabla de símbolos: el modelo de datos COBOL traducido a campos Rust.
2
3use std::collections::HashMap;
4
5use charka_ir::DataItem;
6
7/// Un campo del struct `Program` generado.
8pub(crate) struct Field {
9 /// Nombre COBOL en mayúsculas.
10 pub cobol: String,
11 /// Identificador Rust saneado y único.
12 pub ident: String,
13 /// Numérico o alfanumérico.
14 pub kind: FieldKind,
15 /// La cláusula `VALUE`, si la hay.
16 pub value: Option<String>,
17}
18
19/// El tipo de un campo elemental.
20pub(crate) enum FieldKind {
21 /// Campo numérico — se emite como `Num`.
22 Num { int: u8, frac: u8, signed: bool },
23 /// Campo alfanumérico — se emite como `Text`.
24 Text { len: usize },
25}
26
27/// El conjunto de campos del programa, indexado por nombre COBOL.
28pub(crate) struct Symbols {
29 pub fields: Vec<Field>,
30 by_name: HashMap<String, usize>,
31}
32
33impl Symbols {
34 /// Construye la tabla recorriendo el árbol de datos.
35 pub(crate) fn build(data: &[DataItem]) -> Self {
36 let mut fields = Vec::new();
37 collect(data, &mut fields);
38 dedup_idents(&mut fields);
39 let by_name = fields
40 .iter()
41 .enumerate()
42 .map(|(i, f)| (f.cobol.clone(), i))
43 .collect();
44 Self { fields, by_name }
45 }
46
47 /// Busca un campo por su nombre COBOL (sin distinguir mayúsculas).
48 pub(crate) fn lookup(&self, cobol: &str) -> Option<&Field> {
49 self.by_name
50 .get(&cobol.to_uppercase())
51 .map(|&i| &self.fields[i])
52 }
53}
54
55/// Recoge los datos elementales del árbol. Los grupos no son campos —
56/// se recurre en sus hijos. Se saltan niveles 88/66 y los `FILLER`.
57fn collect(items: &[DataItem], out: &mut Vec<Field>) {
58 for it in items {
59 if it.level == 88 || it.level == 66 {
60 continue;
61 }
62 if !it.children.is_empty() {
63 collect(&it.children, out);
64 continue;
65 }
66 if it.name == "FILLER" {
67 continue;
68 }
69 let Some(kind) = classify(it.picture.as_deref()) else {
70 continue;
71 };
72 out.push(Field {
73 cobol: it.name.clone(),
74 ident: sanitize_ident(&it.name),
75 kind,
76 value: it.value.clone(),
77 });
78 }
79}
80
81/// Dos datos pueden compartir nombre (COBOL los califica); aquí, como
82/// son campos de un struct, sus identificadores deben ser únicos.
83fn dedup_idents(fields: &mut [Field]) {
84 let mut seen: HashMap<String, u32> = HashMap::new();
85 for f in fields.iter_mut() {
86 let n = seen.entry(f.ident.clone()).or_insert(0);
87 if *n > 0 {
88 f.ident = format!("{}_{}", f.ident, n);
89 }
90 *n += 1;
91 }
92}
93
94/// Clasifica una cláusula PICTURE: alfanumérica si tiene `X`/`A`,
95/// numérica si `charka-bcd` la parsea; una PICTURE de edición se trata
96/// como texto de presentación.
97fn classify(pic: Option<&str>) -> Option<FieldKind> {
98 let up = pic?.to_uppercase();
99 if up.contains('X') || up.contains('A') {
100 return Some(FieldKind::Text {
101 len: pic_width(&up).max(1),
102 });
103 }
104 if let Ok(p) = charka_bcd::Picture::parse(&up) {
105 return Some(FieldKind::Num {
106 int: p.integer_digits,
107 frac: p.fraction_digits,
108 signed: p.signed,
109 });
110 }
111 Some(FieldKind::Text {
112 len: pic_width(&up).max(1),
113 })
114}
115
116/// Cuenta las posiciones de presentación de una PICTURE, expandiendo
117/// la repetición `C(n)`. `S` y `V` no ocupan posición.
118fn pic_width(up: &str) -> usize {
119 let chars: Vec<char> = up.chars().collect();
120 let mut i = 0;
121 let mut total = 0usize;
122 while i < chars.len() {
123 let c = chars[i];
124 i += 1;
125 if c == 'S' || c == 'V' {
126 continue;
127 }
128 let mut count = 1usize;
129 if chars.get(i) == Some(&'(') {
130 i += 1;
131 let start = i;
132 while i < chars.len() && chars[i].is_ascii_digit() {
133 i += 1;
134 }
135 if let Ok(n) = chars[start..i].iter().collect::<String>().parse::<usize>() {
136 count = n;
137 }
138 if chars.get(i) == Some(&')') {
139 i += 1;
140 }
141 }
142 total += count;
143 }
144 total
145}
146
147/// Convierte un nombre COBOL en un identificador Rust válido.
148fn sanitize_ident(name: &str) -> String {
149 let mut s: String = name
150 .chars()
151 .map(|c| {
152 if c == '-' {
153 '_'
154 } else {
155 c.to_ascii_lowercase()
156 }
157 })
158 .filter(|c| c.is_ascii_alphanumeric() || *c == '_')
159 .collect();
160 if s.is_empty() || s.starts_with(|c: char| c.is_ascii_digit()) {
161 s = format!("f_{s}");
162 }
163 if is_rust_keyword(&s) {
164 s.push('_');
165 }
166 s
167}
168
169/// El nombre del método de un párrafo. El párrafo implícito ("") es
170/// `p_start`; el resto lleva el prefijo `p_`.
171pub(crate) fn paragraph_method(name: &str) -> String {
172 if name.is_empty() {
173 return "p_start".to_string();
174 }
175 let body: String = name
176 .chars()
177 .map(|c| {
178 if c == '-' {
179 '_'
180 } else {
181 c.to_ascii_lowercase()
182 }
183 })
184 .filter(|c| c.is_ascii_alphanumeric() || *c == '_')
185 .collect();
186 format!("p_{body}")
187}
188
189/// ¿Es `s` una palabra reservada de Rust? (Para no chocar al nombrar
190/// campos — un dato COBOL `MOVE` se volvería el keyword `move`.)
191fn is_rust_keyword(s: &str) -> bool {
192 matches!(
193 s,
194 "as" | "break"
195 | "const"
196 | "continue"
197 | "crate"
198 | "dyn"
199 | "else"
200 | "enum"
201 | "extern"
202 | "false"
203 | "fn"
204 | "for"
205 | "if"
206 | "impl"
207 | "in"
208 | "let"
209 | "loop"
210 | "match"
211 | "mod"
212 | "move"
213 | "mut"
214 | "pub"
215 | "ref"
216 | "return"
217 | "self"
218 | "static"
219 | "struct"
220 | "super"
221 | "trait"
222 | "true"
223 | "type"
224 | "unsafe"
225 | "use"
226 | "where"
227 | "while"
228 | "async"
229 | "await"
230 | "box"
231 | "yield"
232 )
233}