tawasuyu

tawasuyu/brahman

Rust 582TOML 222Markdown 73JSON 198629 Definiciones

⎇ 82ba0b7a1a04b7ff2d7e2175ac74766da0d16066 crates/modules/charka/charka-ir/src/lib.rs Rust · 514 líneas · 16.8 KB Historia Quién tocó qué crudo

feat(charka): SET ... TO TRUE — escribir nombres de condición (88) 82ba0b7a1 · 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`. 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, GroupInfo};
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 initialize_parses_and_groups_are_modeled() {
445        let program = ir("DATA DIVISION.\n\
446             WORKING-STORAGE SECTION.\n\
447             01 WS-REC.\n\
448                05 WS-A PIC 9(3).\n\
449                05 WS-B PIC X(4).\n\
450             PROCEDURE DIVISION.\n\
451             MAIN.\n\
452                 INITIALIZE WS-REC.\n");
453        let g = program.model.group("WS-REC").expect("grupo WS-REC");
454        assert_eq!(g.members, vec!["WS-A".to_string(), "WS-B".to_string()]);
455        match &program.procedures[0].body[0] {
456            Stmt::Initialize { targets } => {
457                assert_eq!(targets, &vec![Operand::Data("WS-REC".into())]);
458            }
459            other => panic!("se esperaba INITIALIZE, vino {other:?}"),
460        }
461    }
462
463    #[test]
464    fn set_to_true_parses() {
465        let b = body("SET ACTIVO LISTO TO TRUE.");
466        match &b[0] {
467            Stmt::SetTrue { conditions } => {
468                assert_eq!(conditions, &vec!["ACTIVO".to_string(), "LISTO".to_string()]);
469            }
470            other => panic!("se esperaba SET, vino {other:?}"),
471        }
472    }
473
474    #[test]
475    fn several_statements_in_one_sentence() {
476        let b = body("MOVE 1 TO X DISPLAY X STOP RUN.");
477        assert_eq!(b.len(), 3);
478        assert!(matches!(b[0], Stmt::Move { .. }));
479        assert!(matches!(b[1], Stmt::Display { .. }));
480        assert_eq!(b[2], Stmt::StopRun);
481    }
482
483    #[test]
484    fn unrecognized_verb_becomes_unknown() {
485        let b = body("CALL 'SUBPROG' USING WS-X.");
486        match &b[0] {
487            Stmt::Unknown { verb, tokens } => {
488                assert_eq!(verb, "CALL");
489                assert!(!tokens.is_empty());
490            }
491            other => panic!("se esperaba Unknown, vino {other:?}"),
492        }
493    }
494
495    #[test]
496    fn full_program_lowers() {
497        let program = ir("IDENTIFICATION DIVISION.\n\
498             PROGRAM-ID. ADDER.\n\
499             DATA DIVISION.\n\
500             WORKING-STORAGE SECTION.\n\
501             01 WS-A PIC 9(3) VALUE 10.\n\
502             01 WS-T PIC 9(4).\n\
503             PROCEDURE DIVISION.\n\
504             MAIN-PARA.\n\
505                 COMPUTE WS-T = WS-A + 5.\n\
506                 DISPLAY WS-T.\n\
507                 STOP RUN.\n");
508        assert_eq!(program.program_id, "ADDER");
509        assert_eq!(program.data.len(), 2);
510        assert_eq!(program.procedures.len(), 1);
511        assert_eq!(program.procedures[0].name, "MAIN-PARA");
512        assert_eq!(program.procedures[0].body.len(), 3);
513    }
514}