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
@@ -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,