diff --git a/crates/modules/charka/SDD.md b/crates/modules/charka/SDD.md index 5177f76..73dab52 100644 --- a/crates/modules/charka/SDD.md +++ b/crates/modules/charka/SDD.md @@ -89,8 +89,10 @@ Tercera etapa: `Program` → `Ir`. Aquí se parsea cada `Sentence` cruda empieza el verbo del siguiente. El parser usa palabras "frontera" (verbos + terminadores `END-*`/`ELSE` + conectores `TO`/`GIVING`...) 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`. - Fuera de alcance v1: `EVALUATE`, `STRING`/`UNSTRING`, E/S de - ficheros, `PERFORM VARYING`, CICS, SQL embebido. + ficheros, CICS, SQL embebido. ## charka-runtime diff --git a/crates/modules/charka/charka-codegen/src/lib.rs b/crates/modules/charka/charka-codegen/src/lib.rs index e7a151c..84147bd 100644 --- a/crates/modules/charka/charka-codegen/src/lib.rs +++ b/crates/modules/charka/charka-codegen/src/lib.rs @@ -334,6 +334,22 @@ mod tests { assert!(out.contains("for _ in 0..3usize {")); } + #[test] + fn perform_varying_emits_init_loop_and_increment() { + let out = gen("DATA DIVISION.\n\ + WORKING-STORAGE SECTION.\n\ + 01 WS-I PIC 9(2).\n\ + 01 WS-N PIC 9(3).\n\ + PROCEDURE DIVISION.\n\ + MAIN.\n\ + PERFORM VARYING WS-I FROM 1 BY 1 UNTIL WS-I > 5\n\ + ADD 1 TO WS-N\n\ + END-PERFORM.\n"); + assert!(out.contains("self.ws_i.store(dec(\"1\"));")); + assert!(out.contains("while !((self.ws_i.value()) > (dec(\"5\"))) {")); + assert!(out.contains("self.ws_i.store(self.ws_i.value().add(&(dec(\"1\"))));")); + } + #[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 13d6803..c907577 100644 --- a/crates/modules/charka/charka-codegen/src/stmt.rs +++ b/crates/modules/charka/charka-codegen/src/stmt.rs @@ -360,5 +360,20 @@ fn emit_perform(em: &mut Emitter, sym: &Symbols, p: &Perform) { em.dedent(); em.line("}"); } + PerformControl::Varying { + var, + from, + by, + until, + } => { + // var = from; mientras no se cumpla `until`: cuerpo; var += by. + emit_store(em, sym, var, &operand_decimal(sym, from), false); + em.line(&format!("while !({}) {{", emit_cond(sym, until))); + em.indent(); + emit_body(em, sym); + emit_inplace(em, sym, var, "add", &operand_decimal(sym, by), false); + em.dedent(); + em.line("}"); + } } } diff --git a/crates/modules/charka/charka-ir/src/ast.rs b/crates/modules/charka/charka-ir/src/ast.rs index 5ea48b0..8bcbdc1 100644 --- a/crates/modules/charka/charka-ir/src/ast.rs +++ b/crates/modules/charka/charka-ir/src/ast.rs @@ -197,4 +197,13 @@ pub enum PerformControl { Times(Operand), /// `UNTIL cond`. Until(Cond), + /// `VARYING var FROM from BY by UNTIL until` — el bucle con + /// variable de control: `var = from`; mientras no se cumpla + /// `until`, ejecuta el cuerpo y hace `var = var + by`. + Varying { + var: String, + from: Operand, + by: Operand, + until: Cond, + }, } diff --git a/crates/modules/charka/charka-ir/src/lib.rs b/crates/modules/charka/charka-ir/src/lib.rs index 9bccad4..2dc6abc 100644 --- a/crates/modules/charka/charka-ir/src/lib.rs +++ b/crates/modules/charka/charka-ir/src/lib.rs @@ -17,9 +17,9 @@ //! `ACCEPT`, `COMPUTE` (con expresiones con precedencia), `ADD`, //! `SUBTRACT`, `MULTIPLY`, `DIVIDE`, `IF`/`ELSE`/`END-IF` (con //! condiciones `AND`/`OR`/`NOT`), `PERFORM` (fuera de línea, en línea, -//! `TIMES`, `UNTIL`), `GO TO`, `STOP RUN`, `GOBACK`, `EXIT`, -//! `CONTINUE`. Fuera de alcance: `EVALUATE`, `STRING`/`UNSTRING`, E/S -//! de ficheros, `PERFORM VARYING`, CICS y SQL embebido. +//! `TIMES`, `UNTIL`, `VARYING`), `GO TO`, `STOP RUN`, `GOBACK`, +//! `EXIT`, `CONTINUE`. Fuera de alcance: `EVALUATE`, +//! `STRING`/`UNSTRING`, E/S de ficheros, CICS y SQL embebido. #![forbid(unsafe_code)] @@ -286,6 +286,31 @@ mod tests { } } + #[test] + fn perform_varying_inline() { + let b = body( + "PERFORM VARYING WS-I FROM 1 BY 2 UNTIL WS-I > 9 \ + CONTINUE END-PERFORM.", + ); + match &b[0] { + Stmt::Perform(p) => match &p.control { + PerformControl::Varying { + var, + from, + by, + until, + } => { + assert_eq!(var, "WS-I"); + assert_eq!(from, &Operand::Num("1".into())); + assert_eq!(by, &Operand::Num("2".into())); + assert!(matches!(until, Cond::Compare { .. })); + } + other => panic!("se esperaba Varying, vino {other:?}"), + }, + other => panic!("se esperaba PERFORM, vino {other:?}"), + } + } + #[test] fn several_statements_in_one_sentence() { let b = body("MOVE 1 TO X DISPLAY X STOP RUN."); diff --git a/crates/modules/charka/charka-ir/src/stmt.rs b/crates/modules/charka/charka-ir/src/stmt.rs index 4dff640..74d712a 100644 --- a/crates/modules/charka/charka-ir/src/stmt.rs +++ b/crates/modules/charka/charka-ir/src/stmt.rs @@ -288,6 +288,14 @@ fn parse_if(c: &mut Cursor) -> Stmt { fn parse_perform(c: &mut Cursor) -> Stmt { c.bump(); // PERFORM + // `PERFORM VARYING ... ... END-PERFORM` — cuerpo en línea. + if c.eat_word("VARYING") { + let control = parse_varying(c); + let body = parse_statements(c, &["END-PERFORM"]); + c.eat_word("END-PERFORM"); + return inline_perform(body, control); + } + // `PERFORM UNTIL cond ... END-PERFORM` — cuerpo en línea. if c.eat_word("UNTIL") { let cond = parse_cond(c); @@ -312,9 +320,9 @@ fn parse_perform(c: &mut Cursor) -> Stmt { return inline_perform(body, PerformControl::Once); } - // `PERFORM PARA [THRU PARA2] [n TIMES | UNTIL cond]` — fuera de línea. + // `PERFORM PARA [THRU PARA2] [VARYING ... | n TIMES | UNTIL cond]`. let Some(name) = parse_one_name(c) else { - // Forma no soportada (p. ej. `PERFORM VARYING`): perform vacío. + // Forma no reconocida tras `PERFORM`: perform vacío. return inline_perform(Vec::new(), PerformControl::Once); }; let thru = if c.eat_word("THRU") || c.eat_word("THROUGH") { @@ -322,7 +330,9 @@ fn parse_perform(c: &mut Cursor) -> Stmt { } else { None }; - let control = if c.eat_word("UNTIL") { + let control = if c.eat_word("VARYING") { + parse_varying(c) + } else if c.eat_word("UNTIL") { PerformControl::Until(parse_cond(c)) } else if at_count(c) { let n = parse_operand(c); @@ -345,6 +355,24 @@ fn inline_perform(body: Vec, control: PerformControl) -> Stmt { }) } +/// Parsea la cláusula `VARYING var FROM x BY y UNTIL cond`, ya +/// consumida la palabra `VARYING`. +fn parse_varying(c: &mut Cursor) -> PerformControl { + let var = parse_one_name(c).unwrap_or_default(); + c.eat_word("FROM"); + let from = parse_operand(c); + c.eat_word("BY"); + let by = parse_operand(c); + c.eat_word("UNTIL"); + let until = parse_cond(c); + PerformControl::Varying { + var, + from, + by, + until, + } +} + /// ¿El cursor está sobre ` TIMES`? fn at_count(c: &Cursor) -> bool { match c.peek().map(|t| t.kind) { diff --git a/crates/modules/charka/charka-shadow/src/interp.rs b/crates/modules/charka/charka-shadow/src/interp.rs index 71614f7..da84944 100644 --- a/crates/modules/charka/charka-shadow/src/interp.rs +++ b/crates/modules/charka/charka-shadow/src/interp.rs @@ -265,6 +265,28 @@ impl<'a> Machine<'a> { return Flow::Stop; } }, + PerformControl::Varying { + var, + from, + by, + until, + } => { + let start = self.eval_decimal(from); + self.store(var, start, false); + loop { + if self.tick() { + return Flow::Stop; + } + if self.eval_cond(until) { + return Flow::Normal; + } + if let Flow::Stop = self.run_target(&p.target) { + return Flow::Stop; + } + let next = self.field_value(var).add(&self.eval_decimal(by)); + self.store(var, next, false); + } + } } } diff --git a/crates/modules/charka/charka-shadow/src/lib.rs b/crates/modules/charka/charka-shadow/src/lib.rs index 325963d..adf2230 100644 --- a/crates/modules/charka/charka-shadow/src/lib.rs +++ b/crates/modules/charka/charka-shadow/src/lib.rs @@ -116,6 +116,7 @@ mod tests { 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"); #[test] fn empty_source_runs_clean() { @@ -146,6 +147,27 @@ mod tests { 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(); diff --git a/crates/modules/charka/corpus/08-varying.cob b/crates/modules/charka/corpus/08-varying.cob new file mode 100644 index 0000000..e33fcb4 --- /dev/null +++ b/crates/modules/charka/corpus/08-varying.cob @@ -0,0 +1,14 @@ +* corpus charka — nivel 4: PERFORM VARYING, el bucle clásico de COBOL +IDENTIFICATION DIVISION. +PROGRAM-ID. SUMAR. +DATA DIVISION. +WORKING-STORAGE SECTION. +01 WS-I PIC 9(2) VALUE 0. +01 WS-SUMA PIC 9(5) VALUE 0. +PROCEDURE DIVISION. +MAIN. + PERFORM VARYING WS-I FROM 1 BY 1 UNTIL WS-I > 10 + ADD WS-I TO WS-SUMA + END-PERFORM. + DISPLAY 'SUMA 1 A 10 = ' WS-SUMA. + STOP RUN. diff --git a/crates/modules/charka/corpus/08-varying.expected b/crates/modules/charka/corpus/08-varying.expected new file mode 100644 index 0000000..a747e94 --- /dev/null +++ b/crates/modules/charka/corpus/08-varying.expected @@ -0,0 +1 @@ +SUMA 1 A 10 = 00055 diff --git a/crates/modules/charka/corpus/README.md b/crates/modules/charka/corpus/README.md index 27911b0..a11abac 100644 --- a/crates/modules/charka/corpus/README.md +++ b/crates/modules/charka/corpus/README.md @@ -16,6 +16,7 @@ salida correcta, una línea por `DISPLAY`. | `05-factorial` | 4 | `PERFORM UNTIL` en línea, `MULTIPLY` in situ | | `06-nomina` | 5 | grupos, `COMPUTE` con paréntesis, `ROUNDED`, V99 | | `07-clasificar` | 5 | `IF` anidado, condiciones con `AND` | +| `08-varying` | 4 | `PERFORM VARYING` — el bucle con variable de control| ## Formato diff --git a/docs/changelog/charka.md b/docs/changelog/charka.md index 66beb67..045ef32 100644 --- a/docs/changelog/charka.md +++ b/docs/changelog/charka.md @@ -3,6 +3,21 @@ 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): PERFORM VARYING — el bucle con variable de control + +El bucle más usado de COBOL, que antes el parser degradaba a un +`PERFORM` vacío. Ahora atraviesa el pipeline entero. + +- IR: `PerformControl::Varying { var, from, by, until }`. +- Parser: reconoce `PERFORM VARYING var FROM x BY y UNTIL cond` en + línea (`END-PERFORM`) y fuera de línea (`PERFORM párrafo VARYING …`). +- Codegen: emite `var = from; while !(until) { cuerpo; var += by; }`. +- Shadow: el intérprete inicializa la variable, evalúa la condición + antes de cada vuelta e incrementa al final. +- Corpus: programa nuevo `08-varying` (suma 1..10 con `PERFORM + VARYING`). Verificado: el intérprete sombra y el crate compilado por + `scaffold` dan ambos `SUMA 1 A 10 = 00055`. + ### feat(charka): CLI del transpilador — transpile / scaffold / run / check App nueva `crates/apps/charka` — el binario `charka`, que vuelve usable