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
@@ -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);
}
+112 -75
View File
@@ -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`.
@@ -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() {