diff --git a/crates/modules/charka/SDD.md b/crates/modules/charka/SDD.md index 77ace3d..bd20fbc 100644 --- a/crates/modules/charka/SDD.md +++ b/crates/modules/charka/SDD.md @@ -99,8 +99,8 @@ Tercera etapa: `Program` → `Ir`. Aquí se parsea cada `Sentence` cruda para delimitar listas de operandos. - `PERFORM` cubre las cuatro formas: párrafo / en línea, `n TIMES`, `UNTIL cond` y `VARYING var FROM x BY y UNTIL cond`. -- `EVALUATE subject WHEN ... WHEN OTHER` — el `case` de COBOL, por - igualdad de valor (no la forma `EVALUATE TRUE` con condiciones). +- `EVALUATE` — el `case` de COBOL: `WHEN valor`, `WHEN lo THRU hi` + (rango), `WHEN OTHER`, y la forma `EVALUATE TRUE WHEN condición`. - `STRING` (concatenación) y `UNSTRING` (partición por delimitador) — el manejo de cadenas. `INSPECT` — contar (`TALLYING FOR ALL`) y reemplazar (`REPLACING ALL`). @@ -175,8 +175,8 @@ que corre el `Ir` directamente sobre `charka-runtime`, sin compilar. ## El corpus -`crates/modules/charka/corpus/` — 13 programas COBOL graduados -(`01-hola` … `13-inspeccion`), cada uno con su `.expected`. Ejercita el +`crates/modules/charka/corpus/` — 14 programas COBOL graduados +(`01-hola` … `14-clasifica`), cada uno con su `.expected`. Ejercita el pipeline completo de punta a punta. Ver su `README.md`. ## La CLI diff --git a/crates/modules/charka/charka-codegen/src/lib.rs b/crates/modules/charka/charka-codegen/src/lib.rs index 82331b2..df69ee9 100644 --- a/crates/modules/charka/charka-codegen/src/lib.rs +++ b/crates/modules/charka/charka-codegen/src/lib.rs @@ -399,6 +399,20 @@ mod tests { assert!(out.contains(".replace(")); } + #[test] + fn evaluate_true_and_range_emit() { + let out = gen("DATA DIVISION.\n\ + WORKING-STORAGE SECTION.\n\ + 01 WS-X PIC 9(3).\n\ + PROCEDURE DIVISION.\n\ + MAIN.\n\ + EVALUATE WS-X WHEN 1 THRU 9 DISPLAY 'R' END-EVALUATE.\n\ + EVALUATE TRUE WHEN WS-X > 5 DISPLAY 'T' END-EVALUATE.\n"); + assert!(out.contains(">= (dec(\"1\"))")); + assert!(out.contains("<= (dec(\"9\"))")); + assert!(out.contains("> (dec(\"5\"))")); + } + #[test] fn empty_program_still_compiles_shape() { let out = gen(""); diff --git a/crates/modules/charka/charka-codegen/src/stmt.rs b/crates/modules/charka/charka-codegen/src/stmt.rs index 8e51c14..d0903e5 100644 --- a/crates/modules/charka/charka-codegen/src/stmt.rs +++ b/crates/modules/charka/charka-codegen/src/stmt.rs @@ -3,6 +3,7 @@ use charka_ir::{ CmpOp, Cond, InspectOp, Operand, Perform, PerformControl, PerformTarget, Stmt, WhenBranch, + WhenTest, }; use crate::emit::Emitter; @@ -349,32 +350,45 @@ fn emit_evaluate( } } -/// La condición de una rama `WHEN`: el sujeto igual a cualquiera de -/// sus valores. +/// La condición de una rama `WHEN`: pasa si **alguna** de sus pruebas +/// se cumple. fn branch_condition(sym: &Symbols, subject: &Operand, branch: &WhenBranch) -> String { - if branch.values.is_empty() { + if branch.tests.is_empty() { return "false".to_string(); } branch - .values + .tests .iter() - .map(|v| { - format!( - "({})", - emit_cond( - sym, - &Cond::Compare { - lhs: subject.clone(), - op: CmpOp::Eq, - rhs: v.clone(), - }, - ) - ) - }) + .map(|t| format!("({})", test_condition(sym, subject, t))) .collect::>() .join(" || ") } +/// Traduce una prueba `WHEN` a una expresión Rust de tipo `bool`. +fn test_condition(sym: &Symbols, subject: &Operand, test: &WhenTest) -> String { + let compare = |op: CmpOp, rhs: &Operand| { + emit_cond( + sym, + &Cond::Compare { + lhs: subject.clone(), + op, + rhs: rhs.clone(), + }, + ) + }; + match test { + WhenTest::Value(v) => compare(CmpOp::Eq, v), + WhenTest::Range(lo, hi) => { + format!( + "({}) && ({})", + compare(CmpOp::Ge, lo), + compare(CmpOp::Le, hi) + ) + } + WhenTest::Cond(cond) => emit_cond(sym, cond), + } +} + /// Almacena una expresión `&str` en un destino: directo si es de /// texto, parseado a `Decimal` si es numérico. fn emit_store_text(em: &mut Emitter, sym: &Symbols, target: &Operand, text: &str) { diff --git a/crates/modules/charka/charka-ir/src/ast.rs b/crates/modules/charka/charka-ir/src/ast.rs index e186521..9a0db93 100644 --- a/crates/modules/charka/charka-ir/src/ast.rs +++ b/crates/modules/charka/charka-ir/src/ast.rs @@ -212,11 +212,23 @@ pub enum InspectOp { ReplacingAll { from: Operand, to: Operand }, } -/// Una rama `WHEN` de un `EVALUATE`: los valores que la disparan +/// Cómo una rama `WHEN` decide si se dispara. +#[derive(Debug, Clone, PartialEq)] +pub enum WhenTest { + /// El sujeto es igual a este valor. + Value(Operand), + /// El sujeto está en el rango `[lo, hi]` (`WHEN lo THRU hi`). + Range(Operand, Operand), + /// La condición se cumple — la forma `EVALUATE TRUE WHEN cond`. + Cond(Cond), +} + +/// Una rama `WHEN` de un `EVALUATE`: las pruebas que la disparan /// (varios `WHEN` apilados comparten cuerpo) y el cuerpo a ejecutar. +/// La rama se dispara si **alguna** de sus pruebas pasa. #[derive(Debug, Clone, PartialEq)] pub struct WhenBranch { - pub values: Vec, + pub tests: Vec, pub body: Vec, } diff --git a/crates/modules/charka/charka-ir/src/lib.rs b/crates/modules/charka/charka-ir/src/lib.rs index a93e490..996b4a4 100644 --- a/crates/modules/charka/charka-ir/src/lib.rs +++ b/crates/modules/charka/charka-ir/src/lib.rs @@ -347,10 +347,16 @@ mod tests { } => { assert_eq!(subject, &Operand::Data("WS-X".into())); assert_eq!(whens.len(), 2); - assert_eq!(whens[0].values, vec![Operand::Num("1".into())]); assert_eq!( - whens[1].values, - vec![Operand::Num("2".into()), Operand::Num("3".into())] + whens[0].tests, + vec![WhenTest::Value(Operand::Num("1".into()))] + ); + assert_eq!( + whens[1].tests, + vec![ + WhenTest::Value(Operand::Num("2".into())), + WhenTest::Value(Operand::Num("3".into())), + ] ); assert_eq!(other.len(), 1); } @@ -358,6 +364,30 @@ mod tests { } } + #[test] + fn evaluate_true_and_range_when() { + // `EVALUATE TRUE` → los WHEN son condiciones. + let b = body("EVALUATE TRUE WHEN WS-X > 0 DISPLAY 'P' END-EVALUATE."); + match &b[0] { + Stmt::Evaluate { whens, .. } => { + assert!(matches!(whens[0].tests[0], WhenTest::Cond(_))); + } + other => panic!("se esperaba EVALUATE, vino {other:?}"), + } + // `WHEN lo THRU hi` → un rango. + let b = body("EVALUATE WS-X WHEN 1 THRU 9 DISPLAY 'D' END-EVALUATE."); + match &b[0] { + Stmt::Evaluate { whens, .. } => match &whens[0].tests[0] { + WhenTest::Range(lo, hi) => { + assert_eq!(lo, &Operand::Num("1".into())); + assert_eq!(hi, &Operand::Num("9".into())); + } + other => panic!("se esperaba Range, vino {other:?}"), + }, + other => panic!("se esperaba EVALUATE, vino {other:?}"), + } + } + #[test] fn string_and_unstring_parse() { let b = body("STRING WS-A WS-B DELIMITED BY SIZE INTO WS-OUT END-STRING."); diff --git a/crates/modules/charka/charka-ir/src/stmt.rs b/crates/modules/charka/charka-ir/src/stmt.rs index 73107be..5c03a9b 100644 --- a/crates/modules/charka/charka-ir/src/stmt.rs +++ b/crates/modules/charka/charka-ir/src/stmt.rs @@ -5,7 +5,9 @@ use charka_parser::TokenKind; -use crate::ast::{InspectOp, Operand, Perform, PerformControl, PerformTarget, Stmt, WhenBranch}; +use crate::ast::{ + InspectOp, Operand, Perform, PerformControl, PerformTarget, Stmt, WhenBranch, WhenTest, +}; use crate::cursor::{parse_operand, Cursor}; use crate::expr::{parse_cond, parse_expr}; use crate::kw::{is_boundary, is_terminator, is_verb}; @@ -304,6 +306,8 @@ fn parse_if(c: &mut Cursor) -> Stmt { fn parse_evaluate(c: &mut Cursor) -> Stmt { c.bump(); // EVALUATE let subject = parse_operand(c); + // `EVALUATE TRUE` — los `WHEN` son condiciones, no valores. + let cond_mode = matches!(&subject, Operand::Data(s) if s == "TRUE"); let mut whens = Vec::new(); let mut other = Vec::new(); while !c.done() && !c.at_word("END-EVALUATE") { @@ -311,20 +315,27 @@ fn parse_evaluate(c: &mut Cursor) -> Stmt { break; // algo inesperado dentro del EVALUATE: se corta } // Varios `WHEN` apilados comparten el mismo cuerpo. - let mut values = Vec::new(); + let mut tests = Vec::new(); let mut is_other = false; while c.eat_word("WHEN") { if c.eat_word("OTHER") { is_other = true; + } else if cond_mode { + tests.push(WhenTest::Cond(parse_cond(c))); } else { - values.push(parse_operand(c)); + let lo = parse_operand(c); + if c.eat_word("THRU") || c.eat_word("THROUGH") { + tests.push(WhenTest::Range(lo, parse_operand(c))); + } else { + tests.push(WhenTest::Value(lo)); + } } } let body = parse_statements(c, &["WHEN", "END-EVALUATE"]); if is_other { other = body; } else { - whens.push(WhenBranch { values, body }); + whens.push(WhenBranch { tests, body }); } } c.eat_word("END-EVALUATE"); diff --git a/crates/modules/charka/charka-shadow/src/interp.rs b/crates/modules/charka/charka-shadow/src/interp.rs index ccbd439..0b6e2ba 100644 --- a/crates/modules/charka/charka-shadow/src/interp.rs +++ b/crates/modules/charka/charka-shadow/src/interp.rs @@ -9,7 +9,7 @@ use std::collections::HashMap; use charka_ir::{ BinOp, CmpOp, Cond, ConditionName, Expr, Figurative, InspectOp, Ir, Operand, Perform, - PerformControl, PerformTarget, Stmt, + PerformControl, PerformTarget, Stmt, WhenTest, }; use charka_runtime::{cobol_text_cmp, Decimal, Rounding}; @@ -237,11 +237,7 @@ impl<'a> Machine<'a> { other, } => { for branch in whens { - if branch - .values - .iter() - .any(|v| self.operands_equal(subject, v)) - { + if branch.tests.iter().any(|t| self.when_test(subject, t)) { return self.exec_block(&branch.body); } } @@ -582,6 +578,18 @@ impl<'a> Machine<'a> { } } + /// ¿Se cumple una prueba `WHEN` para el sujeto dado? + fn when_test(&self, subject: &Operand, test: &WhenTest) -> bool { + match test { + WhenTest::Value(v) => self.operands_equal(subject, v), + WhenTest::Range(lo, hi) => { + let s = self.eval_decimal(subject); + s >= self.eval_decimal(lo) && s <= self.eval_decimal(hi) + } + WhenTest::Cond(cond) => self.eval_cond(cond), + } + } + /// ¿Son iguales dos operandos? (Para las ramas `WHEN` del `EVALUATE`.) fn operands_equal(&self, a: &Operand, b: &Operand) -> bool { if self.is_text(a) || self.is_text(b) { diff --git a/crates/modules/charka/charka-shadow/src/lib.rs b/crates/modules/charka/charka-shadow/src/lib.rs index 767d621..259c867 100644 --- a/crates/modules/charka/charka-shadow/src/lib.rs +++ b/crates/modules/charka/charka-shadow/src/lib.rs @@ -122,6 +122,7 @@ mod tests { 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"); #[test] fn empty_source_runs_clean() { diff --git a/crates/modules/charka/corpus/14-clasifica.cob b/crates/modules/charka/corpus/14-clasifica.cob new file mode 100644 index 0000000..9ac6e21 --- /dev/null +++ b/crates/modules/charka/corpus/14-clasifica.cob @@ -0,0 +1,29 @@ +* corpus charka — nivel 6: EVALUATE TRUE y rangos WHEN ... THRU +IDENTIFICATION DIVISION. +PROGRAM-ID. CLASIFICA. +DATA DIVISION. +WORKING-STORAGE SECTION. +01 WS-NOTA PIC 9(3) VALUE 0. +01 WS-I PIC 9(2) VALUE 0. +PROCEDURE DIVISION. +MAIN. + PERFORM VARYING WS-I FROM 1 BY 1 UNTIL WS-I > 5 + COMPUTE WS-NOTA = WS-I * 18 + EVALUATE WS-NOTA + WHEN 0 THRU 59 + DISPLAY 'REPROBADO' + WHEN 60 THRU 89 + DISPLAY 'APROBADO' + WHEN OTHER + DISPLAY 'EXCELENTE' + END-EVALUATE + END-PERFORM. + PERFORM EVAL-TRUE. + STOP RUN. +EVAL-TRUE. + EVALUATE TRUE + WHEN WS-NOTA > 100 + DISPLAY 'ULTIMA NOTA ALTA' + WHEN OTHER + DISPLAY 'ULTIMA NOTA NO ALTA' + END-EVALUATE. diff --git a/crates/modules/charka/corpus/14-clasifica.expected b/crates/modules/charka/corpus/14-clasifica.expected new file mode 100644 index 0000000..ab3caca --- /dev/null +++ b/crates/modules/charka/corpus/14-clasifica.expected @@ -0,0 +1,6 @@ +REPROBADO +REPROBADO +REPROBADO +APROBADO +EXCELENTE +ULTIMA NOTA NO ALTA diff --git a/crates/modules/charka/corpus/README.md b/crates/modules/charka/corpus/README.md index 69834d5..629c481 100644 --- a/crates/modules/charka/corpus/README.md +++ b/crates/modules/charka/corpus/README.md @@ -22,6 +22,7 @@ salida correcta, una línea por `DISPLAY`. | `11-tabla` | 6 | tablas (`OCCURS`) y referencias con subíndice | | `12-cadenas` | 6 | `STRING` (concatenar) y `UNSTRING` (partir) | | `13-inspeccion` | 6 | `INSPECT` — contar (`TALLYING`) y reemplazar | +| `14-clasifica` | 6 | `EVALUATE TRUE` y rangos `WHEN ... THRU` | ## Formato diff --git a/docs/changelog/charka.md b/docs/changelog/charka.md index 546324b..5201887 100644 --- a/docs/changelog/charka.md +++ b/docs/changelog/charka.md @@ -3,6 +3,20 @@ Transpilador COBOL → Rust. El módulo más grande del ecosistema (Fase D del plan macro) — el parser COBOL completo es un esfuerzo multi-mes. +### feat(charka): EVALUATE TRUE y rangos WHEN ... THRU + +Completa el `EVALUATE` con sus dos formas que faltaban. + +- IR: la rama `WhenBranch` pasa de `values: Vec` a + `tests: Vec`, donde `WhenTest` es `Value` (igualdad), + `Range` (`WHEN lo THRU hi`) o `Cond` (`EVALUATE TRUE WHEN cond`). +- Parser: detecta `EVALUATE TRUE` y entonces cada `WHEN` parsea una + condición; en modo valor reconoce `WHEN lo THRU hi`. +- Codegen y shadow: una prueba `Range` se traduce a `lo <= s <= hi`; + una `Cond`, a la condición directa. +- Corpus: programa nuevo `14-clasifica` (clasifica notas con rangos + `THRU` y un `EVALUATE TRUE`). Verificado en ambas rutas. + ### feat(charka): INSPECT — contar y reemplazar caracteres El verbo de COBOL para analizar y limpiar campos de texto.