From 3902763daa94397756ff2394116daef97814090a Mon Sep 17 00:00:00 2001 From: sergio Date: Thu, 21 May 2026 22:03:48 +0000 Subject: [PATCH] =?UTF-8?q?feat(charka):=20OCCURS=20=E2=80=94=20tablas=20y?= =?UTF-8?q?=20referencias=20con=20sub=C3=ADndice?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Los arrays de COBOL, que antes el transpilador descartaba en silencio. Una rebanada vertical amplia que atraviesa el pipeline entero. - Parser: la cláusula OCCURS n [TIMES] se captura en DataItem. - IR: Operand::Indexed { name, index } — una referencia ELEM(I), con subíndice 1-based. Los destinos de los statements pasan de Vec a Vec, así que se puede escribir a un elemento de tabla (MOVE x TO ELEM(I), COMPUTE ELEM(I) = ...). model::Field gana occurs: Option. - Codegen: un campo OCCURS se emite como Vec/Vec, inicializado con vec![..; n]; una referencia con subíndice indexa el vector (1-based -> 0-based). - Shadow: en el intérprete todo campo es un vector — un escalar es de longitud 1, una tabla de n; las referencias se resuelven a (nombre, índice). - Corpus: programa nuevo 11-tabla (llena una tabla con cuadrados y los suma). Verificado: el intérprete sombra y el crate compilado por scaffold dan ambos SUMA DE CUADRADOS = 000055. Alcance v1: OCCURS elemental, una dimensión, subíndice de un operando. Fuera: OCCURS de grupo, multidimensional, DEPENDING ON. Tests: charka-parser 16, charka-ir 24, charka-codegen 18, charka-shadow 16. fmt + clippy limpios. Co-Authored-By: Claude Opus 4.7 --- crates/modules/charka/SDD.md | 17 +- .../modules/charka/charka-codegen/src/expr.rs | 58 ++++-- .../modules/charka/charka-codegen/src/lib.rs | 31 ++- .../modules/charka/charka-codegen/src/stmt.rs | 163 +++++++-------- .../modules/charka/charka-codegen/src/sym.rs | 3 + crates/modules/charka/charka-ir/src/ast.rs | 21 +- crates/modules/charka/charka-ir/src/cursor.rs | 20 +- crates/modules/charka/charka-ir/src/lib.rs | 30 ++- crates/modules/charka/charka-ir/src/model.rs | 4 + crates/modules/charka/charka-ir/src/stmt.rs | 40 ++-- .../modules/charka/charka-parser/src/lib.rs | 33 ++++ .../modules/charka/charka-shadow/src/field.rs | 20 +- .../charka/charka-shadow/src/interp.rs | 187 +++++++++++------- .../modules/charka/charka-shadow/src/lib.rs | 1 + crates/modules/charka/corpus/11-tabla.cob | 20 ++ .../modules/charka/corpus/11-tabla.expected | 6 + crates/modules/charka/corpus/README.md | 1 + docs/changelog/charka.md | 24 +++ 18 files changed, 440 insertions(+), 239 deletions(-) create mode 100644 crates/modules/charka/corpus/11-tabla.cob create mode 100644 crates/modules/charka/corpus/11-tabla.expected diff --git a/crates/modules/charka/SDD.md b/crates/modules/charka/SDD.md index 5f35400..f136add 100644 --- a/crates/modules/charka/SDD.md +++ b/crates/modules/charka/SDD.md @@ -143,11 +143,14 @@ del programa COBOL. - **Tolerante**: lo no transpilable (`Stmt::Unknown`, dato sin resolver, `**`) se emite como comentario `// charka:` — el código generado siempre compila. +- **Tablas** (`OCCURS n`): un campo `OCCURS` se emite como `Vec` + / `Vec`; una referencia con subíndice `ELEM(I)` indexa el + vector (subíndice 1-based de COBOL → 0-based de Rust). - Verificado de punta a punta: un programa COBOL de demostración transpila a Rust que compila contra `charka-runtime` y produce la salida correcta. - Fuera de alcance v1: grupos como campo propio, `REDEFINES`, - `OCCURS`/tablas, `PERFORM ... THRU` como rango, E/S de ficheros. + `OCCURS` de grupo, `PERFORM ... THRU` como rango, E/S de ficheros. ## charka-shadow @@ -162,15 +165,17 @@ que corre el `Ir` directamente sobre `charka-runtime`, sin compilar. divergieran, eso delataría un bug. - Tope de pasos: un bucle que no termina se corta con `Halt::StepLimit` en vez de colgarse. -- La referencia v1 es el **corpus** (`corpus/`): 7 programas COBOL de +- La referencia v1 es el **corpus** (`corpus/`): programas COBOL de complejidad graduada con sus salidas esperadas verificadas a mano. Un modo futuro, con GnuCOBOL, diferenciará contra el compilador real. +- En el intérprete todo campo es un vector — un escalar es de longitud + 1, una tabla `OCCURS n` de longitud `n`. ## El corpus -`crates/modules/charka/corpus/` — 7 programas COBOL graduados -(`01-hola` … `07-clasificar`), cada uno con su `.expected`. Ejercita -el pipeline completo de punta a punta. Ver su `README.md`. +`crates/modules/charka/corpus/` — 11 programas COBOL graduados +(`01-hola` … `11-tabla`), cada uno con su `.expected`. Ejercita el +pipeline completo de punta a punta. Ver su `README.md`. ## La CLI @@ -192,4 +197,4 @@ concuerdan. Próximo hito mayor: salir del subconjunto COBOL'85 puro hacia CICS, SQL embebido y los dialectos IBM Enterprise; ampliar el codegen -(grupos, `REDEFINES`, `OCCURS`/tablas, E/S de ficheros). +(grupos como campo, `REDEFINES`, `OCCURS` de grupo, E/S de ficheros). diff --git a/crates/modules/charka/charka-codegen/src/expr.rs b/crates/modules/charka/charka-codegen/src/expr.rs index f5cc87b..65c6fc6 100644 --- a/crates/modules/charka/charka-codegen/src/expr.rs +++ b/crates/modules/charka/charka-codegen/src/expr.rs @@ -41,6 +41,33 @@ pub(crate) fn figurative_fill(f: Figurative) -> char { } } +/// La referencia Rust a un campo (un dato escalar `self.x` o un +/// elemento de tabla `self.x[idx]`) y el tipo del campo. `None` si el +/// operando no es una referencia a dato. +pub(crate) fn field_ref(sym: &Symbols, op: &Operand) -> Option<(String, FieldKind)> { + match op { + Operand::Data(name) => sym + .lookup(name) + .map(|f| (format!("self.{}", f.ident), f.kind)), + Operand::Indexed { name, index } => sym.lookup(name).map(|f| { + ( + format!("self.{}[{}]", f.ident, subscript(sym, index)), + f.kind, + ) + }), + _ => None, + } +} + +/// Un subíndice de tabla como expresión `usize`. COBOL es 1-based; +/// Rust 0-based — de ahí el `saturating_sub(1)`. +fn subscript(sym: &Symbols, index: &Operand) -> String { + format!( + "(({}).rescale(0, Rounding::Truncate).mantissa() as usize).saturating_sub(1)", + operand_decimal(sym, index) + ) +} + /// Un operando como expresión de tipo `Decimal`. pub(crate) fn operand_decimal(sym: &Symbols, op: &Operand) -> String { match op { @@ -50,15 +77,12 @@ pub(crate) fn operand_decimal(sym: &Symbols, op: &Operand) -> String { rust_str(s) ), Operand::Figurative(_) => "Decimal::zero()".to_string(), - Operand::Data(name) => match sym.lookup(name) { - Some(f) => match f.kind { - FieldKind::Num { .. } => format!("self.{}.value()", f.ident), - FieldKind::Text { .. } => format!( - "Decimal::parse(self.{}.display().trim()).unwrap_or_else(|_| Decimal::zero())", - f.ident - ), - }, - None => format!("Decimal::zero() /* charka: dato no resuelto {name} */"), + Operand::Data(_) | Operand::Indexed { .. } => match field_ref(sym, op) { + Some((lref, FieldKind::Num { .. })) => format!("{lref}.value()"), + Some((lref, FieldKind::Text { .. })) => format!( + "Decimal::parse({lref}.display().trim()).unwrap_or_else(|_| Decimal::zero())" + ), + None => "Decimal::zero() /* charka: dato no resuelto */".to_string(), }, } } @@ -69,9 +93,9 @@ pub(crate) fn operand_str(sym: &Symbols, op: &Operand) -> String { Operand::Str(s) => rust_str(s), Operand::Num(n) => rust_str(n), Operand::Figurative(f) => rust_str(figurative_text(*f)), - Operand::Data(name) => match sym.lookup(name) { - Some(f) => format!("self.{}.display().as_str()", f.ident), - None => format!("\"\" /* charka: dato no resuelto {name} */"), + Operand::Data(_) | Operand::Indexed { .. } => match field_ref(sym, op) { + Some((lref, _)) => format!("{lref}.display().as_str()"), + None => "\"\" /* charka: dato no resuelto */".to_string(), }, } } @@ -82,9 +106,9 @@ pub(crate) fn operand_display(sym: &Symbols, op: &Operand) -> String { Operand::Str(s) => rust_str(s), Operand::Num(n) => rust_str(n), Operand::Figurative(f) => rust_str(figurative_text(*f)), - Operand::Data(name) => match sym.lookup(name) { - Some(f) => format!("self.{}.display()", f.ident), - None => format!("\"\" /* charka: dato no resuelto {name} */"), + Operand::Data(_) | Operand::Indexed { .. } => match field_ref(sym, op) { + Some((lref, _)) => format!("{lref}.display()"), + None => "\"\" /* charka: dato no resuelto */".to_string(), }, } } @@ -171,8 +195,8 @@ fn emit_compare(sym: &Symbols, lhs: &Operand, op: CmpOp, rhs: &Operand) -> Strin fn is_text_operand(sym: &Symbols, op: &Operand) -> bool { match op { Operand::Str(_) => true, - Operand::Data(name) => matches!( - sym.lookup(name).map(|f| &f.kind), + Operand::Data(_) | Operand::Indexed { .. } => matches!( + field_ref(sym, op).map(|(_, k)| k), Some(FieldKind::Text { .. }) ), _ => false, diff --git a/crates/modules/charka/charka-codegen/src/lib.rs b/crates/modules/charka/charka-codegen/src/lib.rs index dfbd509..18b38d7 100644 --- a/crates/modules/charka/charka-codegen/src/lib.rs +++ b/crates/modules/charka/charka-codegen/src/lib.rs @@ -73,10 +73,14 @@ fn emit_struct(em: &mut Emitter, sym: &Symbols) { em.line("struct Program {"); em.indent(); for f in &sym.fields { - let ty = match f.kind { + let elem = match f.kind { FieldKind::Num { .. } => "Num", FieldKind::Text { .. } => "Text", }; + let ty = match f.occurs { + None => elem.to_string(), + Some(_) => format!("Vec<{elem}>"), + }; em.line(&format!("{}: {ty},", f.ident)); } em.dedent(); @@ -143,9 +147,10 @@ fn emit_main(em: &mut Emitter) { } /// El inicializador de un campo, a partir de su `VALUE` ya -/// normalizado por `charka-ir`. +/// normalizado por `charka-ir`. Una tabla (`OCCURS n`) se inicializa +/// como un `Vec` de `n` copias del valor inicial. fn field_init(f: &Field) -> String { - match &f.kind { + let scalar = match &f.kind { FieldKind::Num { int, frac, signed } => format!( "Num::with_value(Picture::new({int}, {frac}, {signed}), {})", rust_str(&f.init) @@ -153,6 +158,10 @@ fn field_init(f: &Field) -> String { FieldKind::Text { len } => { format!("Text::with_value({len}, {})", rust_str(&f.init)) } + }; + match f.occurs { + None => scalar, + Some(n) => format!("vec![{scalar}; {n}]"), } } @@ -342,6 +351,22 @@ mod tests { assert!(out.contains("cobol_text_cmp(self.ws_flag.display().as_str(), \"Y\").is_eq()")); } + #[test] + fn occurs_emits_a_vec_field_and_indexed_access() { + let out = gen("DATA DIVISION.\n\ + WORKING-STORAGE SECTION.\n\ + 01 WS-T.\n\ + 05 WS-E PIC 9(3) OCCURS 4 TIMES.\n\ + 01 WS-I PIC 9(1).\n\ + PROCEDURE DIVISION.\n\ + MAIN.\n\ + MOVE 7 TO WS-E(WS-I).\n"); + assert!(out.contains("ws_e: Vec,")); + assert!(out.contains("; 4]")); + assert!(out.contains("self.ws_e[")); + assert!(out.contains(".saturating_sub(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 935e1d2..e6543fe 100644 --- a/crates/modules/charka/charka-codegen/src/stmt.rs +++ b/crates/modules/charka/charka-codegen/src/stmt.rs @@ -5,7 +5,7 @@ use charka_ir::{CmpOp, Cond, Operand, Perform, PerformControl, PerformTarget, St use crate::emit::Emitter; use crate::expr::{ - emit_cond, emit_expr, figurative_fill, operand_decimal, operand_display, operand_str, + emit_cond, emit_expr, field_ref, figurative_fill, operand_decimal, operand_display, operand_str, }; use crate::sym::{paragraph_method, FieldKind, Symbols}; @@ -14,10 +14,8 @@ pub(crate) fn emit_stmt(em: &mut Emitter, sym: &Symbols, stmt: &Stmt) { match stmt { Stmt::Move { from, to } => emit_move(em, sym, from, to), Stmt::Display { items } => emit_display(em, sym, items), - Stmt::Accept { into } => { - em.line(&format!( - "// charka: ACCEPT {into} — entrada interactiva no soportada en v1" - )); + Stmt::Accept { .. } => { + em.line("// charka: ACCEPT — entrada interactiva no soportada en v1"); } Stmt::Compute { targets, @@ -101,53 +99,35 @@ fn emit_block(em: &mut Emitter, sym: &Symbols, stmts: &[Stmt]) { } } -/// Almacena un valor `Decimal` (texto de expresión) en un campo. -fn emit_store(em: &mut Emitter, sym: &Symbols, name: &str, value: &str, rounded: bool) { - match sym.lookup(name) { - Some(f) => match f.kind { - FieldKind::Num { .. } => { - let method = if rounded { "store_rounded" } else { "store" }; - em.line(&format!("self.{}.{method}({value});", f.ident)); - } - FieldKind::Text { .. } => { - em.line(&format!( - "self.{}.store(({value}).to_string().as_str());", - f.ident - )); - } - }, - None => em.line(&format!("// charka: destino no resuelto — {name}")), +/// Almacena un valor `Decimal` (texto de expresión) en un destino — +/// un dato escalar o un elemento de tabla. +fn emit_store(em: &mut Emitter, sym: &Symbols, target: &Operand, value: &str, rounded: bool) { + match field_ref(sym, target) { + Some((lref, FieldKind::Num { .. })) => { + let method = if rounded { "store_rounded" } else { "store" }; + em.line(&format!("{lref}.{method}({value});")); + } + Some((lref, FieldKind::Text { .. })) => { + em.line(&format!("{lref}.store(({value}).to_string().as_str());")); + } + None => em.line("// charka: destino no resuelto"), } } -fn emit_move(em: &mut Emitter, sym: &Symbols, from: &Operand, to: &[String]) { +fn emit_move(em: &mut Emitter, sym: &Symbols, from: &Operand, to: &[Operand]) { for t in to { - match sym.lookup(t) { - Some(f) => match f.kind { - FieldKind::Num { .. } => { - em.line(&format!( - "self.{}.store({});", - f.ident, - operand_decimal(sym, from) - )); + match field_ref(sym, t) { + Some((lref, FieldKind::Num { .. })) => { + em.line(&format!("{lref}.store({});", operand_decimal(sym, from))); + } + Some((lref, FieldKind::Text { .. })) => { + if let Operand::Figurative(fig) = from { + em.line(&format!("{lref}.fill('{}');", figurative_fill(*fig))); + } else { + em.line(&format!("{lref}.store({});", operand_str(sym, from))); } - FieldKind::Text { .. } => { - if let Operand::Figurative(fig) = from { - em.line(&format!( - "self.{}.fill('{}');", - f.ident, - figurative_fill(*fig) - )); - } else { - em.line(&format!( - "self.{}.store({});", - f.ident, - operand_str(sym, from) - )); - } - } - }, - None => em.line(&format!("// charka: destino MOVE no resuelto — {t}")), + } + None => em.line("// charka: destino MOVE no resuelto"), } } } @@ -182,17 +162,14 @@ fn emit_add( em: &mut Emitter, sym: &Symbols, addends: &[Operand], - to: &[String], - giving: &[String], + to: &[Operand], + giving: &[Operand], rounded: bool, ) { let sum = fold_sum(sym, addends); if !giving.is_empty() { let base = match to.first() { - Some(first) => format!( - "({sum}).add(&({}))", - operand_decimal(sym, &Operand::Data(first.clone())) - ), + Some(first) => format!("({sum}).add(&({}))", operand_decimal(sym, first)), None => sum, }; for g in giving { @@ -209,15 +186,15 @@ fn emit_subtract( em: &mut Emitter, sym: &Symbols, amounts: &[Operand], - from: &[String], - giving: &[String], + from: &[Operand], + giving: &[Operand], rounded: bool, ) { let sum = fold_sum(sym, amounts); if !giving.is_empty() { let minuend = from .first() - .map(|f| operand_decimal(sym, &Operand::Data(f.clone()))) + .map(|f| operand_decimal(sym, f)) .unwrap_or_else(|| "Decimal::zero()".to_string()); let value = format!("({minuend}).sub(&({sum}))"); for g in giving { @@ -235,20 +212,18 @@ fn emit_multiply( sym: &Symbols, left: &Operand, by: &Operand, - giving: &[String], + giving: &[Operand], rounded: bool, ) { let l = operand_decimal(sym, left); - if !giving.is_empty() { + if giving.is_empty() { + // `MULTIPLY a BY b` sin GIVING: b queda con a*b. + emit_inplace(em, sym, by, "mul", &l, rounded); + } else { let value = format!("({l}).mul(&({}))", operand_decimal(sym, by)); for g in giving { emit_store(em, sym, g, &value, rounded); } - } else if let Operand::Data(name) = by { - // `MULTIPLY a BY b` sin GIVING: b queda con a*b. - emit_inplace(em, sym, name, "mul", &l, rounded); - } else { - em.line("// charka: MULTIPLY sin destino claro"); } } @@ -258,7 +233,7 @@ fn emit_divide( left: &Operand, right: &Operand, by_form: bool, - giving: &[String], + giving: &[Operand], rounded: bool, ) { // `a BY b` → a/b; `a INTO b` → b/a. @@ -267,47 +242,46 @@ fn emit_divide( } else { (operand_decimal(sym, right), operand_decimal(sym, left)) }; - if !giving.is_empty() { + let div = |scale: u8| { + format!( + "({num}).div(&({den}), {scale}, Rounding::Truncate).unwrap_or_else(|_| Decimal::zero())" + ) + }; + if giving.is_empty() { + // `DIVIDE a INTO b` sin GIVING: b queda con b/a. + let value = div(target_scale(sym, right)); + emit_store(em, sym, right, &value, rounded); + } else { for g in giving { - let value = format!( - "({num}).div(&({den}), {}, Rounding::Truncate).unwrap_or_else(|_| Decimal::zero())", - target_scale(sym, g) - ); + let value = div(target_scale(sym, g)); emit_store(em, sym, g, &value, rounded); } - } else if let Operand::Data(name) = right { - // `DIVIDE a INTO b` sin GIVING: b queda con b/a. - let value = format!( - "({num}).div(&({den}), {}, Rounding::Truncate).unwrap_or_else(|_| Decimal::zero())", - target_scale(sym, name) - ); - emit_store(em, sym, name, &value, rounded); - } else { - em.line("// charka: DIVIDE sin destino claro"); } } -/// Emite una operación aritmética en el lugar: `t = t rhs`. -fn emit_inplace(em: &mut Emitter, sym: &Symbols, name: &str, op: &str, rhs: &str, rounded: bool) { - match sym.lookup(name) { - Some(f) if matches!(f.kind, FieldKind::Num { .. }) => { +/// Emite una operación aritmética en el lugar: `target = target rhs`. +fn emit_inplace( + em: &mut Emitter, + sym: &Symbols, + target: &Operand, + op: &str, + rhs: &str, + rounded: bool, +) { + match field_ref(sym, target) { + Some((lref, FieldKind::Num { .. })) => { let method = if rounded { "store_rounded" } else { "store" }; - em.line(&format!( - "self.{0}.{method}(self.{0}.value().{op}(&({rhs})));", - f.ident - )); + em.line(&format!("{lref}.{method}({lref}.value().{op}(&({rhs})));")); } - _ => em.line(&format!( - "// charka: destino aritmético no resuelto — {name}" - )), + _ => em.line("// charka: destino aritmético no resuelto"), } } /// La escala de redondeo de un destino numérico (sus dígitos /// fraccionarios), o 4 por defecto. -fn target_scale(sym: &Symbols, name: &str) -> u8 { - match sym.lookup(name).map(|f| &f.kind) { - Some(FieldKind::Num { frac, .. }) => *frac, +fn target_scale(sym: &Symbols, op: &Operand) -> u8 { + match field_ref(sym, op).map(|(_, k)| k) { + Some(FieldKind::Num { frac, .. }) => frac, _ => 4, } } @@ -438,11 +412,12 @@ fn emit_perform(em: &mut Emitter, sym: &Symbols, p: &Perform) { until, } => { // var = from; mientras no se cumpla `until`: cuerpo; var += by. - emit_store(em, sym, var, &operand_decimal(sym, from), false); + let var_op = Operand::Data(var.clone()); + emit_store(em, sym, &var_op, &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); + emit_inplace(em, sym, &var_op, "add", &operand_decimal(sym, by), false); em.dedent(); em.line("}"); } diff --git a/crates/modules/charka/charka-codegen/src/sym.rs b/crates/modules/charka/charka-codegen/src/sym.rs index 92dc143..1b319a5 100644 --- a/crates/modules/charka/charka-codegen/src/sym.rs +++ b/crates/modules/charka/charka-codegen/src/sym.rs @@ -20,6 +20,8 @@ pub(crate) struct Field { pub kind: FieldKind, /// Valor inicial normalizado (de la cláusula `VALUE`). pub init: String, + /// Si es una tabla (`OCCURS n`), su número de elementos. + pub occurs: Option, } /// Los campos del programa y sus nombres de condición, indexados. @@ -40,6 +42,7 @@ impl Symbols { ident: sanitize_ident(&f.name), kind: f.kind, init: f.init.clone(), + occurs: f.occurs, }) .collect(); dedup_idents(&mut fields); diff --git a/crates/modules/charka/charka-ir/src/ast.rs b/crates/modules/charka/charka-ir/src/ast.rs index 820d8a2..1d0e106 100644 --- a/crates/modules/charka/charka-ir/src/ast.rs +++ b/crates/modules/charka/charka-ir/src/ast.rs @@ -32,6 +32,9 @@ pub struct Procedure { pub enum Operand { /// Referencia a un dato, por nombre (en mayúsculas). Data(String), + /// Referencia a un elemento de tabla: `name(index)`. El subíndice + /// es 1-based, como en COBOL. + Indexed { name: String, index: Box }, /// Literal numérico (texto, posiblemente con signo). Num(String), /// Literal de texto. @@ -111,36 +114,36 @@ pub enum CmpOp { #[derive(Debug, Clone, PartialEq)] pub enum Stmt { /// `MOVE from TO to...` - Move { from: Operand, to: Vec }, + Move { from: Operand, to: Vec }, /// `DISPLAY items...` Display { items: Vec }, /// `ACCEPT into` - Accept { into: String }, + Accept { into: Operand }, /// `COMPUTE targets... [ROUNDED] = expr` Compute { - targets: Vec, + targets: Vec, rounded: bool, expr: Expr, }, /// `ADD addends... TO to... [GIVING giving...]` Add { addends: Vec, - to: Vec, - giving: Vec, + to: Vec, + giving: Vec, rounded: bool, }, /// `SUBTRACT amounts... FROM from... [GIVING giving...]` Subtract { amounts: Vec, - from: Vec, - giving: Vec, + from: Vec, + giving: Vec, rounded: bool, }, /// `MULTIPLY left BY by [GIVING giving...]` Multiply { left: Operand, by: Operand, - giving: Vec, + giving: Vec, rounded: bool, }, /// `DIVIDE left {BY|INTO} right [GIVING giving...]`. `by_form` es @@ -149,7 +152,7 @@ pub enum Stmt { left: Operand, right: Operand, by_form: bool, - giving: Vec, + giving: Vec, rounded: bool, }, /// `IF cond [THEN] then_branch [ELSE else_branch] [END-IF]` diff --git a/crates/modules/charka/charka-ir/src/cursor.rs b/crates/modules/charka/charka-ir/src/cursor.rs index 51ac5a2..570bb6e 100644 --- a/crates/modules/charka/charka-ir/src/cursor.rs +++ b/crates/modules/charka/charka-ir/src/cursor.rs @@ -103,10 +103,26 @@ pub(crate) fn parse_operand(c: &mut Cursor) -> Operand { num.text }); } - match c.bump() { + let base = match c.bump() { Some(t) => token_to_operand(&t), - None => Operand::Num("0".into()), + None => return Operand::Num("0".into()), + }; + // Subíndice de tabla: `name(index)`. La v1 toma un solo subíndice; + // lo demás dentro del paréntesis se descarta. + if let Operand::Data(name) = &base { + if c.eat_sym("(") { + let index = parse_operand(c); + while !c.at_sym(")") && !c.done() { + c.bump(); + } + c.eat_sym(")"); + return Operand::Indexed { + name: name.clone(), + index: Box::new(index), + }; + } } + base } /// Clasifica un token suelto como operando. diff --git a/crates/modules/charka/charka-ir/src/lib.rs b/crates/modules/charka/charka-ir/src/lib.rs index 53aace2..515ee5b 100644 --- a/crates/modules/charka/charka-ir/src/lib.rs +++ b/crates/modules/charka/charka-ir/src/lib.rs @@ -93,7 +93,7 @@ mod tests { b, vec![Stmt::Move { from: Operand::Num("5".into()), - to: vec!["WS-X".into()], + to: vec![Operand::Data("WS-X".into())], }] ); } @@ -105,11 +105,27 @@ mod tests { b, vec![Stmt::Move { from: Operand::Data("WS-A".into()), - to: vec!["WS-B".into(), "WS-C".into()], + to: vec![Operand::Data("WS-B".into()), Operand::Data("WS-C".into()),], }] ); } + #[test] + fn indexed_operand_parses_subscript() { + // `WS-ELEM(WS-I)` — un destino con subíndice de tabla. + let b = body("MOVE 7 TO WS-ELEM(WS-I)."); + match &b[0] { + Stmt::Move { to, .. } => match &to[0] { + Operand::Indexed { name, index } => { + assert_eq!(name, "WS-ELEM"); + assert_eq!(**index, Operand::Data("WS-I".into())); + } + other => panic!("se esperaba Indexed, vino {other:?}"), + }, + other => panic!("se esperaba MOVE, vino {other:?}"), + } + } + #[test] fn display_items_and_figurative() { let b = body("DISPLAY 'TOTAL: ' WS-TOTAL SPACES."); @@ -130,7 +146,7 @@ mod tests { let b = body("COMPUTE WS-T = WS-A + WS-B * 2."); let expr = match &b[0] { Stmt::Compute { targets, expr, .. } => { - assert_eq!(targets, &vec!["WS-T".to_string()]); + assert_eq!(targets, &vec![Operand::Data("WS-T".into())]); expr.clone() } other => panic!("se esperaba COMPUTE, vino {other:?}"), @@ -162,7 +178,7 @@ mod tests { body("ADD 1 TO WS-CT."), vec![Stmt::Add { addends: vec![Operand::Num("1".into())], - to: vec!["WS-CT".into()], + to: vec![Operand::Data("WS-CT".into())], giving: vec![], rounded: false, }] @@ -172,7 +188,7 @@ mod tests { vec![Stmt::Add { addends: vec![Operand::Data("WS-A".into()), Operand::Data("WS-B".into()),], to: vec![], - giving: vec!["WS-C".into()], + giving: vec![Operand::Data("WS-C".into())], rounded: false, }] ); @@ -184,8 +200,8 @@ mod tests { body("SUBTRACT WS-TAX FROM WS-GROSS GIVING WS-NET."), vec![Stmt::Subtract { amounts: vec![Operand::Data("WS-TAX".into())], - from: vec!["WS-GROSS".into()], - giving: vec!["WS-NET".into()], + from: vec![Operand::Data("WS-GROSS".into())], + giving: vec![Operand::Data("WS-NET".into())], rounded: false, }] ); diff --git a/crates/modules/charka/charka-ir/src/model.rs b/crates/modules/charka/charka-ir/src/model.rs index f92a130..84e61f8 100644 --- a/crates/modules/charka/charka-ir/src/model.rs +++ b/crates/modules/charka/charka-ir/src/model.rs @@ -28,6 +28,9 @@ pub struct Field { pub kind: FieldKind, /// Valor inicial ya normalizado (de la cláusula `VALUE`). pub init: String, + /// Si es una tabla (`OCCURS n`), su número de elementos; `None` + /// para un dato escalar. + pub occurs: Option, } /// Un nombre de condición — un dato de nivel 88. `IF ` equivale @@ -104,6 +107,7 @@ fn walk(items: &[DataItem], model: &mut DataModel) { name: it.name.to_uppercase(), kind, init, + occurs: it.occurs, }); } } diff --git a/crates/modules/charka/charka-ir/src/stmt.rs b/crates/modules/charka/charka-ir/src/stmt.rs index fef6184..fcc7f9b 100644 --- a/crates/modules/charka/charka-ir/src/stmt.rs +++ b/crates/modules/charka/charka-ir/src/stmt.rs @@ -57,10 +57,11 @@ fn parse_one_stmt(c: &mut Cursor, stops: &[&str]) -> Stmt { // ── Listas ──────────────────────────────────────────────────────── -/// Lee una lista de nombres de dato (separados por comas opcionales), -/// hasta una palabra frontera. Consume las apariciones de `ROUNDED`. -fn parse_name_list(c: &mut Cursor, rounded: &mut bool) -> Vec { - let mut names = Vec::new(); +/// Lee una lista de destinos de dato (separados por comas opcionales), +/// hasta una palabra frontera. Cada destino puede llevar subíndice de +/// tabla. Consume las apariciones de `ROUNDED`. +fn parse_targets(c: &mut Cursor, rounded: &mut bool) -> Vec { + let mut targets = Vec::new(); loop { c.eat_sym(","); if c.eat_word("ROUNDED") { @@ -68,14 +69,11 @@ fn parse_name_list(c: &mut Cursor, rounded: &mut bool) -> Vec { continue; } match c.peek_word() { - Some(w) if !is_boundary(&w) => { - c.bump(); - names.push(w); - } + Some(w) if !is_boundary(&w) => targets.push(parse_operand(c)), _ => break, } } - names + targets } /// Lee una lista de operandos hasta una palabra frontera. @@ -137,7 +135,7 @@ fn parse_move(c: &mut Cursor) -> Stmt { let from = parse_operand(c); c.eat_word("TO"); let mut rounded = false; - let to = parse_name_list(c, &mut rounded); + let to = parse_targets(c, &mut rounded); Stmt::Move { from, to } } @@ -150,7 +148,11 @@ fn parse_display(c: &mut Cursor) -> Stmt { fn parse_accept(c: &mut Cursor) -> Stmt { c.bump(); // ACCEPT - let into = parse_one_name(c).unwrap_or_default(); + let into = if c.peek_word().map(|w| !is_boundary(&w)).unwrap_or(false) { + parse_operand(c) + } else { + Operand::Data(String::new()) + }; skip_to_stmt_boundary(c); // p. ej. `FROM DATE` Stmt::Accept { into } } @@ -158,7 +160,7 @@ fn parse_accept(c: &mut Cursor) -> Stmt { fn parse_compute(c: &mut Cursor) -> Stmt { c.bump(); // COMPUTE let mut rounded = false; - let targets = parse_name_list(c, &mut rounded); + let targets = parse_targets(c, &mut rounded); if !c.eat_sym("=") { c.eat_word("EQUAL"); } @@ -180,10 +182,10 @@ fn parse_add(c: &mut Cursor) -> Stmt { let mut to = Vec::new(); let mut giving = Vec::new(); if c.eat_word("TO") { - to = parse_name_list(c, &mut rounded); + to = parse_targets(c, &mut rounded); } if c.eat_word("GIVING") { - giving = parse_name_list(c, &mut rounded); + giving = parse_targets(c, &mut rounded); } c.eat_word("END-ADD"); Stmt::Add { @@ -203,10 +205,10 @@ fn parse_subtract(c: &mut Cursor) -> Stmt { let mut from = Vec::new(); let mut giving = Vec::new(); if c.eat_word("FROM") { - from = parse_name_list(c, &mut rounded); + from = parse_targets(c, &mut rounded); } if c.eat_word("GIVING") { - giving = parse_name_list(c, &mut rounded); + giving = parse_targets(c, &mut rounded); } c.eat_word("END-SUBTRACT"); Stmt::Subtract { @@ -225,7 +227,7 @@ fn parse_multiply(c: &mut Cursor) -> Stmt { let mut rounded = false; let mut giving = Vec::new(); if c.eat_word("GIVING") { - giving = parse_name_list(c, &mut rounded); + giving = parse_targets(c, &mut rounded); } else if c.eat_word("ROUNDED") { rounded = true; } @@ -251,12 +253,12 @@ fn parse_divide(c: &mut Cursor) -> Stmt { let mut rounded = false; let mut giving = Vec::new(); if c.eat_word("GIVING") { - giving = parse_name_list(c, &mut rounded); + giving = parse_targets(c, &mut rounded); } else if c.eat_word("ROUNDED") { rounded = true; } if c.eat_word("REMAINDER") { - let _ = parse_name_list(c, &mut rounded); + let _ = parse_targets(c, &mut rounded); } c.eat_word("END-DIVIDE"); Stmt::Divide { diff --git a/crates/modules/charka/charka-parser/src/lib.rs b/crates/modules/charka/charka-parser/src/lib.rs index 49a63b0..3a22132 100644 --- a/crates/modules/charka/charka-parser/src/lib.rs +++ b/crates/modules/charka/charka-parser/src/lib.rs @@ -53,6 +53,9 @@ pub struct DataItem { /// Cláusula `VALUE`: literal numérico (con signo), constante /// figurativa en mayúsculas, o literal de texto entre comillas. pub value: Option, + /// Cláusula `OCCURS n [TIMES]`: el dato es una tabla de `n` + /// elementos. `None` si es un dato escalar. + pub occurs: Option, /// Ítems subordinados (de nivel numérico mayor). pub children: Vec, } @@ -205,9 +208,24 @@ fn parse_data_entry(level: u8, sent: &[Token]) -> Result { let mut picture = None; let mut value = None; + let mut occurs = None; let mut i = 2; while i < sent.len() { match kw(sent.get(i)).as_deref() { + Some("OCCURS") => { + i += 1; + if let Some(t) = sent.get(i) { + if t.kind == TokenKind::Number { + if occurs.is_none() { + occurs = t.text.parse::().ok(); + } + i += 1; + } + } + if kw(sent.get(i)).as_deref() == Some("TIMES") { + i += 1; + } + } Some("PIC") | Some("PICTURE") => { i += 1; if kw(sent.get(i)).as_deref() == Some("IS") { @@ -239,6 +257,7 @@ fn parse_data_entry(level: u8, sent: &[Token]) -> Result { name, picture, value, + occurs, children: Vec::new(), }) } @@ -605,6 +624,20 @@ mod tests { assert_eq!(p.data[0].children[1].name, "FILLER"); } + #[test] + fn occurs_clause_captured() { + let p = parse_src( + "DATA DIVISION.\n\ + WORKING-STORAGE SECTION.\n\ + 01 WS-TABLA.\n\ + 05 WS-ELEM PIC 9(3) OCCURS 10 TIMES.\n", + ); + let elem = &p.data[0].children[0]; + assert_eq!(elem.name, "WS-ELEM"); + assert_eq!(elem.occurs, Some(10)); + assert_eq!(elem.picture.as_deref(), Some("9(3)")); + } + #[test] fn bad_level_number_is_error() { let toks = lex( diff --git a/crates/modules/charka/charka-shadow/src/field.rs b/crates/modules/charka/charka-shadow/src/field.rs index 745bbd5..3807e9d 100644 --- a/crates/modules/charka/charka-shadow/src/field.rs +++ b/crates/modules/charka/charka-shadow/src/field.rs @@ -6,21 +6,27 @@ use std::collections::HashMap; use charka_ir::{DataModel, FieldKind}; use charka_runtime::{Num, Picture, Text}; -/// Un campo vivo: numérico o alfanumérico. +/// Un campo vivo. Todo campo es un vector: un dato escalar es un +/// vector de un elemento; una tabla (`OCCURS n`) es de `n` elementos. pub(crate) enum Cell { - Num(Num), - Text(Text), + Num(Vec), + Text(Vec), } /// Materializa los campos del modelo en un mapa `nombre → campo`. pub(crate) fn build_fields(model: &DataModel) -> HashMap { let mut map = HashMap::new(); for f in &model.fields { + let n = f.occurs.unwrap_or(1).max(1) as usize; let cell = match f.kind { - FieldKind::Num { int, frac, signed } => { - Cell::Num(Num::with_value(Picture::new(int, frac, signed), &f.init)) - } - FieldKind::Text { len } => Cell::Text(Text::with_value(len, &f.init)), + FieldKind::Num { int, frac, signed } => Cell::Num(vec![ + Num::with_value( + Picture::new(int, frac, signed), + &f.init + ); + n + ]), + FieldKind::Text { len } => Cell::Text(vec![Text::with_value(len, &f.init); n]), }; map.entry(f.name.clone()).or_insert(cell); } diff --git a/crates/modules/charka/charka-shadow/src/interp.rs b/crates/modules/charka/charka-shadow/src/interp.rs index 9927624..2cf170f 100644 --- a/crates/modules/charka/charka-shadow/src/interp.rs +++ b/crates/modules/charka/charka-shadow/src/interp.rs @@ -141,12 +141,12 @@ impl<'a> Machine<'a> { let sum = self.fold_sum(addends); if giving.is_empty() { for t in to { - let cur = self.field_value(t); + let cur = self.eval_decimal(t); self.store(t, cur.add(&sum), *rounded); } } else { let base = match to.first() { - Some(first) => sum.add(&self.field_value(first)), + Some(first) => sum.add(&self.eval_decimal(first)), None => sum, }; for g in giving { @@ -164,13 +164,13 @@ impl<'a> Machine<'a> { let sum = self.fold_sum(amounts); if giving.is_empty() { for t in from { - let cur = self.field_value(t); + let cur = self.eval_decimal(t); self.store(t, cur.sub(&sum), *rounded); } } else { let minuend = from .first() - .map(|f| self.field_value(f)) + .map(|f| self.eval_decimal(f)) .unwrap_or_else(Decimal::zero); let value = minuend.sub(&sum); for g in giving { @@ -187,9 +187,8 @@ impl<'a> Machine<'a> { } => { let value = self.eval_decimal(left).mul(&self.eval_decimal(by)); if giving.is_empty() { - if let Operand::Data(name) = by { - self.store(name, value, *rounded); - } + // `MULTIPLY a BY b` sin GIVING: b queda con a*b. + self.store(by, value, *rounded); } else { for g in giving { self.store(g, value, *rounded); @@ -210,10 +209,9 @@ impl<'a> Machine<'a> { (self.eval_decimal(right), self.eval_decimal(left)) }; if giving.is_empty() { - if let Operand::Data(name) = right { - let v = divide(num, den, self.target_scale(name)); - self.store(name, v, *rounded); - } + // `DIVIDE a INTO b` sin GIVING: b queda con b/a. + let v = divide(num, den, self.target_scale(right)); + self.store(right, v, *rounded); } else { for g in giving { let v = divide(num, den, self.target_scale(g)); @@ -296,8 +294,9 @@ impl<'a> Machine<'a> { by, until, } => { + let var_op = Operand::Data(var.clone()); let start = self.eval_decimal(from); - self.store(var, start, false); + self.store(&var_op, start, false); loop { if self.tick() { return Flow::Stop; @@ -308,8 +307,8 @@ impl<'a> Machine<'a> { 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); + let next = self.eval_decimal(&var_op).add(&self.eval_decimal(by)); + self.store(&var_op, next, false); } } } @@ -339,44 +338,74 @@ impl<'a> Machine<'a> { } } - /// `MOVE from` a un solo campo destino. - fn do_move(&mut self, from: &Operand, target: &str) { - let key = target.to_uppercase(); - match self.fields.get(&key) { - Some(Cell::Num(_)) => { - let v = self.eval_decimal(from); - if let Some(Cell::Num(n)) = self.fields.get_mut(&key) { - n.store(v); - } + /// Resuelve una referencia a dato (escalar o elemento de tabla) a + /// su nombre y un índice 0-based. `None` si no es una referencia. + fn resolve(&self, op: &Operand) -> Option<(String, usize)> { + match op { + Operand::Data(name) => Some((name.to_uppercase(), 0)), + Operand::Indexed { name, index } => { + // El subíndice de COBOL es 1-based. + let i = self + .eval_decimal(index) + .rescale(0, Rounding::Truncate) + .mantissa(); + let idx = if i < 1 { 0 } else { (i - 1) as usize }; + Some((name.to_uppercase(), idx)) } - Some(Cell::Text(_)) => { - if let Operand::Figurative(fig) = from { - let ch = figurative_fill(*fig); - if let Some(Cell::Text(t)) = self.fields.get_mut(&key) { - t.fill(ch); - } - } else { - let s = self.eval_text(from); - if let Some(Cell::Text(t)) = self.fields.get_mut(&key) { - t.store(&s); - } - } - } - None => {} + _ => None, } } - /// Almacena un valor en un campo, conformándolo a su tipo. - fn store(&mut self, name: &str, value: Decimal, rounded: bool) { - match self.fields.get_mut(&name.to_uppercase()) { - Some(Cell::Num(n)) => { - if rounded { - n.store_rounded(value); - } else { - n.store(value); + /// `MOVE from` a un solo destino (escalar o elemento de tabla). + fn do_move(&mut self, from: &Operand, target: &Operand) { + let Some((key, idx)) = self.resolve(target) else { + return; + }; + let is_num = matches!(self.fields.get(&key), Some(Cell::Num(_))); + if is_num { + let v = self.eval_decimal(from); + if let Some(Cell::Num(arr)) = self.fields.get_mut(&key) { + if let Some(n) = arr.get_mut(idx) { + n.store(v); + } + } + } else if let Operand::Figurative(fig) = from { + let ch = figurative_fill(*fig); + if let Some(Cell::Text(arr)) = self.fields.get_mut(&key) { + if let Some(t) = arr.get_mut(idx) { + t.fill(ch); + } + } + } else { + let s = self.eval_text(from); + if let Some(Cell::Text(arr)) = self.fields.get_mut(&key) { + if let Some(t) = arr.get_mut(idx) { + t.store(&s); + } + } + } + } + + /// Almacena un valor en un destino, conformándolo a su tipo. + fn store(&mut self, target: &Operand, value: Decimal, rounded: bool) { + let Some((key, idx)) = self.resolve(target) else { + return; + }; + match self.fields.get_mut(&key) { + Some(Cell::Num(arr)) => { + if let Some(n) = arr.get_mut(idx) { + if rounded { + n.store_rounded(value); + } else { + n.store(value); + } + } + } + Some(Cell::Text(arr)) => { + if let Some(t) = arr.get_mut(idx) { + t.store(&value.to_string()); } } - Some(Cell::Text(t)) => t.store(&value.to_string()), None => {} } } @@ -388,13 +417,22 @@ impl<'a> Machine<'a> { Operand::Num(n) => Decimal::parse(n).unwrap_or_else(|_| Decimal::zero()), Operand::Str(s) => Decimal::parse(s).unwrap_or_else(|_| Decimal::zero()), Operand::Figurative(_) => Decimal::zero(), - Operand::Data(name) => match self.fields.get(&name.to_uppercase()) { - Some(Cell::Num(n)) => n.value(), - Some(Cell::Text(t)) => { - Decimal::parse(t.as_str().trim()).unwrap_or_else(|_| Decimal::zero()) + Operand::Data(_) | Operand::Indexed { .. } => { + let Some((key, idx)) = self.resolve(op) else { + return Decimal::zero(); + }; + match self.fields.get(&key) { + Some(Cell::Num(arr)) => arr + .get(idx) + .map(|n| n.value()) + .unwrap_or_else(Decimal::zero), + Some(Cell::Text(arr)) => arr + .get(idx) + .and_then(|t| Decimal::parse(t.as_str().trim()).ok()) + .unwrap_or_else(Decimal::zero), + None => Decimal::zero(), } - None => Decimal::zero(), - }, + } } } @@ -403,11 +441,16 @@ impl<'a> Machine<'a> { Operand::Str(s) => s.clone(), Operand::Num(n) => n.clone(), Operand::Figurative(f) => figurative_text(*f).to_string(), - Operand::Data(name) => match self.fields.get(&name.to_uppercase()) { - Some(Cell::Num(n)) => n.display(), - Some(Cell::Text(t)) => t.display(), - None => String::new(), - }, + Operand::Data(_) | Operand::Indexed { .. } => { + let Some((key, idx)) = self.resolve(op) else { + return String::new(); + }; + match self.fields.get(&key) { + Some(Cell::Num(arr)) => arr.get(idx).map(|n| n.display()).unwrap_or_default(), + Some(Cell::Text(arr)) => arr.get(idx).map(|t| t.display()).unwrap_or_default(), + None => String::new(), + } + } } } @@ -461,9 +504,10 @@ impl<'a> Machine<'a> { fn is_text(&self, op: &Operand) -> bool { match op { Operand::Str(_) => true, - Operand::Data(name) => { - matches!(self.fields.get(&name.to_uppercase()), Some(Cell::Text(_))) - } + Operand::Data(_) | Operand::Indexed { .. } => match self.resolve(op) { + Some((key, _)) => matches!(self.fields.get(&key), Some(Cell::Text(_))), + None => false, + }, _ => false, } } @@ -486,23 +530,16 @@ impl<'a> Machine<'a> { acc } - /// El valor actual de un campo por nombre. - fn field_value(&self, name: &str) -> Decimal { - match self.fields.get(&name.to_uppercase()) { - Some(Cell::Num(n)) => n.value(), - Some(Cell::Text(t)) => { - Decimal::parse(t.as_str().trim()).unwrap_or_else(|_| Decimal::zero()) + /// Los dígitos fraccionarios de un destino numérico. + fn target_scale(&self, op: &Operand) -> u8 { + if let Some((key, idx)) = self.resolve(op) { + if let Some(Cell::Num(arr)) = self.fields.get(&key) { + if let Some(n) = arr.get(idx) { + return n.picture().fraction_digits; + } } - None => Decimal::zero(), - } - } - - /// Los dígitos fraccionarios de un campo numérico destino. - fn target_scale(&self, name: &str) -> u8 { - match self.fields.get(&name.to_uppercase()) { - Some(Cell::Num(n)) => n.picture().fraction_digits, - _ => 4, } + 4 } /// El número de repeticiones de un `PERFORM ... TIMES`. diff --git a/crates/modules/charka/charka-shadow/src/lib.rs b/crates/modules/charka/charka-shadow/src/lib.rs index 4f520ff..38e50a3 100644 --- a/crates/modules/charka/charka-shadow/src/lib.rs +++ b/crates/modules/charka/charka-shadow/src/lib.rs @@ -119,6 +119,7 @@ mod tests { corpus_test!(corpus_08_varying, "08-varying"); corpus_test!(corpus_09_evaluar, "09-evaluar"); corpus_test!(corpus_10_condicion, "10-condicion"); + corpus_test!(corpus_11_tabla, "11-tabla"); #[test] fn empty_source_runs_clean() { diff --git a/crates/modules/charka/corpus/11-tabla.cob b/crates/modules/charka/corpus/11-tabla.cob new file mode 100644 index 0000000..b7e027d --- /dev/null +++ b/crates/modules/charka/corpus/11-tabla.cob @@ -0,0 +1,20 @@ +* corpus charka — nivel 6: tablas (OCCURS) y subíndices +IDENTIFICATION DIVISION. +PROGRAM-ID. TABLAS. +DATA DIVISION. +WORKING-STORAGE SECTION. +01 WS-TABLA. + 05 WS-ELEM PIC 9(4) OCCURS 5 TIMES. +01 WS-I PIC 9(2) VALUE 0. +01 WS-TOTAL PIC 9(6) VALUE 0. +PROCEDURE DIVISION. +MAIN. + PERFORM VARYING WS-I FROM 1 BY 1 UNTIL WS-I > 5 + COMPUTE WS-ELEM(WS-I) = WS-I * WS-I + END-PERFORM. + PERFORM VARYING WS-I FROM 1 BY 1 UNTIL WS-I > 5 + ADD WS-ELEM(WS-I) TO WS-TOTAL + DISPLAY 'ELEM ' WS-I ' = ' WS-ELEM(WS-I) + END-PERFORM. + DISPLAY 'SUMA DE CUADRADOS = ' WS-TOTAL. + STOP RUN. diff --git a/crates/modules/charka/corpus/11-tabla.expected b/crates/modules/charka/corpus/11-tabla.expected new file mode 100644 index 0000000..d896ed8 --- /dev/null +++ b/crates/modules/charka/corpus/11-tabla.expected @@ -0,0 +1,6 @@ +ELEM 01 = 0001 +ELEM 02 = 0004 +ELEM 03 = 0009 +ELEM 04 = 0016 +ELEM 05 = 0025 +SUMA DE CUADRADOS = 000055 diff --git a/crates/modules/charka/corpus/README.md b/crates/modules/charka/corpus/README.md index b2fed23..acaf473 100644 --- a/crates/modules/charka/corpus/README.md +++ b/crates/modules/charka/corpus/README.md @@ -19,6 +19,7 @@ salida correcta, una línea por `DISPLAY`. | `08-varying` | 4 | `PERFORM VARYING` — el bucle con variable de control| | `09-evaluar` | 5 | `EVALUATE` — el `case` de COBOL, `WHEN` / `OTHER` | | `10-condicion` | 5 | nombres de condición (nivel 88) en `IF` | +| `11-tabla` | 6 | tablas (`OCCURS`) y referencias con subíndice | ## Formato diff --git a/docs/changelog/charka.md b/docs/changelog/charka.md index fa8fb0a..612a5be 100644 --- a/docs/changelog/charka.md +++ b/docs/changelog/charka.md @@ -3,6 +3,30 @@ 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): OCCURS — tablas y referencias con subíndice + +Los arrays de COBOL, que antes el transpilador descartaba en silencio. +Atraviesa el pipeline entero. + +- Parser: la cláusula `OCCURS n [TIMES]` se captura en `DataItem`. +- IR: `Operand::Indexed { name, index }` — una referencia `ELEM(I)`. + El subíndice es 1-based, como COBOL. Los destinos de los statements + pasan de `Vec` a `Vec`, así que se puede escribir a + un elemento de tabla (`MOVE x TO ELEM(I)`, `COMPUTE ELEM(I) = ...`). + `Field` del modelo gana `occurs: Option`. +- Codegen: un campo `OCCURS` se emite como `Vec`/`Vec`, + inicializado con `vec![..; n]`; una referencia con subíndice indexa + el vector (1-based → 0-based). +- Shadow: en el intérprete todo campo es un vector — un escalar es de + longitud 1, una tabla de `n`; las referencias se resuelven a + `(nombre, índice)`. +- Corpus: programa nuevo `11-tabla` (llena una tabla con cuadrados y + los suma). Verificado: el intérprete sombra y el crate compilado por + `scaffold` dan ambos `SUMA DE CUADRADOS = 000055`. +- Alcance v1: `OCCURS` en datos elementales, una dimensión, subíndice + de un solo operando. Fuera: `OCCURS` de grupo, multidimensional, + `OCCURS DEPENDING ON`. + ### feat(charka): nombres de condición (nivel 88) + modelo de datos compartido `IF ES-VALIDO` — los nombres de condición de COBOL, que antes el