⎇ 3902763daa94397756ff2394116daef97814090a
crates/modules/charka/charka-shadow/src/lib.rs
History
Who touched what
raw
feat(charka): OCCURS — tablas y referencias con subíndice
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
124 #[test]
125 fn empty_source_runs_clean() {
126 let outcome = run_source("").expect("pipeline OK");
127 assert!(outcome.lines.is_empty());
128 assert_eq!(outcome.halt, Halt::Normal);
129 }
130
131 #[test]
132 fn stop_run_is_reported() {
133 let outcome = run_source("PROCEDURE DIVISION.\nMAIN.\n DISPLAY 'X'.\n STOP RUN.\n")
134 .expect("pipeline OK");
135 assert_eq!(outcome.lines, vec!["X".to_string()]);
136 assert_eq!(outcome.halt, Halt::StopRun);
137 }
138
139 #[test]
140 fn endless_loop_is_cut_by_the_step_limit() {
141 // `PERFORM UNTIL 1 = 0` nunca se cumple — el tope lo corta.
142 let outcome = run_source(
143 "PROCEDURE DIVISION.\n\
144 MAIN.\n\
145 PERFORM UNTIL 1 = 0\n\
146 CONTINUE\n\
147 END-PERFORM.\n",
148 )
149 .expect("pipeline OK");
150 assert_eq!(outcome.halt, Halt::StepLimit);
151 }
152
153 #[test]
154 fn perform_varying_out_of_line() {
155 // `PERFORM CONTAR VARYING ...` — el párrafo es el cuerpo del bucle.
156 // WS-I = 1, 3, 5, 7, 9 (FROM 1 BY 2 UNTIL > 9) → 5 iteraciones.
157 let outcome = run_source(
158 "DATA DIVISION.\n\
159 WORKING-STORAGE SECTION.\n\
160 01 WS-I PIC 9(2) VALUE 0.\n\
161 01 WS-N PIC 9(3) VALUE 0.\n\
162 PROCEDURE DIVISION.\n\
163 MAIN.\n\
164 PERFORM CONTAR VARYING WS-I FROM 1 BY 2 UNTIL WS-I > 9.\n\
165 DISPLAY WS-N.\n\
166 STOP RUN.\n\
167 CONTAR.\n\
168 ADD 1 TO WS-N.\n",
169 )
170 .expect("pipeline OK");
171 assert_eq!(outcome.lines, vec!["005".to_string()]);
172 }
173
174 #[test]
175 fn lex_error_surfaces() {
176 let err = run_source("PROCEDURE DIVISION.\nMAIN.\n DISPLAY 'sin cerrar.\n").unwrap_err();
177 assert!(matches!(err, ShadowError::Lex(_)));
178 }
179}