⎇ 7867d6830e128b8a2afe95cb752093e12a184988
crates/modules/charka/charka-ir/src/lib.rs
Ñawpa willay
Pi imata llamkarqan
ch'uya
feat(charka): EVALUATE TRUE y rangos WHEN ... THRU
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`), `EVALUATE`/`WHEN`, `STRING`,
20//! `UNSTRING`, `INSPECT`, `PERFORM` (fuera de línea, en línea,
21//! `TIMES`, `UNTIL`, `VARYING`), `GO TO`, `STOP RUN`, `GOBACK`,
22//! `EXIT`, `CONTINUE`. Fuera de alcance: E/S de ficheros, CICS y SQL.
23
24#![forbid(unsafe_code)]
25
26mod ast;
27mod cursor;
28mod expr;
29mod kw;
30mod model;
31mod stmt;
32
33pub use ast::*;
34pub use charka_parser::Program;
35pub use model::{resolve_data, ConditionName, DataModel, Field, FieldKind};
36
37use cursor::Cursor;
38
39/// Baja un [`Program`] parseado a la representación intermedia.
40pub fn lower(program: &Program) -> Ir {
41 let procedures = program
42 .paragraphs
43 .iter()
44 .map(|p| {
45 let mut body = Vec::new();
46 for sentence in &p.sentences {
47 let mut cur = Cursor::new(&sentence.tokens);
48 body.extend(stmt::parse_statements(&mut cur, &[]));
49 }
50 Procedure {
51 name: p.name.clone(),
52 body,
53 }
54 })
55 .collect();
56
57 Ir {
58 program_id: program.program_id.clone().unwrap_or_default(),
59 data: program.data.clone(),
60 model: model::resolve_data(&program.data),
61 procedures,
62 }
63}
64
65#[cfg(test)]
66mod tests {
67 use super::*;
68 use charka_lexer::{lex, SourceFormat};
69
70 /// Helper: lexa, parsea y baja a IR un fuente en formato libre.
71 fn ir(src: &str) -> Ir {
72 let toks = lex(src, SourceFormat::Free).expect("lex OK");
73 let program = charka_parser::parse(&toks).expect("parse OK");
74 lower(&program)
75 }
76
77 /// Helper: el cuerpo del primer (y normalmente único) párrafo.
78 fn body(src: &str) -> Vec<Stmt> {
79 let prog = format!("PROCEDURE DIVISION.\nMAIN.\n{src}\n");
80 ir(&prog).procedures.into_iter().next().unwrap().body
81 }
82
83 #[test]
84 fn empty_program_lowers_to_default() {
85 let got = lower(&charka_parser::parse(&[]).unwrap());
86 assert_eq!(got, Ir::default());
87 }
88
89 #[test]
90 fn move_simple() {
91 let b = body("MOVE 5 TO WS-X.");
92 assert_eq!(
93 b,
94 vec![Stmt::Move {
95 from: Operand::Num("5".into()),
96 to: vec![Operand::Data("WS-X".into())],
97 }]
98 );
99 }
100
101 #[test]
102 fn move_to_several_targets() {
103 let b = body("MOVE WS-A TO WS-B WS-C.");
104 assert_eq!(
105 b,
106 vec![Stmt::Move {
107 from: Operand::Data("WS-A".into()),
108 to: vec![Operand::Data("WS-B".into()), Operand::Data("WS-C".into()),],
109 }]
110 );
111 }
112
113 #[test]
114 fn indexed_operand_parses_subscript() {
115 // `WS-ELEM(WS-I)` — un destino con subíndice de tabla.
116 let b = body("MOVE 7 TO WS-ELEM(WS-I).");
117 match &b[0] {
118 Stmt::Move { to, .. } => match &to[0] {
119 Operand::Indexed { name, index } => {
120 assert_eq!(name, "WS-ELEM");
121 assert_eq!(**index, Operand::Data("WS-I".into()));
122 }
123 other => panic!("se esperaba Indexed, vino {other:?}"),
124 },
125 other => panic!("se esperaba MOVE, vino {other:?}"),
126 }
127 }
128
129 #[test]
130 fn display_items_and_figurative() {
131 let b = body("DISPLAY 'TOTAL: ' WS-TOTAL SPACES.");
132 assert_eq!(
133 b,
134 vec![Stmt::Display {
135 items: vec![
136 Operand::Str("TOTAL: ".into()),
137 Operand::Data("WS-TOTAL".into()),
138 Operand::Figurative(Figurative::Space),
139 ],
140 }]
141 );
142 }
143
144 #[test]
145 fn compute_respects_precedence() {
146 let b = body("COMPUTE WS-T = WS-A + WS-B * 2.");
147 let expr = match &b[0] {
148 Stmt::Compute { targets, expr, .. } => {
149 assert_eq!(targets, &vec![Operand::Data("WS-T".into())]);
150 expr.clone()
151 }
152 other => panic!("se esperaba COMPUTE, vino {other:?}"),
153 };
154 // WS-A + (WS-B * 2)
155 assert_eq!(
156 expr,
157 Expr::Binary {
158 op: BinOp::Add,
159 lhs: Box::new(Expr::Operand(Operand::Data("WS-A".into()))),
160 rhs: Box::new(Expr::Binary {
161 op: BinOp::Mul,
162 lhs: Box::new(Expr::Operand(Operand::Data("WS-B".into()))),
163 rhs: Box::new(Expr::Operand(Operand::Num("2".into()))),
164 }),
165 }
166 );
167 }
168
169 #[test]
170 fn compute_rounded_flag() {
171 let b = body("COMPUTE WS-T ROUNDED = WS-A / 3.");
172 assert!(matches!(&b[0], Stmt::Compute { rounded: true, .. }));
173 }
174
175 #[test]
176 fn add_in_place_and_giving() {
177 assert_eq!(
178 body("ADD 1 TO WS-CT."),
179 vec![Stmt::Add {
180 addends: vec![Operand::Num("1".into())],
181 to: vec![Operand::Data("WS-CT".into())],
182 giving: vec![],
183 rounded: false,
184 }]
185 );
186 assert_eq!(
187 body("ADD WS-A WS-B GIVING WS-C."),
188 vec![Stmt::Add {
189 addends: vec![Operand::Data("WS-A".into()), Operand::Data("WS-B".into()),],
190 to: vec![],
191 giving: vec![Operand::Data("WS-C".into())],
192 rounded: false,
193 }]
194 );
195 }
196
197 #[test]
198 fn subtract_from_giving() {
199 assert_eq!(
200 body("SUBTRACT WS-TAX FROM WS-GROSS GIVING WS-NET."),
201 vec![Stmt::Subtract {
202 amounts: vec![Operand::Data("WS-TAX".into())],
203 from: vec![Operand::Data("WS-GROSS".into())],
204 giving: vec![Operand::Data("WS-NET".into())],
205 rounded: false,
206 }]
207 );
208 }
209
210 #[test]
211 fn divide_by_and_into() {
212 assert!(matches!(
213 &body("DIVIDE WS-A BY WS-B GIVING WS-C.")[0],
214 Stmt::Divide { by_form: true, .. }
215 ));
216 assert!(matches!(
217 &body("DIVIDE WS-A INTO WS-B.")[0],
218 Stmt::Divide { by_form: false, .. }
219 ));
220 }
221
222 #[test]
223 fn if_else_end_if() {
224 let b = body("IF WS-X > 0 DISPLAY 'POS' ELSE DISPLAY 'NEG' END-IF.");
225 match &b[0] {
226 Stmt::If {
227 cond,
228 then_branch,
229 else_branch,
230 } => {
231 assert_eq!(
232 cond,
233 &Cond::Compare {
234 lhs: Operand::Data("WS-X".into()),
235 op: CmpOp::Gt,
236 rhs: Operand::Num("0".into()),
237 }
238 );
239 assert_eq!(then_branch.len(), 1);
240 assert_eq!(else_branch.len(), 1);
241 }
242 other => panic!("se esperaba IF, vino {other:?}"),
243 }
244 }
245
246 #[test]
247 fn if_condition_with_and() {
248 let b = body("IF A = 1 AND B = 2 CONTINUE END-IF.");
249 match &b[0] {
250 Stmt::If { cond, .. } => {
251 assert!(matches!(cond, Cond::And(_, _)));
252 }
253 other => panic!("se esperaba IF, vino {other:?}"),
254 }
255 }
256
257 #[test]
258 fn if_named_condition() {
259 // Un dato suelto en la condición es un nombre de condición (88).
260 let b = body("IF FLAG-IS-OK MOVE 1 TO X END-IF.");
261 match &b[0] {
262 Stmt::If { cond, .. } => {
263 assert_eq!(cond, &Cond::Named("FLAG-IS-OK".into()));
264 }
265 other => panic!("se esperaba IF, vino {other:?}"),
266 }
267 }
268
269 #[test]
270 fn perform_paragraph_and_times() {
271 assert_eq!(
272 body("PERFORM SUB-PARA."),
273 vec![Stmt::Perform(Perform {
274 target: PerformTarget::Paragraph {
275 name: "SUB-PARA".into(),
276 thru: None,
277 },
278 control: PerformControl::Once,
279 })]
280 );
281 assert_eq!(
282 body("PERFORM SUB-PARA 3 TIMES."),
283 vec![Stmt::Perform(Perform {
284 target: PerformTarget::Paragraph {
285 name: "SUB-PARA".into(),
286 thru: None,
287 },
288 control: PerformControl::Times(Operand::Num("3".into())),
289 })]
290 );
291 }
292
293 #[test]
294 fn perform_inline_until() {
295 let b = body("PERFORM UNTIL WS-DONE = 1 ADD 1 TO WS-CT END-PERFORM.");
296 match &b[0] {
297 Stmt::Perform(p) => {
298 assert!(matches!(p.control, PerformControl::Until(_)));
299 match &p.target {
300 PerformTarget::Inline(body) => assert_eq!(body.len(), 1),
301 other => panic!("se esperaba cuerpo en línea, vino {other:?}"),
302 }
303 }
304 other => panic!("se esperaba PERFORM, vino {other:?}"),
305 }
306 }
307
308 #[test]
309 fn perform_varying_inline() {
310 let b = body(
311 "PERFORM VARYING WS-I FROM 1 BY 2 UNTIL WS-I > 9 \
312 CONTINUE END-PERFORM.",
313 );
314 match &b[0] {
315 Stmt::Perform(p) => match &p.control {
316 PerformControl::Varying {
317 var,
318 from,
319 by,
320 until,
321 } => {
322 assert_eq!(var, "WS-I");
323 assert_eq!(from, &Operand::Num("1".into()));
324 assert_eq!(by, &Operand::Num("2".into()));
325 assert!(matches!(until, Cond::Compare { .. }));
326 }
327 other => panic!("se esperaba Varying, vino {other:?}"),
328 },
329 other => panic!("se esperaba PERFORM, vino {other:?}"),
330 }
331 }
332
333 #[test]
334 fn evaluate_parses_whens_and_other() {
335 let b = body(
336 "EVALUATE WS-X \
337 WHEN 1 DISPLAY 'A' \
338 WHEN 2 WHEN 3 DISPLAY 'B' \
339 WHEN OTHER DISPLAY 'C' \
340 END-EVALUATE.",
341 );
342 match &b[0] {
343 Stmt::Evaluate {
344 subject,
345 whens,
346 other,
347 } => {
348 assert_eq!(subject, &Operand::Data("WS-X".into()));
349 assert_eq!(whens.len(), 2);
350 assert_eq!(
351 whens[0].tests,
352 vec![WhenTest::Value(Operand::Num("1".into()))]
353 );
354 assert_eq!(
355 whens[1].tests,
356 vec![
357 WhenTest::Value(Operand::Num("2".into())),
358 WhenTest::Value(Operand::Num("3".into())),
359 ]
360 );
361 assert_eq!(other.len(), 1);
362 }
363 other => panic!("se esperaba EVALUATE, vino {other:?}"),
364 }
365 }
366
367 #[test]
368 fn evaluate_true_and_range_when() {
369 // `EVALUATE TRUE` → los WHEN son condiciones.
370 let b = body("EVALUATE TRUE WHEN WS-X > 0 DISPLAY 'P' END-EVALUATE.");
371 match &b[0] {
372 Stmt::Evaluate { whens, .. } => {
373 assert!(matches!(whens[0].tests[0], WhenTest::Cond(_)));
374 }
375 other => panic!("se esperaba EVALUATE, vino {other:?}"),
376 }
377 // `WHEN lo THRU hi` → un rango.
378 let b = body("EVALUATE WS-X WHEN 1 THRU 9 DISPLAY 'D' END-EVALUATE.");
379 match &b[0] {
380 Stmt::Evaluate { whens, .. } => match &whens[0].tests[0] {
381 WhenTest::Range(lo, hi) => {
382 assert_eq!(lo, &Operand::Num("1".into()));
383 assert_eq!(hi, &Operand::Num("9".into()));
384 }
385 other => panic!("se esperaba Range, vino {other:?}"),
386 },
387 other => panic!("se esperaba EVALUATE, vino {other:?}"),
388 }
389 }
390
391 #[test]
392 fn string_and_unstring_parse() {
393 let b = body("STRING WS-A WS-B DELIMITED BY SIZE INTO WS-OUT END-STRING.");
394 match &b[0] {
395 Stmt::StringConcat { sources, into } => {
396 assert_eq!(sources.len(), 2);
397 assert_eq!(into, &Operand::Data("WS-OUT".into()));
398 }
399 other => panic!("se esperaba STRING, vino {other:?}"),
400 }
401 let b = body("UNSTRING WS-SRC DELIMITED BY ',' INTO WS-A WS-B END-UNSTRING.");
402 match &b[0] {
403 Stmt::Unstring {
404 source,
405 delimiter,
406 into,
407 } => {
408 assert_eq!(source, &Operand::Data("WS-SRC".into()));
409 assert_eq!(delimiter, &Operand::Str(",".into()));
410 assert_eq!(into.len(), 2);
411 }
412 other => panic!("se esperaba UNSTRING, vino {other:?}"),
413 }
414 }
415
416 #[test]
417 fn inspect_tallying_and_replacing_parse() {
418 let b = body("INSPECT WS-T TALLYING WS-N FOR ALL 'A'.");
419 match &b[0] {
420 Stmt::Inspect {
421 target,
422 op: InspectOp::TallyingForAll { counter, search },
423 } => {
424 assert_eq!(target, &Operand::Data("WS-T".into()));
425 assert_eq!(counter, &Operand::Data("WS-N".into()));
426 assert_eq!(search, &Operand::Str("A".into()));
427 }
428 other => panic!("se esperaba INSPECT TALLYING, vino {other:?}"),
429 }
430 let b = body("INSPECT WS-T REPLACING ALL 'A' BY 'O'.");
431 match &b[0] {
432 Stmt::Inspect {
433 op: InspectOp::ReplacingAll { from, to },
434 ..
435 } => {
436 assert_eq!(from, &Operand::Str("A".into()));
437 assert_eq!(to, &Operand::Str("O".into()));
438 }
439 other => panic!("se esperaba INSPECT REPLACING, vino {other:?}"),
440 }
441 }
442
443 #[test]
444 fn several_statements_in_one_sentence() {
445 let b = body("MOVE 1 TO X DISPLAY X STOP RUN.");
446 assert_eq!(b.len(), 3);
447 assert!(matches!(b[0], Stmt::Move { .. }));
448 assert!(matches!(b[1], Stmt::Display { .. }));
449 assert_eq!(b[2], Stmt::StopRun);
450 }
451
452 #[test]
453 fn unrecognized_verb_becomes_unknown() {
454 let b = body("INITIALIZE WS-X WS-Y.");
455 match &b[0] {
456 Stmt::Unknown { verb, tokens } => {
457 assert_eq!(verb, "INITIALIZE");
458 assert!(!tokens.is_empty());
459 }
460 other => panic!("se esperaba Unknown, vino {other:?}"),
461 }
462 }
463
464 #[test]
465 fn full_program_lowers() {
466 let program = ir("IDENTIFICATION DIVISION.\n\
467 PROGRAM-ID. ADDER.\n\
468 DATA DIVISION.\n\
469 WORKING-STORAGE SECTION.\n\
470 01 WS-A PIC 9(3) VALUE 10.\n\
471 01 WS-T PIC 9(4).\n\
472 PROCEDURE DIVISION.\n\
473 MAIN-PARA.\n\
474 COMPUTE WS-T = WS-A + 5.\n\
475 DISPLAY WS-T.\n\
476 STOP RUN.\n");
477 assert_eq!(program.program_id, "ADDER");
478 assert_eq!(program.data.len(), 2);
479 assert_eq!(program.procedures.len(), 1);
480 assert_eq!(program.procedures[0].name, "MAIN-PARA");
481 assert_eq!(program.procedures[0].body.len(), 3);
482 }
483}