tawasuyu

tawasuyu/brahman

Rust 582TOML 222Markdown 73JSON 198635 Definitions

⎇ 3902763daa94397756ff2394116daef97814090a crates/modules/charka/charka-ir/src/lib.rs Rust · 401 lines · 12.7 KB History Who touched what raw

feat(charka): OCCURS — tablas y referencias con subíndice 3902763da · 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`, `PERFORM` (fuera
20//! de línea, en línea, `TIMES`, `UNTIL`, `VARYING`), `GO TO`,
21//! `STOP RUN`, `GOBACK`, `EXIT`, `CONTINUE`. Fuera de alcance:
22//! `STRING`/`UNSTRING`, E/S de ficheros, CICS y SQL embebido.
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!(whens[0].values, vec![Operand::Num("1".into())]);
351                assert_eq!(
352                    whens[1].values,
353                    vec![Operand::Num("2".into()), Operand::Num("3".into())]
354                );
355                assert_eq!(other.len(), 1);
356            }
357            other => panic!("se esperaba EVALUATE, vino {other:?}"),
358        }
359    }
360
361    #[test]
362    fn several_statements_in_one_sentence() {
363        let b = body("MOVE 1 TO X DISPLAY X STOP RUN.");
364        assert_eq!(b.len(), 3);
365        assert!(matches!(b[0], Stmt::Move { .. }));
366        assert!(matches!(b[1], Stmt::Display { .. }));
367        assert_eq!(b[2], Stmt::StopRun);
368    }
369
370    #[test]
371    fn unrecognized_verb_becomes_unknown() {
372        let b = body("INSPECT WS-X TALLYING WS-N FOR ALL ' '.");
373        match &b[0] {
374            Stmt::Unknown { verb, tokens } => {
375                assert_eq!(verb, "INSPECT");
376                assert!(!tokens.is_empty());
377            }
378            other => panic!("se esperaba Unknown, vino {other:?}"),
379        }
380    }
381
382    #[test]
383    fn full_program_lowers() {
384        let program = ir("IDENTIFICATION DIVISION.\n\
385             PROGRAM-ID. ADDER.\n\
386             DATA DIVISION.\n\
387             WORKING-STORAGE SECTION.\n\
388             01 WS-A PIC 9(3) VALUE 10.\n\
389             01 WS-T PIC 9(4).\n\
390             PROCEDURE DIVISION.\n\
391             MAIN-PARA.\n\
392                 COMPUTE WS-T = WS-A + 5.\n\
393                 DISPLAY WS-T.\n\
394                 STOP RUN.\n");
395        assert_eq!(program.program_id, "ADDER");
396        assert_eq!(program.data.len(), 2);
397        assert_eq!(program.procedures.len(), 1);
398        assert_eq!(program.procedures[0].name, "MAIN-PARA");
399        assert_eq!(program.procedures[0].body.len(), 3);
400    }
401}