//! `charka-shadow` — el validador en sombra del transpilador. //! //! Certifica que el pipeline de charka (lexer → parser → IR → codegen) //! preserva la semántica del programa COBOL original. Lo hace con una //! **ejecución sombra**: un intérprete que corre el [`Ir`] directamente //! sobre los tipos de `charka-runtime`, sin compilar nada. //! //! El intérprete es una segunda ruta de ejecución, independiente del //! código que emite `charka-codegen`. Si la sombra y el transpilado //! produjeran salidas distintas, eso delataría un bug del codegen. //! //! - [`interpret`] — ejecuta un `Ir` y devuelve su salida. //! - [`run_source`] — el pipeline completo, de fuente COBOL a salida. //! //! La referencia contra la que se comparan los resultados es, en la //! v1, un conjunto de salidas esperadas verificadas a mano (el corpus //! en `crates/modules/charka/corpus/`). Cuando haya un GnuCOBOL //! disponible, un modo futuro podrá diferenciar contra el compilador //! de COBOL real — la validación «original vs transpilado» plena. //! //! El intérprete tiene un tope de pasos: un bucle que no termina se //! corta con [`Halt::StepLimit`] en vez de colgarse. #![forbid(unsafe_code)] mod field; mod interp; use charka_ir::Ir; use interp::Machine; /// Cómo terminó una ejecución sombra. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum Halt { /// Cayó por el final del PROCEDURE division. Normal, /// Un `STOP RUN` o `GOBACK`. StopRun, /// Se agotó el tope de pasos (un bucle que no termina). StepLimit, } /// El resultado de una ejecución sombra. #[derive(Debug, Clone, PartialEq, Eq)] pub struct Outcome { /// Las líneas que el programa emitió por `DISPLAY`. pub lines: Vec, /// Cómo terminó. pub halt: Halt, } /// Falla del pipeline previo al intérprete. #[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)] pub enum ShadowError { #[error("error de léxico: {0}")] Lex(#[from] charka_lexer::LexError), #[error("error de parseo: {0}")] Parse(#[from] charka_parser::ParseError), } /// Ejecuta un [`Ir`] en sombra y captura su salida. pub fn interpret(ir: &Ir) -> Outcome { let mut machine = Machine::new(ir); machine.run(); let halt = if machine.step_limit_hit { Halt::StepLimit } else if machine.stopped { Halt::StopRun } else { Halt::Normal }; Outcome { lines: machine.output, halt, } } /// Corre el pipeline completo: fuente COBOL (formato libre) → salida. pub fn run_source(cobol: &str) -> Result { let tokens = charka_lexer::lex(cobol, charka_lexer::SourceFormat::Free)?; let program = charka_parser::parse(&tokens)?; let ir = charka_ir::lower(&program); Ok(interpret(&ir)) } #[cfg(test)] mod tests { use super::*; /// Verifica un programa del corpus contra su salida esperada. La /// comparación ignora los espacios finales de cada línea. fn check(cobol: &str, expected: &str) { let outcome = run_source(cobol).expect("el pipeline no debe fallar"); let got: Vec<&str> = outcome.lines.iter().map(|l| l.trim_end()).collect(); let want: Vec<&str> = expected.lines().map(|l| l.trim_end()).collect(); assert_eq!(got, want, "salida sombra distinta de la esperada"); } /// Declara un test que corre un programa del corpus. macro_rules! corpus_test { ($name:ident, $file:literal) => { #[test] fn $name() { check( include_str!(concat!("../../corpus/", $file, ".cob")), include_str!(concat!("../../corpus/", $file, ".expected")), ); } }; } corpus_test!(corpus_01_hola, "01-hola"); corpus_test!(corpus_02_aritmetica, "02-aritmetica"); corpus_test!(corpus_03_condicional, "03-condicional"); corpus_test!(corpus_04_bucle, "04-bucle"); corpus_test!(corpus_05_factorial, "05-factorial"); corpus_test!(corpus_06_nomina, "06-nomina"); corpus_test!(corpus_07_clasificar, "07-clasificar"); corpus_test!(corpus_08_varying, "08-varying"); corpus_test!(corpus_09_evaluar, "09-evaluar"); corpus_test!(corpus_10_condicion, "10-condicion"); corpus_test!(corpus_11_tabla, "11-tabla"); corpus_test!(corpus_12_cadenas, "12-cadenas"); corpus_test!(corpus_13_inspeccion, "13-inspeccion"); corpus_test!(corpus_14_clasifica, "14-clasifica"); corpus_test!(corpus_15_resetear, "15-resetear"); corpus_test!(corpus_16_bandera, "16-bandera"); corpus_test!(corpus_17_rangopar, "17-rangopar"); corpus_test!(corpus_18_fichero, "18-fichero"); corpus_test!(corpus_19_reporte, "19-reporte"); #[test] fn empty_source_runs_clean() { let outcome = run_source("").expect("pipeline OK"); assert!(outcome.lines.is_empty()); assert_eq!(outcome.halt, Halt::Normal); } #[test] fn stop_run_is_reported() { let outcome = run_source("PROCEDURE DIVISION.\nMAIN.\n DISPLAY 'X'.\n STOP RUN.\n") .expect("pipeline OK"); assert_eq!(outcome.lines, vec!["X".to_string()]); assert_eq!(outcome.halt, Halt::StopRun); } #[test] fn endless_loop_is_cut_by_the_step_limit() { // `PERFORM UNTIL 1 = 0` nunca se cumple — el tope lo corta. let outcome = run_source( "PROCEDURE DIVISION.\n\ MAIN.\n\ PERFORM UNTIL 1 = 0\n\ CONTINUE\n\ END-PERFORM.\n", ) .expect("pipeline OK"); assert_eq!(outcome.halt, Halt::StepLimit); } #[test] fn perform_varying_out_of_line() { // `PERFORM CONTAR VARYING ...` — el párrafo es el cuerpo del bucle. // WS-I = 1, 3, 5, 7, 9 (FROM 1 BY 2 UNTIL > 9) → 5 iteraciones. let outcome = run_source( "DATA DIVISION.\n\ WORKING-STORAGE SECTION.\n\ 01 WS-I PIC 9(2) VALUE 0.\n\ 01 WS-N PIC 9(3) VALUE 0.\n\ PROCEDURE DIVISION.\n\ MAIN.\n\ PERFORM CONTAR VARYING WS-I FROM 1 BY 2 UNTIL WS-I > 9.\n\ DISPLAY WS-N.\n\ STOP RUN.\n\ CONTAR.\n\ ADD 1 TO WS-N.\n", ) .expect("pipeline OK"); assert_eq!(outcome.lines, vec!["005".to_string()]); } #[test] fn lex_error_surfaces() { let err = run_source("PROCEDURE DIVISION.\nMAIN.\n DISPLAY 'sin cerrar.\n").unwrap_err(); assert!(matches!(err, ShadowError::Lex(_))); } }