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>
This commit is contained in:
sergio
2026-05-21 22:22:43 +00:00
parent 2728698f5e
commit 7867d6830e
12 changed files with 176 additions and 36 deletions
+4 -4
View File
@@ -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
@@ -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("");
@@ -3,6 +3,7 @@
use charka_ir::{
CmpOp, Cond, InspectOp, Operand, Perform, PerformControl, PerformTarget, Stmt, WhenBranch,
WhenTest,
};
use crate::emit::Emitter;
@@ -349,30 +350,43 @@ 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!(
"({})",
.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: CmpOp::Eq,
rhs: v.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)
)
})
.collect::<Vec<_>>()
.join(" || ")
}
WhenTest::Cond(cond) => emit_cond(sym, cond),
}
}
/// Almacena una expresión `&str` en un destino: directo si es de
+14 -2
View File
@@ -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>,
}
+33 -3
View File
@@ -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.");
+15 -4
View File
@@ -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");
@@ -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) {
@@ -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() {
@@ -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.
@@ -0,0 +1,6 @@
REPROBADO
REPROBADO
REPROBADO
APROBADO
EXCELENTE
ULTIMA NOTA NO ALTA
+1
View File
@@ -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
+14
View File
@@ -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.