gioser

tawasuyu/brahman

Rust 328TOML 124JSON 17Markdown 144636 Definitions

⎇ main crates/modules/charka/charka-ir/src/lib.rs Rust · 549 lines · 18.1 KB History Who touched what raw

feat(charka): E/S de ficheros — SELECT/FD/OPEN/READ/WRITE/CLOSE b3278bdb0 · sergio ·

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}