⎇ 634a43006ac4257288af40320b5360e9f157ab09
crates/modules/charka/charka-codegen/src/sym.rs
Ñawpa willay
Pi imata llamkarqan
ch'uya
feat(charka): PICTURE de edición — Z, coma de millares y punto decimal
1//! Tabla de símbolos del código generado: los campos del `struct
2//! Program` y los nombres de condición, derivados del modelo de datos
3//! resuelto que entrega `charka-ir`.
4
5use std::collections::HashMap;
6
7use charka_ir::{ConditionName, Ir};
8
9/// El tipo de campo lo aporta `charka-ir`; se reexporta para que el
10/// resto del crate lo nombre como `crate::sym::FieldKind`.
11pub(crate) use charka_ir::FieldKind;
12
13/// Un campo del struct `Program` generado.
14pub(crate) struct Field {
15 /// Nombre COBOL en mayúsculas.
16 pub cobol: String,
17 /// Identificador Rust saneado y único.
18 pub ident: String,
19 /// Numérico o alfanumérico.
20 pub kind: FieldKind,
21 /// Valor inicial normalizado (de la cláusula `VALUE`).
22 pub init: String,
23 /// Si es una tabla (`OCCURS n`), su número de elementos.
24 pub occurs: Option<u32>,
25 /// Si es un campo de edición, su PICTURE.
26 pub edit: Option<String>,
27}
28
29/// Un fichero del programa generado.
30pub(crate) struct FileSym {
31 /// Nombre COBOL del fichero.
32 pub cobol: String,
33 /// Identificador Rust del campo `CobFile` (prefijo `file_`).
34 pub ident: String,
35 /// Ruta a la que está asignado.
36 pub path: String,
37 /// Nombre COBOL del registro asociado (su `FD`).
38 pub record: String,
39}
40
41/// Los campos del programa, sus nombres de condición, sus grupos, sus
42/// párrafos y sus ficheros.
43pub(crate) struct Symbols {
44 pub fields: Vec<Field>,
45 by_name: HashMap<String, usize>,
46 conditions: HashMap<String, ConditionName>,
47 groups: HashMap<String, Vec<String>>,
48 /// Los párrafos en orden: `(nombre COBOL, nombre de método Rust)`.
49 pub paragraphs: Vec<(String, String)>,
50 /// Los ficheros declarados.
51 pub files: Vec<FileSym>,
52}
53
54impl Symbols {
55 /// Construye la tabla desde el IR (su modelo de datos y párrafos).
56 pub(crate) fn build(ir: &Ir) -> Self {
57 let model = &ir.model;
58 let mut fields: Vec<Field> = model
59 .fields
60 .iter()
61 .map(|f| Field {
62 cobol: f.name.clone(),
63 ident: sanitize_ident(&f.name),
64 kind: f.kind,
65 init: f.init.clone(),
66 occurs: f.occurs,
67 edit: f.edit.clone(),
68 })
69 .collect();
70 dedup_idents(&mut fields);
71 let by_name = fields
72 .iter()
73 .enumerate()
74 .map(|(i, f)| (f.cobol.clone(), i))
75 .collect();
76 let conditions = model
77 .conditions
78 .iter()
79 .map(|c| (c.name.clone(), c.clone()))
80 .collect();
81 let groups = model
82 .groups
83 .iter()
84 .map(|g| (g.name.clone(), g.members.clone()))
85 .collect();
86 // Párrafos en orden, con su nombre de método único.
87 let mut seen: HashMap<String, u32> = HashMap::new();
88 let paragraphs = ir
89 .procedures
90 .iter()
91 .map(|proc| {
92 let base = paragraph_method(&proc.name);
93 let n = seen.entry(base.clone()).or_insert(0);
94 let method = if *n > 0 { format!("{base}_{n}") } else { base };
95 *n += 1;
96 (proc.name.to_uppercase(), method)
97 })
98 .collect();
99 let files = ir
100 .files
101 .iter()
102 .map(|f| FileSym {
103 cobol: f.name.clone(),
104 ident: format!("file_{}", sanitize_ident(&f.name)),
105 path: f.path.clone(),
106 record: f.record.clone(),
107 })
108 .collect();
109 Self {
110 fields,
111 by_name,
112 conditions,
113 groups,
114 paragraphs,
115 files,
116 }
117 }
118
119 /// Busca un fichero por su nombre COBOL.
120 pub(crate) fn file(&self, name: &str) -> Option<&FileSym> {
121 let up = name.to_uppercase();
122 self.files.iter().find(|f| f.cobol == up)
123 }
124
125 /// Busca el fichero cuyo registro `FD` es `record`.
126 pub(crate) fn file_of_record(&self, record: &str) -> Option<&FileSym> {
127 let up = record.to_uppercase();
128 self.files.iter().find(|f| f.record == up)
129 }
130
131 /// Los métodos a llamar para un `PERFORM name [THRU thru]`: el
132 /// rango de párrafos desde `name` hasta `thru` inclusive.
133 pub(crate) fn paragraph_range(&self, name: &str, thru: Option<&str>) -> Vec<String> {
134 let up = name.to_uppercase();
135 let Some(start) = self.paragraphs.iter().position(|(c, _)| *c == up) else {
136 return vec![paragraph_method(name)];
137 };
138 let end = match thru {
139 Some(t) => {
140 let tu = t.to_uppercase();
141 self.paragraphs
142 .iter()
143 .position(|(c, _)| *c == tu)
144 .unwrap_or(start)
145 }
146 None => start,
147 };
148 let (lo, hi) = if start <= end {
149 (start, end)
150 } else {
151 (end, start)
152 };
153 self.paragraphs[lo..=hi]
154 .iter()
155 .map(|(_, m)| m.clone())
156 .collect()
157 }
158
159 /// Los miembros de un grupo, si `name` es un grupo.
160 pub(crate) fn group(&self, name: &str) -> Option<&[String]> {
161 self.groups.get(&name.to_uppercase()).map(|v| v.as_slice())
162 }
163
164 /// Busca un campo por su nombre COBOL (sin distinguir mayúsculas).
165 pub(crate) fn lookup(&self, cobol: &str) -> Option<&Field> {
166 self.by_name
167 .get(&cobol.to_uppercase())
168 .map(|&i| &self.fields[i])
169 }
170
171 /// Busca un nombre de condición (un dato de nivel 88).
172 pub(crate) fn condition(&self, name: &str) -> Option<&ConditionName> {
173 self.conditions.get(&name.to_uppercase())
174 }
175}
176
177/// Dos datos pueden compartir nombre (COBOL los califica); aquí, como
178/// son campos de un struct, sus identificadores deben ser únicos.
179fn dedup_idents(fields: &mut [Field]) {
180 let mut seen: HashMap<String, u32> = HashMap::new();
181 for f in fields.iter_mut() {
182 let n = seen.entry(f.ident.clone()).or_insert(0);
183 if *n > 0 {
184 f.ident = format!("{}_{}", f.ident, n);
185 }
186 *n += 1;
187 }
188}
189
190/// Convierte un nombre COBOL en un identificador Rust válido.
191fn sanitize_ident(name: &str) -> String {
192 let mut s: String = name
193 .chars()
194 .map(|c| {
195 if c == '-' {
196 '_'
197 } else {
198 c.to_ascii_lowercase()
199 }
200 })
201 .filter(|c| c.is_ascii_alphanumeric() || *c == '_')
202 .collect();
203 if s.is_empty() || s.starts_with(|c: char| c.is_ascii_digit()) {
204 s = format!("f_{s}");
205 }
206 if is_rust_keyword(&s) {
207 s.push('_');
208 }
209 s
210}
211
212/// El nombre del método de un párrafo. El párrafo implícito ("") es
213/// `p_start`; el resto lleva el prefijo `p_`.
214pub(crate) fn paragraph_method(name: &str) -> String {
215 if name.is_empty() {
216 return "p_start".to_string();
217 }
218 let body: String = name
219 .chars()
220 .map(|c| {
221 if c == '-' {
222 '_'
223 } else {
224 c.to_ascii_lowercase()
225 }
226 })
227 .filter(|c| c.is_ascii_alphanumeric() || *c == '_')
228 .collect();
229 format!("p_{body}")
230}
231
232/// ¿Es `s` una palabra reservada de Rust? (Para no chocar al nombrar
233/// campos — un dato COBOL `MOVE` se volvería el keyword `move`.)
234fn is_rust_keyword(s: &str) -> bool {
235 matches!(
236 s,
237 "as" | "break"
238 | "const"
239 | "continue"
240 | "crate"
241 | "dyn"
242 | "else"
243 | "enum"
244 | "extern"
245 | "false"
246 | "fn"
247 | "for"
248 | "if"
249 | "impl"
250 | "in"
251 | "let"
252 | "loop"
253 | "match"
254 | "mod"
255 | "move"
256 | "mut"
257 | "pub"
258 | "ref"
259 | "return"
260 | "self"
261 | "static"
262 | "struct"
263 | "super"
264 | "trait"
265 | "true"
266 | "type"
267 | "unsafe"
268 | "use"
269 | "where"
270 | "while"
271 | "async"
272 | "await"
273 | "box"
274 | "yield"
275 )
276}