feat(charka): charka-shadow — validador en sombra + corpus COBOL
El pipeline COBOL->Rust queda completo (7 crates) y validado de punta a punta. charka-shadow certifica que el transpilador preserva la semántica del COBOL original con una ejecución sombra: un intérprete que corre el Ir directamente sobre charka-runtime, sin compilar nada. Es una segunda ruta de ejecución, independiente del código que emite charka-codegen — si la sombra y el transpilado divergieran, sería un bug. - interpret(&Ir) -> Outcome ejecuta el IR y captura las líneas de DISPLAY; run_source(&str) corre el pipeline completo. - Tope de pasos (Halt::StepLimit): un bucle que no termina se corta en vez de colgarse. - Módulos: field (datos -> campos vivos) / interp (el motor). Corpus nuevo crates/modules/charka/corpus/ — 7 programas COBOL de complejidad graduada (01-hola .. 07-clasificar) con sus salidas esperadas verificadas a mano: DISPLAY, aritmética con GIVING, IF/ELSE, PERFORM TIMES/UNTIL, grupos, COMPUTE con paréntesis, ROUNDED, IF anidado con AND. Material de prueba del pipeline entero. 11 tests (los 7 del corpus + fuente vacío, STOP RUN, tope de pasos, error de léxico); fmt + clippy limpios. No hay GnuCOBOL en la máquina: la referencia v1 es el corpus; un modo futuro diferenciará contra el compilador real. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,154 @@
|
||||
//! `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<String>,
|
||||
/// 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<Outcome, ShadowError> {
|
||||
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");
|
||||
|
||||
#[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 lex_error_surfaces() {
|
||||
let err = run_source("PROCEDURE DIVISION.\nMAIN.\n DISPLAY 'sin cerrar.\n").unwrap_err();
|
||||
assert!(matches!(err, ShadowError::Lex(_)));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user