⎇ 71a4068d12e81d0be7b08d0717114dd5b10868fe
crates/modules/charka/charka-ir/src/lib.rs
Ñawpa willay
Pi imata llamkarqan
ch'uya
feat(charka): charka-ir — representación intermedia con statements tipados
1//! `charka-ir` — la representación intermedia del transpilador.
2//!
3//! Tercera etapa del pipeline COBOL→Rust: toma el [`Program`] de
4//! `charka-parser` (cuyo PROCEDURE division es una lista de sentencias
5//! con tokens crudos) y produce un [`Ir`] donde cada sentencia ya es un
6//! árbol de [`Stmt`] tipados: `MOVE`, `IF`, `PERFORM`, `COMPUTE`, los
7//! verbos aritméticos, etc.
8//!
9//! El modelo de datos (`DATA division`) pasa tal cual — el árbol de
10//! [`DataItem`] que ya armó el parser sirve de tabla de símbolos.
11//!
12//! El lowering es **tolerante y total**: nunca falla. Un verbo que la
13//! v1 no parsea se conserva como [`Stmt::Unknown`] con sus tokens
14//! crudos, listo para que el codegen (o un humano) lo revise.
15//!
16//! Alcance v1 — los verbos parseados a fondo: `MOVE`, `DISPLAY`,
17//! `ACCEPT`, `COMPUTE` (con expresiones con precedencia), `ADD`,
18//! `SUBTRACT`, `MULTIPLY`, `DIVIDE`, `IF`/`ELSE`/`END-IF` (con
19//! condiciones `AND`/`OR`/`NOT`), `PERFORM` (fuera de línea, en línea,
20//! `TIMES`, `UNTIL`), `GO TO`, `STOP RUN`, `GOBACK`, `EXIT`,
21//! `CONTINUE`. Fuera de alcance: `EVALUATE`, `STRING`/`UNSTRING`, E/S
22//! de ficheros, `PERFORM VARYING`, CICS y SQL embebido.
23
24#![forbid(unsafe_code)]
25
26mod ast;
27mod cursor;
28mod expr;
29mod kw;
30mod stmt;
31
32pub use ast::*;
33pub use charka_parser::Program;
34
35use cursor::Cursor;
36
37/// Baja un [`Program`] parseado a la representación intermedia.
38pub fn lower(program: &Program) -> Ir {
39 let procedures = program
40 .paragraphs
41 .iter()
42 .map(|p| {
43 let mut body = Vec::new();
44 for sentence in &p.sentences {
45 let mut cur = Cursor::new(&sentence.tokens);
46 body.extend(stmt::parse_statements(&mut cur, &[]));
47 }
48 Procedure {
49 name: p.name.clone(),
50 body,
51 }
52 })
53 .collect();
54
55 Ir {
56 program_id: program.program_id.clone().unwrap_or_default(),
57 data: program.data.clone(),
58 procedures,
59 }
60}
61
62#[cfg(test)]
63mod tests {
64 use super::*;
65 use charka_lexer::{lex, SourceFormat};
66
67 /// Helper: lexa, parsea y baja a IR un fuente en formato libre.
68 fn ir(src: &str) -> Ir {
69 let toks = lex(src, SourceFormat::Free).expect("lex OK");
70 let program = charka_parser::parse(&toks).expect("parse OK");
71 lower(&program)
72 }
73
74 /// Helper: el cuerpo del primer (y normalmente único) párrafo.
75 fn body(src: &str) -> Vec<Stmt> {
76 let prog = format!("PROCEDURE DIVISION.\nMAIN.\n{src}\n");
77 ir(&prog).procedures.into_iter().next().unwrap().body
78 }
79
80 #[test]
81 fn empty_program_lowers_to_default() {
82 let got = lower(&charka_parser::parse(&[]).unwrap());
83 assert_eq!(got, Ir::default());
84 }
85
86 #[test]
87 fn move_simple() {
88 let b = body("MOVE 5 TO WS-X.");
89 assert_eq!(
90 b,
91 vec![Stmt::Move {
92 from: Operand::Num("5".into()),
93 to: vec!["WS-X".into()],
94 }]
95 );
96 }
97
98 #[test]
99 fn move_to_several_targets() {
100 let b = body("MOVE WS-A TO WS-B WS-C.");
101 assert_eq!(
102 b,
103 vec![Stmt::Move {
104 from: Operand::Data("WS-A".into()),
105 to: vec!["WS-B".into(), "WS-C".into()],
106 }]
107 );
108 }
109
110 #[test]
111 fn display_items_and_figurative() {
112 let b = body("DISPLAY 'TOTAL: ' WS-TOTAL SPACES.");
113 assert_eq!(
114 b,
115 vec![Stmt::Display {
116 items: vec![
117 Operand::Str("TOTAL: ".into()),
118 Operand::Data("WS-TOTAL".into()),
119 Operand::Figurative(Figurative::Space),
120 ],
121 }]
122 );
123 }
124
125 #[test]
126 fn compute_respects_precedence() {
127 let b = body("COMPUTE WS-T = WS-A + WS-B * 2.");
128 let expr = match &b[0] {
129 Stmt::Compute { targets, expr, .. } => {
130 assert_eq!(targets, &vec!["WS-T".to_string()]);
131 expr.clone()
132 }
133 other => panic!("se esperaba COMPUTE, vino {other:?}"),
134 };
135 // WS-A + (WS-B * 2)
136 assert_eq!(
137 expr,
138 Expr::Binary {
139 op: BinOp::Add,
140 lhs: Box::new(Expr::Operand(Operand::Data("WS-A".into()))),
141 rhs: Box::new(Expr::Binary {
142 op: BinOp::Mul,
143 lhs: Box::new(Expr::Operand(Operand::Data("WS-B".into()))),
144 rhs: Box::new(Expr::Operand(Operand::Num("2".into()))),
145 }),
146 }
147 );
148 }
149
150 #[test]
151 fn compute_rounded_flag() {
152 let b = body("COMPUTE WS-T ROUNDED = WS-A / 3.");
153 assert!(matches!(&b[0], Stmt::Compute { rounded: true, .. }));
154 }
155
156 #[test]
157 fn add_in_place_and_giving() {
158 assert_eq!(
159 body("ADD 1 TO WS-CT."),
160 vec![Stmt::Add {
161 addends: vec![Operand::Num("1".into())],
162 to: vec!["WS-CT".into()],
163 giving: vec![],
164 rounded: false,
165 }]
166 );
167 assert_eq!(
168 body("ADD WS-A WS-B GIVING WS-C."),
169 vec![Stmt::Add {
170 addends: vec![Operand::Data("WS-A".into()), Operand::Data("WS-B".into()),],
171 to: vec![],
172 giving: vec!["WS-C".into()],
173 rounded: false,
174 }]
175 );
176 }
177
178 #[test]
179 fn subtract_from_giving() {
180 assert_eq!(
181 body("SUBTRACT WS-TAX FROM WS-GROSS GIVING WS-NET."),
182 vec![Stmt::Subtract {
183 amounts: vec![Operand::Data("WS-TAX".into())],
184 from: vec!["WS-GROSS".into()],
185 giving: vec!["WS-NET".into()],
186 rounded: false,
187 }]
188 );
189 }
190
191 #[test]
192 fn divide_by_and_into() {
193 assert!(matches!(
194 &body("DIVIDE WS-A BY WS-B GIVING WS-C.")[0],
195 Stmt::Divide { by_form: true, .. }
196 ));
197 assert!(matches!(
198 &body("DIVIDE WS-A INTO WS-B.")[0],
199 Stmt::Divide { by_form: false, .. }
200 ));
201 }
202
203 #[test]
204 fn if_else_end_if() {
205 let b = body("IF WS-X > 0 DISPLAY 'POS' ELSE DISPLAY 'NEG' END-IF.");
206 match &b[0] {
207 Stmt::If {
208 cond,
209 then_branch,
210 else_branch,
211 } => {
212 assert_eq!(
213 cond,
214 &Cond::Compare {
215 lhs: Operand::Data("WS-X".into()),
216 op: CmpOp::Gt,
217 rhs: Operand::Num("0".into()),
218 }
219 );
220 assert_eq!(then_branch.len(), 1);
221 assert_eq!(else_branch.len(), 1);
222 }
223 other => panic!("se esperaba IF, vino {other:?}"),
224 }
225 }
226
227 #[test]
228 fn if_condition_with_and() {
229 let b = body("IF A = 1 AND B = 2 CONTINUE END-IF.");
230 match &b[0] {
231 Stmt::If { cond, .. } => {
232 assert!(matches!(cond, Cond::And(_, _)));
233 }
234 other => panic!("se esperaba IF, vino {other:?}"),
235 }
236 }
237
238 #[test]
239 fn if_named_condition() {
240 // Un dato suelto en la condición es un nombre de condición (88).
241 let b = body("IF FLAG-IS-OK MOVE 1 TO X END-IF.");
242 match &b[0] {
243 Stmt::If { cond, .. } => {
244 assert_eq!(cond, &Cond::Named("FLAG-IS-OK".into()));
245 }
246 other => panic!("se esperaba IF, vino {other:?}"),
247 }
248 }
249
250 #[test]
251 fn perform_paragraph_and_times() {
252 assert_eq!(
253 body("PERFORM SUB-PARA."),
254 vec![Stmt::Perform(Perform {
255 target: PerformTarget::Paragraph {
256 name: "SUB-PARA".into(),
257 thru: None,
258 },
259 control: PerformControl::Once,
260 })]
261 );
262 assert_eq!(
263 body("PERFORM SUB-PARA 3 TIMES."),
264 vec![Stmt::Perform(Perform {
265 target: PerformTarget::Paragraph {
266 name: "SUB-PARA".into(),
267 thru: None,
268 },
269 control: PerformControl::Times(Operand::Num("3".into())),
270 })]
271 );
272 }
273
274 #[test]
275 fn perform_inline_until() {
276 let b = body("PERFORM UNTIL WS-DONE = 1 ADD 1 TO WS-CT END-PERFORM.");
277 match &b[0] {
278 Stmt::Perform(p) => {
279 assert!(matches!(p.control, PerformControl::Until(_)));
280 match &p.target {
281 PerformTarget::Inline(body) => assert_eq!(body.len(), 1),
282 other => panic!("se esperaba cuerpo en línea, vino {other:?}"),
283 }
284 }
285 other => panic!("se esperaba PERFORM, vino {other:?}"),
286 }
287 }
288
289 #[test]
290 fn several_statements_in_one_sentence() {
291 let b = body("MOVE 1 TO X DISPLAY X STOP RUN.");
292 assert_eq!(b.len(), 3);
293 assert!(matches!(b[0], Stmt::Move { .. }));
294 assert!(matches!(b[1], Stmt::Display { .. }));
295 assert_eq!(b[2], Stmt::StopRun);
296 }
297
298 #[test]
299 fn unrecognized_verb_becomes_unknown() {
300 let b = body("INSPECT WS-X TALLYING WS-N FOR ALL ' '.");
301 match &b[0] {
302 Stmt::Unknown { verb, tokens } => {
303 assert_eq!(verb, "INSPECT");
304 assert!(!tokens.is_empty());
305 }
306 other => panic!("se esperaba Unknown, vino {other:?}"),
307 }
308 }
309
310 #[test]
311 fn full_program_lowers() {
312 let program = ir("IDENTIFICATION DIVISION.\n\
313 PROGRAM-ID. ADDER.\n\
314 DATA DIVISION.\n\
315 WORKING-STORAGE SECTION.\n\
316 01 WS-A PIC 9(3) VALUE 10.\n\
317 01 WS-T PIC 9(4).\n\
318 PROCEDURE DIVISION.\n\
319 MAIN-PARA.\n\
320 COMPUTE WS-T = WS-A + 5.\n\
321 DISPLAY WS-T.\n\
322 STOP RUN.\n");
323 assert_eq!(program.program_id, "ADDER");
324 assert_eq!(program.data.len(), 2);
325 assert_eq!(program.procedures.len(), 1);
326 assert_eq!(program.procedures[0].name, "MAIN-PARA");
327 assert_eq!(program.procedures[0].body.len(), 3);
328 }
329}