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
+12 -9
View File
@@ -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]`
+18 -2
View File
@@ -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.
+23 -7
View File
@@ -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,
}]
);
@@ -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,
});
}
}
+21 -19
View File
@@ -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 {