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