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<Operand> a
tests: Vec<WhenTest>, 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: intérprete sombra y crate compilado
dan la misma salida.
Tests: charka-ir 27, charka-codegen 21, charka-shadow 19. fmt +
clippy limpios.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
7867d6830e128b8a2afe95cb752093e12a184988 · por sergio · 2026-05-21T22:22:43Z (hace 5 meses) · padre 2728698f5 · ver el árbol en este commit
Delta semántico · 10
crates/modules/charka/charka-codegen/src/lib.rs
crates/modules/charka/charka-codegen/src/stmt.rs
crates/modules/charka/charka-ir/src/ast.rs
crates/modules/charka/charka-ir/src/lib.rs
crates/modules/charka/charka-ir/src/stmt.rs
crates/modules/charka/charka-shadow/src/interp.rs
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::<Vec<_>>()
.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<Operand>,
+ pub tests: Vec<WhenTest>,
pub body: Vec<Stmt>,
}
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<Operand>` a
+ `tests: Vec<WhenTest>`, 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.