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:
@@ -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]`
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user