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>
This commit is contained in:
sergio
2026-05-21 22:03:48 +00:00
parent 28ee1ae260
commit 3902763daa
18 changed files with 440 additions and 239 deletions
@@ -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(