gioser

tawasuyu/brahman

Rust 476TOML 190Markdown 50JSON 176602 Definitions

⎇ 71a4068d12e81d0be7b08d0717114dd5b10868fe crates/modules/charka/charka-ir/src/lib.rs Rust · 329 lines · 10.1 KB History Who touched what raw

feat(charka): charka-ir — representación intermedia con statements tipados 71a4068d1 · 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`), `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}