tawasuyu

tawasuyu/brahman

Rust 212TOML 64JSON 13HTML 12666 Sutichaykuna

⎇ f250fd0765d4b7b91b1bf9f58efa17c429924abf crates/modules/charka/charka-shadow/src/lib.rs Rust · 185 siq'ikuna · 6.4 KB Ñawpa willay Pi imata llamkarqan ch'uya

feat(charka): PERFORM ... THRU como rango real de párrafos f250fd076 · sergio ·

1//! `charka-shadow` — el validador en sombra del transpilador.
2//!
3//! Certifica que el pipeline de charka (lexer → parser → IR → codegen)
4//! preserva la semántica del programa COBOL original. Lo hace con una
5//! **ejecución sombra**: un intérprete que corre el [`Ir`] directamente
6//! sobre los tipos de `charka-runtime`, sin compilar nada.
7//!
8//! El intérprete es una segunda ruta de ejecución, independiente del
9//! código que emite `charka-codegen`. Si la sombra y el transpilado
10//! produjeran salidas distintas, eso delataría un bug del codegen.
11//!
12//! - [`interpret`] — ejecuta un `Ir` y devuelve su salida.
13//! - [`run_source`] — el pipeline completo, de fuente COBOL a salida.
14//!
15//! La referencia contra la que se comparan los resultados es, en la
16//! v1, un conjunto de salidas esperadas verificadas a mano (el corpus
17//! en `crates/modules/charka/corpus/`). Cuando haya un GnuCOBOL
18//! disponible, un modo futuro podrá diferenciar contra el compilador
19//! de COBOL real — la validación «original vs transpilado» plena.
20//!
21//! El intérprete tiene un tope de pasos: un bucle que no termina se
22//! corta con [`Halt::StepLimit`] en vez de colgarse.
23
24#![forbid(unsafe_code)]
25
26mod field;
27mod interp;
28
29use charka_ir::Ir;
30use interp::Machine;
31
32/// Cómo terminó una ejecución sombra.
33#[derive(Debug, Clone, Copy, PartialEq, Eq)]
34pub enum Halt {
35    /// Cayó por el final del PROCEDURE division.
36    Normal,
37    /// Un `STOP RUN` o `GOBACK`.
38    StopRun,
39    /// Se agotó el tope de pasos (un bucle que no termina).
40    StepLimit,
41}
42
43/// El resultado de una ejecución sombra.
44#[derive(Debug, Clone, PartialEq, Eq)]
45pub struct Outcome {
46    /// Las líneas que el programa emitió por `DISPLAY`.
47    pub lines: Vec<String>,
48    /// Cómo terminó.
49    pub halt: Halt,
50}
51
52/// Falla del pipeline previo al intérprete.
53#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
54pub enum ShadowError {
55    #[error("error de léxico: {0}")]
56    Lex(#[from] charka_lexer::LexError),
57    #[error("error de parseo: {0}")]
58    Parse(#[from] charka_parser::ParseError),
59}
60
61/// Ejecuta un [`Ir`] en sombra y captura su salida.
62pub fn interpret(ir: &Ir) -> Outcome {
63    let mut machine = Machine::new(ir);
64    machine.run();
65    let halt = if machine.step_limit_hit {
66        Halt::StepLimit
67    } else if machine.stopped {
68        Halt::StopRun
69    } else {
70        Halt::Normal
71    };
72    Outcome {
73        lines: machine.output,
74        halt,
75    }
76}
77
78/// Corre el pipeline completo: fuente COBOL (formato libre) → salida.
79pub fn run_source(cobol: &str) -> Result<Outcome, ShadowError> {
80    let tokens = charka_lexer::lex(cobol, charka_lexer::SourceFormat::Free)?;
81    let program = charka_parser::parse(&tokens)?;
82    let ir = charka_ir::lower(&program);
83    Ok(interpret(&ir))
84}
85
86#[cfg(test)]
87mod tests {
88    use super::*;
89
90    /// Verifica un programa del corpus contra su salida esperada. La
91    /// comparación ignora los espacios finales de cada línea.
92    fn check(cobol: &str, expected: &str) {
93        let outcome = run_source(cobol).expect("el pipeline no debe fallar");
94        let got: Vec<&str> = outcome.lines.iter().map(|l| l.trim_end()).collect();
95        let want: Vec<&str> = expected.lines().map(|l| l.trim_end()).collect();
96        assert_eq!(got, want, "salida sombra distinta de la esperada");
97    }
98
99    /// Declara un test que corre un programa del corpus.
100    macro_rules! corpus_test {
101        ($name:ident, $file:literal) => {
102            #[test]
103            fn $name() {
104                check(
105                    include_str!(concat!("../../corpus/", $file, ".cob")),
106                    include_str!(concat!("../../corpus/", $file, ".expected")),
107                );
108            }
109        };
110    }
111
112    corpus_test!(corpus_01_hola, "01-hola");
113    corpus_test!(corpus_02_aritmetica, "02-aritmetica");
114    corpus_test!(corpus_03_condicional, "03-condicional");
115    corpus_test!(corpus_04_bucle, "04-bucle");
116    corpus_test!(corpus_05_factorial, "05-factorial");
117    corpus_test!(corpus_06_nomina, "06-nomina");
118    corpus_test!(corpus_07_clasificar, "07-clasificar");
119    corpus_test!(corpus_08_varying, "08-varying");
120    corpus_test!(corpus_09_evaluar, "09-evaluar");
121    corpus_test!(corpus_10_condicion, "10-condicion");
122    corpus_test!(corpus_11_tabla, "11-tabla");
123    corpus_test!(corpus_12_cadenas, "12-cadenas");
124    corpus_test!(corpus_13_inspeccion, "13-inspeccion");
125    corpus_test!(corpus_14_clasifica, "14-clasifica");
126    corpus_test!(corpus_15_resetear, "15-resetear");
127    corpus_test!(corpus_16_bandera, "16-bandera");
128    corpus_test!(corpus_17_rangopar, "17-rangopar");
129
130    #[test]
131    fn empty_source_runs_clean() {
132        let outcome = run_source("").expect("pipeline OK");
133        assert!(outcome.lines.is_empty());
134        assert_eq!(outcome.halt, Halt::Normal);
135    }
136
137    #[test]
138    fn stop_run_is_reported() {
139        let outcome = run_source("PROCEDURE DIVISION.\nMAIN.\n DISPLAY 'X'.\n STOP RUN.\n")
140            .expect("pipeline OK");
141        assert_eq!(outcome.lines, vec!["X".to_string()]);
142        assert_eq!(outcome.halt, Halt::StopRun);
143    }
144
145    #[test]
146    fn endless_loop_is_cut_by_the_step_limit() {
147        // `PERFORM UNTIL 1 = 0` nunca se cumple — el tope lo corta.
148        let outcome = run_source(
149            "PROCEDURE DIVISION.\n\
150             MAIN.\n\
151                 PERFORM UNTIL 1 = 0\n\
152                     CONTINUE\n\
153                 END-PERFORM.\n",
154        )
155        .expect("pipeline OK");
156        assert_eq!(outcome.halt, Halt::StepLimit);
157    }
158
159    #[test]
160    fn perform_varying_out_of_line() {
161        // `PERFORM CONTAR VARYING ...` — el párrafo es el cuerpo del bucle.
162        // WS-I = 1, 3, 5, 7, 9 (FROM 1 BY 2 UNTIL > 9) → 5 iteraciones.
163        let outcome = run_source(
164            "DATA DIVISION.\n\
165             WORKING-STORAGE SECTION.\n\
166             01 WS-I PIC 9(2) VALUE 0.\n\
167             01 WS-N PIC 9(3) VALUE 0.\n\
168             PROCEDURE DIVISION.\n\
169             MAIN.\n\
170                 PERFORM CONTAR VARYING WS-I FROM 1 BY 2 UNTIL WS-I > 9.\n\
171                 DISPLAY WS-N.\n\
172                 STOP RUN.\n\
173             CONTAR.\n\
174                 ADD 1 TO WS-N.\n",
175        )
176        .expect("pipeline OK");
177        assert_eq!(outcome.lines, vec!["005".to_string()]);
178    }
179
180    #[test]
181    fn lex_error_surfaces() {
182        let err = run_source("PROCEDURE DIVISION.\nMAIN.\n DISPLAY 'sin cerrar.\n").unwrap_err();
183        assert!(matches!(err, ShadowError::Lex(_)));
184    }
185}