gioser

tawasuyu/brahman

Rust 260TOML 84JSON 14HTML 13386 Sutichaykuna

feat(charka): OCCURS — tablas y referencias con subíndice

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<String> a Vec<Operand>, así que se puede escribir a un elemento
  de tabla (MOVE x TO ELEM(I), COMPUTE ELEM(I) = ...). model::Field
  gana occurs: Option<u32>.
- Codegen: un campo OCCURS se emite como Vec<Num>/Vec<Text>,
  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 <noreply@anthropic.com>

3902763daa94397756ff2394116daef97814090a · ruwaq sergio · (5 killa ñawpaq) · tayta 28ee1ae26 · kay commitpi sach'ata qhaway

Semántico sayk'uy · 56

crates/modules/charka/charka-codegen/src/expr.rs

crates/modules/charka/charka-codegen/src/lib.rs

crates/modules/charka/charka-codegen/src/stmt.rs

crates/modules/charka/charka-codegen/src/sym.rs

  • silq'uyninpi t'ikrakurqan def Field
  • ukhunpi t'ikrakurqan def build

crates/modules/charka/charka-ir/src/ast.rs

  • silq'uyninpi t'ikrakurqan enum Operand pub
  • silq'uyninpi t'ikrakurqan enum Stmt pub

crates/modules/charka/charka-ir/src/cursor.rs

crates/modules/charka/charka-ir/src/lib.rs

crates/modules/charka/charka-ir/src/model.rs

  • silq'uyninpi t'ikrakurqan struct Field pub
  • ukhunpi t'ikrakurqan fn walk

crates/modules/charka/charka-ir/src/stmt.rs

crates/modules/charka/charka-parser/src/lib.rs

crates/modules/charka/charka-shadow/src/field.rs

crates/modules/charka/charka-shadow/src/interp.rs

18 qillqakuna · +436 −235

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<Num>`
+  / `Vec<Text>`; 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<Num>,"));
+        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)
-                    ));
-                }
-                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)
-                        ));
-                    }
+        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)));
                 }
-            },
-            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 <op> 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 <op> 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<u32>,
 }
 
 /// 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<Operand> },
     /// 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<String> },
+    Move { from: Operand, to: Vec<Operand> },
     /// `DISPLAY items...`
     Display { items: Vec<Operand> },
     /// `ACCEPT into`
-    Accept { into: String },
+    Accept { into: Operand },
     /// `COMPUTE targets... [ROUNDED] = expr`
     Compute {
-        targets: Vec<String>,
+        targets: Vec<Operand>,
         rounded: bool,
         expr: Expr,
     },
     /// `ADD addends... TO to... [GIVING giving...]`
     Add {
         addends: Vec<Operand>,
-        to: Vec<String>,
-        giving: Vec<String>,
+        to: Vec<Operand>,
+        giving: Vec<Operand>,
         rounded: bool,
     },
     /// `SUBTRACT amounts... FROM from... [GIVING giving...]`
     Subtract {
         amounts: Vec<Operand>,
-        from: Vec<String>,
-        giving: Vec<String>,
+        from: Vec<Operand>,
+        giving: Vec<Operand>,
         rounded: bool,
     },
     /// `MULTIPLY left BY by [GIVING giving...]`
     Multiply {
         left: Operand,
         by: Operand,
-        giving: Vec<String>,
+        giving: Vec<Operand>,
         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<String>,
+        giving: Vec<Operand>,
         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<u32>,
 }
 
 /// Un nombre de condición — un dato de nivel 88. `IF <name>` 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<String> {
-    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<Operand> {
+    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<String> {
             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<String>,
+    /// Cláusula `OCCURS n [TIMES]`: el dato es una tabla de `n`
+    /// elementos. `None` si es un dato escalar.
+    pub occurs: Option<u32>,
     /// Ítems subordinados (de nivel numérico mayor).
     pub children: Vec<DataItem>,
 }
@@ -205,9 +208,24 @@ fn parse_data_entry(level: u8, sent: &[Token]) -> Result<DataItem, ParseError> {
 
     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::<u32>().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<DataItem, ParseError> {
         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<Num>),
+    Text(Vec<Text>),
 }
 
 /// Materializa los campos del modelo en un mapa `nombre → campo`.
 pub(crate) fn build_fields(model: &DataModel) -> HashMap<String, Cell> {
     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) {
+    /// 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))
+            }
+            _ => None,
+        }
+    }
+
+    /// `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);
                 }
             }
-            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);
-                    }
+        } 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);
                 }
             }
-            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);
+    /// 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<String>` a `Vec<Operand>`, así que se puede escribir a
+  un elemento de tabla (`MOVE x TO ELEM(I)`, `COMPUTE ELEM(I) = ...`).
+  `Field` del modelo gana `occurs: Option<u32>`.
+- Codegen: un campo `OCCURS` se emite como `Vec<Num>`/`Vec<Text>`,
+  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