⎇ 4d9ce11b1e444430e6e2bcc0a72baa1d40a4527c
crates/modules/charka/charka-shadow/src/lib.rs
Historia
Quién tocó qué
crudo
feat(charka): charka-shadow — validador en sombra + corpus COBOL
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
120 #[test]
121 fn empty_source_runs_clean() {
122 let outcome = run_source("").expect("pipeline OK");
123 assert!(outcome.lines.is_empty());
124 assert_eq!(outcome.halt, Halt::Normal);
125 }
126
127 #[test]
128 fn stop_run_is_reported() {
129 let outcome = run_source("PROCEDURE DIVISION.\nMAIN.\n DISPLAY 'X'.\n STOP RUN.\n")
130 .expect("pipeline OK");
131 assert_eq!(outcome.lines, vec!["X".to_string()]);
132 assert_eq!(outcome.halt, Halt::StopRun);
133 }
134
135 #[test]
136 fn endless_loop_is_cut_by_the_step_limit() {
137 // `PERFORM UNTIL 1 = 0` nunca se cumple — el tope lo corta.
138 let outcome = run_source(
139 "PROCEDURE DIVISION.\n\
140 MAIN.\n\
141 PERFORM UNTIL 1 = 0\n\
142 CONTINUE\n\
143 END-PERFORM.\n",
144 )
145 .expect("pipeline OK");
146 assert_eq!(outcome.halt, Halt::StepLimit);
147 }
148
149 #[test]
150 fn lex_error_surfaces() {
151 let err = run_source("PROCEDURE DIVISION.\nMAIN.\n DISPLAY 'sin cerrar.\n").unwrap_err();
152 assert!(matches!(err, ShadowError::Lex(_)));
153 }
154}