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,
@@ -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("");
@@ -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)
));
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)));
}
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)
));
}
}
},
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("}");
}
@@ -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);