⎇ 634a43006ac4257288af40320b5360e9f157ab09
crates/modules/charka/charka-shadow/src/lib.rs
Ñawpa willay
Pi imata llamkarqan
ch'uya
feat(charka): PICTURE de edición — Z, coma de millares y punto decimal
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 corpus_test!(corpus_18_fichero, "18-fichero");
130 corpus_test!(corpus_19_reporte, "19-reporte");
131
132 #[test]
133 fn empty_source_runs_clean() {
134 let outcome = run_source("").expect("pipeline OK");
135 assert!(outcome.lines.is_empty());
136 assert_eq!(outcome.halt, Halt::Normal);
137 }
138
139 #[test]
140 fn stop_run_is_reported() {
141 let outcome = run_source("PROCEDURE DIVISION.\nMAIN.\n DISPLAY 'X'.\n STOP RUN.\n")
142 .expect("pipeline OK");
143 assert_eq!(outcome.lines, vec!["X".to_string()]);
144 assert_eq!(outcome.halt, Halt::StopRun);
145 }
146
147 #[test]
148 fn endless_loop_is_cut_by_the_step_limit() {
149 // `PERFORM UNTIL 1 = 0` nunca se cumple — el tope lo corta.
150 let outcome = run_source(
151 "PROCEDURE DIVISION.\n\
152 MAIN.\n\
153 PERFORM UNTIL 1 = 0\n\
154 CONTINUE\n\
155 END-PERFORM.\n",
156 )
157 .expect("pipeline OK");
158 assert_eq!(outcome.halt, Halt::StepLimit);
159 }
160
161 #[test]
162 fn perform_varying_out_of_line() {
163 // `PERFORM CONTAR VARYING ...` — el párrafo es el cuerpo del bucle.
164 // WS-I = 1, 3, 5, 7, 9 (FROM 1 BY 2 UNTIL > 9) → 5 iteraciones.
165 let outcome = run_source(
166 "DATA DIVISION.\n\
167 WORKING-STORAGE SECTION.\n\
168 01 WS-I PIC 9(2) VALUE 0.\n\
169 01 WS-N PIC 9(3) VALUE 0.\n\
170 PROCEDURE DIVISION.\n\
171 MAIN.\n\
172 PERFORM CONTAR VARYING WS-I FROM 1 BY 2 UNTIL WS-I > 9.\n\
173 DISPLAY WS-N.\n\
174 STOP RUN.\n\
175 CONTAR.\n\
176 ADD 1 TO WS-N.\n",
177 )
178 .expect("pipeline OK");
179 assert_eq!(outcome.lines, vec!["005".to_string()]);
180 }
181
182 #[test]
183 fn lex_error_surfaces() {
184 let err = run_source("PROCEDURE DIVISION.\nMAIN.\n DISPLAY 'sin cerrar.\n").unwrap_err();
185 assert!(matches!(err, ShadowError::Lex(_)));
186 }
187}