⎇ f250fd0765d4b7b91b1bf9f58efa17c429924abf
crates/modules/charka/charka-shadow/src/lib.rs
Ñawpa willay
Pi imata llamkarqan
ch'uya
feat(charka): PERFORM ... THRU como rango real de párrafos
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}