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