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:
@@ -143,11 +143,14 @@ del programa COBOL.
|
|||||||
- **Tolerante**: lo no transpilable (`Stmt::Unknown`, dato sin
|
- **Tolerante**: lo no transpilable (`Stmt::Unknown`, dato sin
|
||||||
resolver, `**`) se emite como comentario `// charka:` — el código
|
resolver, `**`) se emite como comentario `// charka:` — el código
|
||||||
generado siempre compila.
|
generado siempre compila.
|
||||||
|
- **Tablas** (`OCCURS n`): un campo `OCCURS` se emite como `Vec<Num>`
|
||||||
|
/ `Vec<Text>`; una referencia con subíndice `ELEM(I)` indexa el
|
||||||
|
vector (subíndice 1-based de COBOL → 0-based de Rust).
|
||||||
- Verificado de punta a punta: un programa COBOL de demostración
|
- Verificado de punta a punta: un programa COBOL de demostración
|
||||||
transpila a Rust que compila contra `charka-runtime` y produce la
|
transpila a Rust que compila contra `charka-runtime` y produce la
|
||||||
salida correcta.
|
salida correcta.
|
||||||
- Fuera de alcance v1: grupos como campo propio, `REDEFINES`,
|
- Fuera de alcance v1: grupos como campo propio, `REDEFINES`,
|
||||||
`OCCURS`/tablas, `PERFORM ... THRU` como rango, E/S de ficheros.
|
`OCCURS` de grupo, `PERFORM ... THRU` como rango, E/S de ficheros.
|
||||||
|
|
||||||
## charka-shadow
|
## charka-shadow
|
||||||
|
|
||||||
@@ -162,15 +165,17 @@ que corre el `Ir` directamente sobre `charka-runtime`, sin compilar.
|
|||||||
divergieran, eso delataría un bug.
|
divergieran, eso delataría un bug.
|
||||||
- Tope de pasos: un bucle que no termina se corta con
|
- Tope de pasos: un bucle que no termina se corta con
|
||||||
`Halt::StepLimit` en vez de colgarse.
|
`Halt::StepLimit` en vez de colgarse.
|
||||||
- La referencia v1 es el **corpus** (`corpus/`): 7 programas COBOL de
|
- La referencia v1 es el **corpus** (`corpus/`): programas COBOL de
|
||||||
complejidad graduada con sus salidas esperadas verificadas a mano.
|
complejidad graduada con sus salidas esperadas verificadas a mano.
|
||||||
Un modo futuro, con GnuCOBOL, diferenciará contra el compilador real.
|
Un modo futuro, con GnuCOBOL, diferenciará contra el compilador real.
|
||||||
|
- En el intérprete todo campo es un vector — un escalar es de longitud
|
||||||
|
1, una tabla `OCCURS n` de longitud `n`.
|
||||||
|
|
||||||
## El corpus
|
## El corpus
|
||||||
|
|
||||||
`crates/modules/charka/corpus/` — 7 programas COBOL graduados
|
`crates/modules/charka/corpus/` — 11 programas COBOL graduados
|
||||||
(`01-hola` … `07-clasificar`), cada uno con su `.expected`. Ejercita
|
(`01-hola` … `11-tabla`), cada uno con su `.expected`. Ejercita el
|
||||||
el pipeline completo de punta a punta. Ver su `README.md`.
|
pipeline completo de punta a punta. Ver su `README.md`.
|
||||||
|
|
||||||
## La CLI
|
## La CLI
|
||||||
|
|
||||||
@@ -192,4 +197,4 @@ concuerdan.
|
|||||||
|
|
||||||
Próximo hito mayor: salir del subconjunto COBOL'85 puro hacia CICS,
|
Próximo hito mayor: salir del subconjunto COBOL'85 puro hacia CICS,
|
||||||
SQL embebido y los dialectos IBM Enterprise; ampliar el codegen
|
SQL embebido y los dialectos IBM Enterprise; ampliar el codegen
|
||||||
(grupos, `REDEFINES`, `OCCURS`/tablas, E/S de ficheros).
|
(grupos como campo, `REDEFINES`, `OCCURS` de grupo, E/S de ficheros).
|
||||||
|
|||||||
@@ -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`.
|
/// Un operando como expresión de tipo `Decimal`.
|
||||||
pub(crate) fn operand_decimal(sym: &Symbols, op: &Operand) -> String {
|
pub(crate) fn operand_decimal(sym: &Symbols, op: &Operand) -> String {
|
||||||
match op {
|
match op {
|
||||||
@@ -50,15 +77,12 @@ pub(crate) fn operand_decimal(sym: &Symbols, op: &Operand) -> String {
|
|||||||
rust_str(s)
|
rust_str(s)
|
||||||
),
|
),
|
||||||
Operand::Figurative(_) => "Decimal::zero()".to_string(),
|
Operand::Figurative(_) => "Decimal::zero()".to_string(),
|
||||||
Operand::Data(name) => match sym.lookup(name) {
|
Operand::Data(_) | Operand::Indexed { .. } => match field_ref(sym, op) {
|
||||||
Some(f) => match f.kind {
|
Some((lref, FieldKind::Num { .. })) => format!("{lref}.value()"),
|
||||||
FieldKind::Num { .. } => format!("self.{}.value()", f.ident),
|
Some((lref, FieldKind::Text { .. })) => format!(
|
||||||
FieldKind::Text { .. } => format!(
|
"Decimal::parse({lref}.display().trim()).unwrap_or_else(|_| Decimal::zero())"
|
||||||
"Decimal::parse(self.{}.display().trim()).unwrap_or_else(|_| Decimal::zero())",
|
),
|
||||||
f.ident
|
None => "Decimal::zero() /* charka: dato no resuelto */".to_string(),
|
||||||
),
|
|
||||||
},
|
|
||||||
None => format!("Decimal::zero() /* charka: dato no resuelto {name} */"),
|
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -69,9 +93,9 @@ pub(crate) fn operand_str(sym: &Symbols, op: &Operand) -> String {
|
|||||||
Operand::Str(s) => rust_str(s),
|
Operand::Str(s) => rust_str(s),
|
||||||
Operand::Num(n) => rust_str(n),
|
Operand::Num(n) => rust_str(n),
|
||||||
Operand::Figurative(f) => rust_str(figurative_text(*f)),
|
Operand::Figurative(f) => rust_str(figurative_text(*f)),
|
||||||
Operand::Data(name) => match sym.lookup(name) {
|
Operand::Data(_) | Operand::Indexed { .. } => match field_ref(sym, op) {
|
||||||
Some(f) => format!("self.{}.display().as_str()", f.ident),
|
Some((lref, _)) => format!("{lref}.display().as_str()"),
|
||||||
None => format!("\"\" /* charka: dato no resuelto {name} */"),
|
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::Str(s) => rust_str(s),
|
||||||
Operand::Num(n) => rust_str(n),
|
Operand::Num(n) => rust_str(n),
|
||||||
Operand::Figurative(f) => rust_str(figurative_text(*f)),
|
Operand::Figurative(f) => rust_str(figurative_text(*f)),
|
||||||
Operand::Data(name) => match sym.lookup(name) {
|
Operand::Data(_) | Operand::Indexed { .. } => match field_ref(sym, op) {
|
||||||
Some(f) => format!("self.{}.display()", f.ident),
|
Some((lref, _)) => format!("{lref}.display()"),
|
||||||
None => format!("\"\" /* charka: dato no resuelto {name} */"),
|
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 {
|
fn is_text_operand(sym: &Symbols, op: &Operand) -> bool {
|
||||||
match op {
|
match op {
|
||||||
Operand::Str(_) => true,
|
Operand::Str(_) => true,
|
||||||
Operand::Data(name) => matches!(
|
Operand::Data(_) | Operand::Indexed { .. } => matches!(
|
||||||
sym.lookup(name).map(|f| &f.kind),
|
field_ref(sym, op).map(|(_, k)| k),
|
||||||
Some(FieldKind::Text { .. })
|
Some(FieldKind::Text { .. })
|
||||||
),
|
),
|
||||||
_ => false,
|
_ => false,
|
||||||
|
|||||||
@@ -73,10 +73,14 @@ fn emit_struct(em: &mut Emitter, sym: &Symbols) {
|
|||||||
em.line("struct Program {");
|
em.line("struct Program {");
|
||||||
em.indent();
|
em.indent();
|
||||||
for f in &sym.fields {
|
for f in &sym.fields {
|
||||||
let ty = match f.kind {
|
let elem = match f.kind {
|
||||||
FieldKind::Num { .. } => "Num",
|
FieldKind::Num { .. } => "Num",
|
||||||
FieldKind::Text { .. } => "Text",
|
FieldKind::Text { .. } => "Text",
|
||||||
};
|
};
|
||||||
|
let ty = match f.occurs {
|
||||||
|
None => elem.to_string(),
|
||||||
|
Some(_) => format!("Vec<{elem}>"),
|
||||||
|
};
|
||||||
em.line(&format!("{}: {ty},", f.ident));
|
em.line(&format!("{}: {ty},", f.ident));
|
||||||
}
|
}
|
||||||
em.dedent();
|
em.dedent();
|
||||||
@@ -143,9 +147,10 @@ fn emit_main(em: &mut Emitter) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// El inicializador de un campo, a partir de su `VALUE` ya
|
/// 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 {
|
fn field_init(f: &Field) -> String {
|
||||||
match &f.kind {
|
let scalar = match &f.kind {
|
||||||
FieldKind::Num { int, frac, signed } => format!(
|
FieldKind::Num { int, frac, signed } => format!(
|
||||||
"Num::with_value(Picture::new({int}, {frac}, {signed}), {})",
|
"Num::with_value(Picture::new({int}, {frac}, {signed}), {})",
|
||||||
rust_str(&f.init)
|
rust_str(&f.init)
|
||||||
@@ -153,6 +158,10 @@ fn field_init(f: &Field) -> String {
|
|||||||
FieldKind::Text { len } => {
|
FieldKind::Text { len } => {
|
||||||
format!("Text::with_value({len}, {})", rust_str(&f.init))
|
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()"));
|
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]
|
#[test]
|
||||||
fn empty_program_still_compiles_shape() {
|
fn empty_program_still_compiles_shape() {
|
||||||
let out = gen("");
|
let out = gen("");
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ use charka_ir::{CmpOp, Cond, Operand, Perform, PerformControl, PerformTarget, St
|
|||||||
|
|
||||||
use crate::emit::Emitter;
|
use crate::emit::Emitter;
|
||||||
use crate::expr::{
|
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};
|
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 {
|
match stmt {
|
||||||
Stmt::Move { from, to } => emit_move(em, sym, from, to),
|
Stmt::Move { from, to } => emit_move(em, sym, from, to),
|
||||||
Stmt::Display { items } => emit_display(em, sym, items),
|
Stmt::Display { items } => emit_display(em, sym, items),
|
||||||
Stmt::Accept { into } => {
|
Stmt::Accept { .. } => {
|
||||||
em.line(&format!(
|
em.line("// charka: ACCEPT — entrada interactiva no soportada en v1");
|
||||||
"// charka: ACCEPT {into} — entrada interactiva no soportada en v1"
|
|
||||||
));
|
|
||||||
}
|
}
|
||||||
Stmt::Compute {
|
Stmt::Compute {
|
||||||
targets,
|
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.
|
/// Almacena un valor `Decimal` (texto de expresión) en un destino —
|
||||||
fn emit_store(em: &mut Emitter, sym: &Symbols, name: &str, value: &str, rounded: bool) {
|
/// un dato escalar o un elemento de tabla.
|
||||||
match sym.lookup(name) {
|
fn emit_store(em: &mut Emitter, sym: &Symbols, target: &Operand, value: &str, rounded: bool) {
|
||||||
Some(f) => match f.kind {
|
match field_ref(sym, target) {
|
||||||
FieldKind::Num { .. } => {
|
Some((lref, FieldKind::Num { .. })) => {
|
||||||
let method = if rounded { "store_rounded" } else { "store" };
|
let method = if rounded { "store_rounded" } else { "store" };
|
||||||
em.line(&format!("self.{}.{method}({value});", f.ident));
|
em.line(&format!("{lref}.{method}({value});"));
|
||||||
}
|
}
|
||||||
FieldKind::Text { .. } => {
|
Some((lref, FieldKind::Text { .. })) => {
|
||||||
em.line(&format!(
|
em.line(&format!("{lref}.store(({value}).to_string().as_str());"));
|
||||||
"self.{}.store(({value}).to_string().as_str());",
|
}
|
||||||
f.ident
|
None => em.line("// charka: destino no resuelto"),
|
||||||
));
|
|
||||||
}
|
|
||||||
},
|
|
||||||
None => em.line(&format!("// charka: destino no resuelto — {name}")),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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 {
|
for t in to {
|
||||||
match sym.lookup(t) {
|
match field_ref(sym, t) {
|
||||||
Some(f) => match f.kind {
|
Some((lref, FieldKind::Num { .. })) => {
|
||||||
FieldKind::Num { .. } => {
|
em.line(&format!("{lref}.store({});", operand_decimal(sym, from)));
|
||||||
em.line(&format!(
|
}
|
||||||
"self.{}.store({});",
|
Some((lref, FieldKind::Text { .. })) => {
|
||||||
f.ident,
|
if let Operand::Figurative(fig) = from {
|
||||||
operand_decimal(sym, 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 {
|
None => em.line("// charka: destino MOVE no resuelto"),
|
||||||
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}")),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -182,17 +162,14 @@ fn emit_add(
|
|||||||
em: &mut Emitter,
|
em: &mut Emitter,
|
||||||
sym: &Symbols,
|
sym: &Symbols,
|
||||||
addends: &[Operand],
|
addends: &[Operand],
|
||||||
to: &[String],
|
to: &[Operand],
|
||||||
giving: &[String],
|
giving: &[Operand],
|
||||||
rounded: bool,
|
rounded: bool,
|
||||||
) {
|
) {
|
||||||
let sum = fold_sum(sym, addends);
|
let sum = fold_sum(sym, addends);
|
||||||
if !giving.is_empty() {
|
if !giving.is_empty() {
|
||||||
let base = match to.first() {
|
let base = match to.first() {
|
||||||
Some(first) => format!(
|
Some(first) => format!("({sum}).add(&({}))", operand_decimal(sym, first)),
|
||||||
"({sum}).add(&({}))",
|
|
||||||
operand_decimal(sym, &Operand::Data(first.clone()))
|
|
||||||
),
|
|
||||||
None => sum,
|
None => sum,
|
||||||
};
|
};
|
||||||
for g in giving {
|
for g in giving {
|
||||||
@@ -209,15 +186,15 @@ fn emit_subtract(
|
|||||||
em: &mut Emitter,
|
em: &mut Emitter,
|
||||||
sym: &Symbols,
|
sym: &Symbols,
|
||||||
amounts: &[Operand],
|
amounts: &[Operand],
|
||||||
from: &[String],
|
from: &[Operand],
|
||||||
giving: &[String],
|
giving: &[Operand],
|
||||||
rounded: bool,
|
rounded: bool,
|
||||||
) {
|
) {
|
||||||
let sum = fold_sum(sym, amounts);
|
let sum = fold_sum(sym, amounts);
|
||||||
if !giving.is_empty() {
|
if !giving.is_empty() {
|
||||||
let minuend = from
|
let minuend = from
|
||||||
.first()
|
.first()
|
||||||
.map(|f| operand_decimal(sym, &Operand::Data(f.clone())))
|
.map(|f| operand_decimal(sym, f))
|
||||||
.unwrap_or_else(|| "Decimal::zero()".to_string());
|
.unwrap_or_else(|| "Decimal::zero()".to_string());
|
||||||
let value = format!("({minuend}).sub(&({sum}))");
|
let value = format!("({minuend}).sub(&({sum}))");
|
||||||
for g in giving {
|
for g in giving {
|
||||||
@@ -235,20 +212,18 @@ fn emit_multiply(
|
|||||||
sym: &Symbols,
|
sym: &Symbols,
|
||||||
left: &Operand,
|
left: &Operand,
|
||||||
by: &Operand,
|
by: &Operand,
|
||||||
giving: &[String],
|
giving: &[Operand],
|
||||||
rounded: bool,
|
rounded: bool,
|
||||||
) {
|
) {
|
||||||
let l = operand_decimal(sym, left);
|
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));
|
let value = format!("({l}).mul(&({}))", operand_decimal(sym, by));
|
||||||
for g in giving {
|
for g in giving {
|
||||||
emit_store(em, sym, g, &value, rounded);
|
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,
|
left: &Operand,
|
||||||
right: &Operand,
|
right: &Operand,
|
||||||
by_form: bool,
|
by_form: bool,
|
||||||
giving: &[String],
|
giving: &[Operand],
|
||||||
rounded: bool,
|
rounded: bool,
|
||||||
) {
|
) {
|
||||||
// `a BY b` → a/b; `a INTO b` → b/a.
|
// `a BY b` → a/b; `a INTO b` → b/a.
|
||||||
@@ -267,47 +242,46 @@ fn emit_divide(
|
|||||||
} else {
|
} else {
|
||||||
(operand_decimal(sym, right), operand_decimal(sym, left))
|
(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 {
|
for g in giving {
|
||||||
let value = format!(
|
let value = div(target_scale(sym, g));
|
||||||
"({num}).div(&({den}), {}, Rounding::Truncate).unwrap_or_else(|_| Decimal::zero())",
|
|
||||||
target_scale(sym, g)
|
|
||||||
);
|
|
||||||
emit_store(em, sym, g, &value, rounded);
|
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`.
|
/// Emite una operación aritmética en el lugar: `target = target <op> rhs`.
|
||||||
fn emit_inplace(em: &mut Emitter, sym: &Symbols, name: &str, op: &str, rhs: &str, rounded: bool) {
|
fn emit_inplace(
|
||||||
match sym.lookup(name) {
|
em: &mut Emitter,
|
||||||
Some(f) if matches!(f.kind, FieldKind::Num { .. }) => {
|
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" };
|
let method = if rounded { "store_rounded" } else { "store" };
|
||||||
em.line(&format!(
|
em.line(&format!("{lref}.{method}({lref}.value().{op}(&({rhs})));"));
|
||||||
"self.{0}.{method}(self.{0}.value().{op}(&({rhs})));",
|
|
||||||
f.ident
|
|
||||||
));
|
|
||||||
}
|
}
|
||||||
_ => em.line(&format!(
|
_ => em.line("// charka: destino aritmético no resuelto"),
|
||||||
"// charka: destino aritmético no resuelto — {name}"
|
|
||||||
)),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// La escala de redondeo de un destino numérico (sus dígitos
|
/// La escala de redondeo de un destino numérico (sus dígitos
|
||||||
/// fraccionarios), o 4 por defecto.
|
/// fraccionarios), o 4 por defecto.
|
||||||
fn target_scale(sym: &Symbols, name: &str) -> u8 {
|
fn target_scale(sym: &Symbols, op: &Operand) -> u8 {
|
||||||
match sym.lookup(name).map(|f| &f.kind) {
|
match field_ref(sym, op).map(|(_, k)| k) {
|
||||||
Some(FieldKind::Num { frac, .. }) => *frac,
|
Some(FieldKind::Num { frac, .. }) => frac,
|
||||||
_ => 4,
|
_ => 4,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -438,11 +412,12 @@ fn emit_perform(em: &mut Emitter, sym: &Symbols, p: &Perform) {
|
|||||||
until,
|
until,
|
||||||
} => {
|
} => {
|
||||||
// var = from; mientras no se cumpla `until`: cuerpo; var += by.
|
// 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.line(&format!("while !({}) {{", emit_cond(sym, until)));
|
||||||
em.indent();
|
em.indent();
|
||||||
emit_body(em, sym);
|
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.dedent();
|
||||||
em.line("}");
|
em.line("}");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,6 +20,8 @@ pub(crate) struct Field {
|
|||||||
pub kind: FieldKind,
|
pub kind: FieldKind,
|
||||||
/// Valor inicial normalizado (de la cláusula `VALUE`).
|
/// Valor inicial normalizado (de la cláusula `VALUE`).
|
||||||
pub init: String,
|
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.
|
/// Los campos del programa y sus nombres de condición, indexados.
|
||||||
@@ -40,6 +42,7 @@ impl Symbols {
|
|||||||
ident: sanitize_ident(&f.name),
|
ident: sanitize_ident(&f.name),
|
||||||
kind: f.kind,
|
kind: f.kind,
|
||||||
init: f.init.clone(),
|
init: f.init.clone(),
|
||||||
|
occurs: f.occurs,
|
||||||
})
|
})
|
||||||
.collect();
|
.collect();
|
||||||
dedup_idents(&mut fields);
|
dedup_idents(&mut fields);
|
||||||
|
|||||||
@@ -32,6 +32,9 @@ pub struct Procedure {
|
|||||||
pub enum Operand {
|
pub enum Operand {
|
||||||
/// Referencia a un dato, por nombre (en mayúsculas).
|
/// Referencia a un dato, por nombre (en mayúsculas).
|
||||||
Data(String),
|
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).
|
/// Literal numérico (texto, posiblemente con signo).
|
||||||
Num(String),
|
Num(String),
|
||||||
/// Literal de texto.
|
/// Literal de texto.
|
||||||
@@ -111,36 +114,36 @@ pub enum CmpOp {
|
|||||||
#[derive(Debug, Clone, PartialEq)]
|
#[derive(Debug, Clone, PartialEq)]
|
||||||
pub enum Stmt {
|
pub enum Stmt {
|
||||||
/// `MOVE from TO to...`
|
/// `MOVE from TO to...`
|
||||||
Move { from: Operand, to: Vec<String> },
|
Move { from: Operand, to: Vec<Operand> },
|
||||||
/// `DISPLAY items...`
|
/// `DISPLAY items...`
|
||||||
Display { items: Vec<Operand> },
|
Display { items: Vec<Operand> },
|
||||||
/// `ACCEPT into`
|
/// `ACCEPT into`
|
||||||
Accept { into: String },
|
Accept { into: Operand },
|
||||||
/// `COMPUTE targets... [ROUNDED] = expr`
|
/// `COMPUTE targets... [ROUNDED] = expr`
|
||||||
Compute {
|
Compute {
|
||||||
targets: Vec<String>,
|
targets: Vec<Operand>,
|
||||||
rounded: bool,
|
rounded: bool,
|
||||||
expr: Expr,
|
expr: Expr,
|
||||||
},
|
},
|
||||||
/// `ADD addends... TO to... [GIVING giving...]`
|
/// `ADD addends... TO to... [GIVING giving...]`
|
||||||
Add {
|
Add {
|
||||||
addends: Vec<Operand>,
|
addends: Vec<Operand>,
|
||||||
to: Vec<String>,
|
to: Vec<Operand>,
|
||||||
giving: Vec<String>,
|
giving: Vec<Operand>,
|
||||||
rounded: bool,
|
rounded: bool,
|
||||||
},
|
},
|
||||||
/// `SUBTRACT amounts... FROM from... [GIVING giving...]`
|
/// `SUBTRACT amounts... FROM from... [GIVING giving...]`
|
||||||
Subtract {
|
Subtract {
|
||||||
amounts: Vec<Operand>,
|
amounts: Vec<Operand>,
|
||||||
from: Vec<String>,
|
from: Vec<Operand>,
|
||||||
giving: Vec<String>,
|
giving: Vec<Operand>,
|
||||||
rounded: bool,
|
rounded: bool,
|
||||||
},
|
},
|
||||||
/// `MULTIPLY left BY by [GIVING giving...]`
|
/// `MULTIPLY left BY by [GIVING giving...]`
|
||||||
Multiply {
|
Multiply {
|
||||||
left: Operand,
|
left: Operand,
|
||||||
by: Operand,
|
by: Operand,
|
||||||
giving: Vec<String>,
|
giving: Vec<Operand>,
|
||||||
rounded: bool,
|
rounded: bool,
|
||||||
},
|
},
|
||||||
/// `DIVIDE left {BY|INTO} right [GIVING giving...]`. `by_form` es
|
/// `DIVIDE left {BY|INTO} right [GIVING giving...]`. `by_form` es
|
||||||
@@ -149,7 +152,7 @@ pub enum Stmt {
|
|||||||
left: Operand,
|
left: Operand,
|
||||||
right: Operand,
|
right: Operand,
|
||||||
by_form: bool,
|
by_form: bool,
|
||||||
giving: Vec<String>,
|
giving: Vec<Operand>,
|
||||||
rounded: bool,
|
rounded: bool,
|
||||||
},
|
},
|
||||||
/// `IF cond [THEN] then_branch [ELSE else_branch] [END-IF]`
|
/// `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
|
num.text
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
match c.bump() {
|
let base = match c.bump() {
|
||||||
Some(t) => token_to_operand(&t),
|
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.
|
/// Clasifica un token suelto como operando.
|
||||||
|
|||||||
@@ -93,7 +93,7 @@ mod tests {
|
|||||||
b,
|
b,
|
||||||
vec![Stmt::Move {
|
vec![Stmt::Move {
|
||||||
from: Operand::Num("5".into()),
|
from: Operand::Num("5".into()),
|
||||||
to: vec!["WS-X".into()],
|
to: vec![Operand::Data("WS-X".into())],
|
||||||
}]
|
}]
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -105,11 +105,27 @@ mod tests {
|
|||||||
b,
|
b,
|
||||||
vec![Stmt::Move {
|
vec![Stmt::Move {
|
||||||
from: Operand::Data("WS-A".into()),
|
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]
|
#[test]
|
||||||
fn display_items_and_figurative() {
|
fn display_items_and_figurative() {
|
||||||
let b = body("DISPLAY 'TOTAL: ' WS-TOTAL SPACES.");
|
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 b = body("COMPUTE WS-T = WS-A + WS-B * 2.");
|
||||||
let expr = match &b[0] {
|
let expr = match &b[0] {
|
||||||
Stmt::Compute { targets, expr, .. } => {
|
Stmt::Compute { targets, expr, .. } => {
|
||||||
assert_eq!(targets, &vec!["WS-T".to_string()]);
|
assert_eq!(targets, &vec![Operand::Data("WS-T".into())]);
|
||||||
expr.clone()
|
expr.clone()
|
||||||
}
|
}
|
||||||
other => panic!("se esperaba COMPUTE, vino {other:?}"),
|
other => panic!("se esperaba COMPUTE, vino {other:?}"),
|
||||||
@@ -162,7 +178,7 @@ mod tests {
|
|||||||
body("ADD 1 TO WS-CT."),
|
body("ADD 1 TO WS-CT."),
|
||||||
vec![Stmt::Add {
|
vec![Stmt::Add {
|
||||||
addends: vec![Operand::Num("1".into())],
|
addends: vec![Operand::Num("1".into())],
|
||||||
to: vec!["WS-CT".into()],
|
to: vec![Operand::Data("WS-CT".into())],
|
||||||
giving: vec![],
|
giving: vec![],
|
||||||
rounded: false,
|
rounded: false,
|
||||||
}]
|
}]
|
||||||
@@ -172,7 +188,7 @@ mod tests {
|
|||||||
vec![Stmt::Add {
|
vec![Stmt::Add {
|
||||||
addends: vec![Operand::Data("WS-A".into()), Operand::Data("WS-B".into()),],
|
addends: vec![Operand::Data("WS-A".into()), Operand::Data("WS-B".into()),],
|
||||||
to: vec![],
|
to: vec![],
|
||||||
giving: vec!["WS-C".into()],
|
giving: vec![Operand::Data("WS-C".into())],
|
||||||
rounded: false,
|
rounded: false,
|
||||||
}]
|
}]
|
||||||
);
|
);
|
||||||
@@ -184,8 +200,8 @@ mod tests {
|
|||||||
body("SUBTRACT WS-TAX FROM WS-GROSS GIVING WS-NET."),
|
body("SUBTRACT WS-TAX FROM WS-GROSS GIVING WS-NET."),
|
||||||
vec![Stmt::Subtract {
|
vec![Stmt::Subtract {
|
||||||
amounts: vec![Operand::Data("WS-TAX".into())],
|
amounts: vec![Operand::Data("WS-TAX".into())],
|
||||||
from: vec!["WS-GROSS".into()],
|
from: vec![Operand::Data("WS-GROSS".into())],
|
||||||
giving: vec!["WS-NET".into()],
|
giving: vec![Operand::Data("WS-NET".into())],
|
||||||
rounded: false,
|
rounded: false,
|
||||||
}]
|
}]
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -28,6 +28,9 @@ pub struct Field {
|
|||||||
pub kind: FieldKind,
|
pub kind: FieldKind,
|
||||||
/// Valor inicial ya normalizado (de la cláusula `VALUE`).
|
/// Valor inicial ya normalizado (de la cláusula `VALUE`).
|
||||||
pub init: String,
|
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
|
/// 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(),
|
name: it.name.to_uppercase(),
|
||||||
kind,
|
kind,
|
||||||
init,
|
init,
|
||||||
|
occurs: it.occurs,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -57,10 +57,11 @@ fn parse_one_stmt(c: &mut Cursor, stops: &[&str]) -> Stmt {
|
|||||||
|
|
||||||
// ── Listas ────────────────────────────────────────────────────────
|
// ── Listas ────────────────────────────────────────────────────────
|
||||||
|
|
||||||
/// Lee una lista de nombres de dato (separados por comas opcionales),
|
/// Lee una lista de destinos de dato (separados por comas opcionales),
|
||||||
/// hasta una palabra frontera. Consume las apariciones de `ROUNDED`.
|
/// hasta una palabra frontera. Cada destino puede llevar subíndice de
|
||||||
fn parse_name_list(c: &mut Cursor, rounded: &mut bool) -> Vec<String> {
|
/// tabla. Consume las apariciones de `ROUNDED`.
|
||||||
let mut names = Vec::new();
|
fn parse_targets(c: &mut Cursor, rounded: &mut bool) -> Vec<Operand> {
|
||||||
|
let mut targets = Vec::new();
|
||||||
loop {
|
loop {
|
||||||
c.eat_sym(",");
|
c.eat_sym(",");
|
||||||
if c.eat_word("ROUNDED") {
|
if c.eat_word("ROUNDED") {
|
||||||
@@ -68,14 +69,11 @@ fn parse_name_list(c: &mut Cursor, rounded: &mut bool) -> Vec<String> {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
match c.peek_word() {
|
match c.peek_word() {
|
||||||
Some(w) if !is_boundary(&w) => {
|
Some(w) if !is_boundary(&w) => targets.push(parse_operand(c)),
|
||||||
c.bump();
|
|
||||||
names.push(w);
|
|
||||||
}
|
|
||||||
_ => break,
|
_ => break,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
names
|
targets
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Lee una lista de operandos hasta una palabra frontera.
|
/// 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);
|
let from = parse_operand(c);
|
||||||
c.eat_word("TO");
|
c.eat_word("TO");
|
||||||
let mut rounded = false;
|
let mut rounded = false;
|
||||||
let to = parse_name_list(c, &mut rounded);
|
let to = parse_targets(c, &mut rounded);
|
||||||
Stmt::Move { from, to }
|
Stmt::Move { from, to }
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -150,7 +148,11 @@ fn parse_display(c: &mut Cursor) -> Stmt {
|
|||||||
|
|
||||||
fn parse_accept(c: &mut Cursor) -> Stmt {
|
fn parse_accept(c: &mut Cursor) -> Stmt {
|
||||||
c.bump(); // ACCEPT
|
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`
|
skip_to_stmt_boundary(c); // p. ej. `FROM DATE`
|
||||||
Stmt::Accept { into }
|
Stmt::Accept { into }
|
||||||
}
|
}
|
||||||
@@ -158,7 +160,7 @@ fn parse_accept(c: &mut Cursor) -> Stmt {
|
|||||||
fn parse_compute(c: &mut Cursor) -> Stmt {
|
fn parse_compute(c: &mut Cursor) -> Stmt {
|
||||||
c.bump(); // COMPUTE
|
c.bump(); // COMPUTE
|
||||||
let mut rounded = false;
|
let mut rounded = false;
|
||||||
let targets = parse_name_list(c, &mut rounded);
|
let targets = parse_targets(c, &mut rounded);
|
||||||
if !c.eat_sym("=") {
|
if !c.eat_sym("=") {
|
||||||
c.eat_word("EQUAL");
|
c.eat_word("EQUAL");
|
||||||
}
|
}
|
||||||
@@ -180,10 +182,10 @@ fn parse_add(c: &mut Cursor) -> Stmt {
|
|||||||
let mut to = Vec::new();
|
let mut to = Vec::new();
|
||||||
let mut giving = Vec::new();
|
let mut giving = Vec::new();
|
||||||
if c.eat_word("TO") {
|
if c.eat_word("TO") {
|
||||||
to = parse_name_list(c, &mut rounded);
|
to = parse_targets(c, &mut rounded);
|
||||||
}
|
}
|
||||||
if c.eat_word("GIVING") {
|
if c.eat_word("GIVING") {
|
||||||
giving = parse_name_list(c, &mut rounded);
|
giving = parse_targets(c, &mut rounded);
|
||||||
}
|
}
|
||||||
c.eat_word("END-ADD");
|
c.eat_word("END-ADD");
|
||||||
Stmt::Add {
|
Stmt::Add {
|
||||||
@@ -203,10 +205,10 @@ fn parse_subtract(c: &mut Cursor) -> Stmt {
|
|||||||
let mut from = Vec::new();
|
let mut from = Vec::new();
|
||||||
let mut giving = Vec::new();
|
let mut giving = Vec::new();
|
||||||
if c.eat_word("FROM") {
|
if c.eat_word("FROM") {
|
||||||
from = parse_name_list(c, &mut rounded);
|
from = parse_targets(c, &mut rounded);
|
||||||
}
|
}
|
||||||
if c.eat_word("GIVING") {
|
if c.eat_word("GIVING") {
|
||||||
giving = parse_name_list(c, &mut rounded);
|
giving = parse_targets(c, &mut rounded);
|
||||||
}
|
}
|
||||||
c.eat_word("END-SUBTRACT");
|
c.eat_word("END-SUBTRACT");
|
||||||
Stmt::Subtract {
|
Stmt::Subtract {
|
||||||
@@ -225,7 +227,7 @@ fn parse_multiply(c: &mut Cursor) -> Stmt {
|
|||||||
let mut rounded = false;
|
let mut rounded = false;
|
||||||
let mut giving = Vec::new();
|
let mut giving = Vec::new();
|
||||||
if c.eat_word("GIVING") {
|
if c.eat_word("GIVING") {
|
||||||
giving = parse_name_list(c, &mut rounded);
|
giving = parse_targets(c, &mut rounded);
|
||||||
} else if c.eat_word("ROUNDED") {
|
} else if c.eat_word("ROUNDED") {
|
||||||
rounded = true;
|
rounded = true;
|
||||||
}
|
}
|
||||||
@@ -251,12 +253,12 @@ fn parse_divide(c: &mut Cursor) -> Stmt {
|
|||||||
let mut rounded = false;
|
let mut rounded = false;
|
||||||
let mut giving = Vec::new();
|
let mut giving = Vec::new();
|
||||||
if c.eat_word("GIVING") {
|
if c.eat_word("GIVING") {
|
||||||
giving = parse_name_list(c, &mut rounded);
|
giving = parse_targets(c, &mut rounded);
|
||||||
} else if c.eat_word("ROUNDED") {
|
} else if c.eat_word("ROUNDED") {
|
||||||
rounded = true;
|
rounded = true;
|
||||||
}
|
}
|
||||||
if c.eat_word("REMAINDER") {
|
if c.eat_word("REMAINDER") {
|
||||||
let _ = parse_name_list(c, &mut rounded);
|
let _ = parse_targets(c, &mut rounded);
|
||||||
}
|
}
|
||||||
c.eat_word("END-DIVIDE");
|
c.eat_word("END-DIVIDE");
|
||||||
Stmt::Divide {
|
Stmt::Divide {
|
||||||
|
|||||||
@@ -53,6 +53,9 @@ pub struct DataItem {
|
|||||||
/// Cláusula `VALUE`: literal numérico (con signo), constante
|
/// Cláusula `VALUE`: literal numérico (con signo), constante
|
||||||
/// figurativa en mayúsculas, o literal de texto entre comillas.
|
/// figurativa en mayúsculas, o literal de texto entre comillas.
|
||||||
pub value: Option<String>,
|
pub value: Option<String>,
|
||||||
|
/// Cláusula `OCCURS n [TIMES]`: el dato es una tabla de `n`
|
||||||
|
/// elementos. `None` si es un dato escalar.
|
||||||
|
pub occurs: Option<u32>,
|
||||||
/// Ítems subordinados (de nivel numérico mayor).
|
/// Ítems subordinados (de nivel numérico mayor).
|
||||||
pub children: Vec<DataItem>,
|
pub children: Vec<DataItem>,
|
||||||
}
|
}
|
||||||
@@ -205,9 +208,24 @@ fn parse_data_entry(level: u8, sent: &[Token]) -> Result<DataItem, ParseError> {
|
|||||||
|
|
||||||
let mut picture = None;
|
let mut picture = None;
|
||||||
let mut value = None;
|
let mut value = None;
|
||||||
|
let mut occurs = None;
|
||||||
let mut i = 2;
|
let mut i = 2;
|
||||||
while i < sent.len() {
|
while i < sent.len() {
|
||||||
match kw(sent.get(i)).as_deref() {
|
match kw(sent.get(i)).as_deref() {
|
||||||
|
Some("OCCURS") => {
|
||||||
|
i += 1;
|
||||||
|
if let Some(t) = sent.get(i) {
|
||||||
|
if t.kind == TokenKind::Number {
|
||||||
|
if occurs.is_none() {
|
||||||
|
occurs = t.text.parse::<u32>().ok();
|
||||||
|
}
|
||||||
|
i += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if kw(sent.get(i)).as_deref() == Some("TIMES") {
|
||||||
|
i += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
Some("PIC") | Some("PICTURE") => {
|
Some("PIC") | Some("PICTURE") => {
|
||||||
i += 1;
|
i += 1;
|
||||||
if kw(sent.get(i)).as_deref() == Some("IS") {
|
if kw(sent.get(i)).as_deref() == Some("IS") {
|
||||||
@@ -239,6 +257,7 @@ fn parse_data_entry(level: u8, sent: &[Token]) -> Result<DataItem, ParseError> {
|
|||||||
name,
|
name,
|
||||||
picture,
|
picture,
|
||||||
value,
|
value,
|
||||||
|
occurs,
|
||||||
children: Vec::new(),
|
children: Vec::new(),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -605,6 +624,20 @@ mod tests {
|
|||||||
assert_eq!(p.data[0].children[1].name, "FILLER");
|
assert_eq!(p.data[0].children[1].name, "FILLER");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn occurs_clause_captured() {
|
||||||
|
let p = parse_src(
|
||||||
|
"DATA DIVISION.\n\
|
||||||
|
WORKING-STORAGE SECTION.\n\
|
||||||
|
01 WS-TABLA.\n\
|
||||||
|
05 WS-ELEM PIC 9(3) OCCURS 10 TIMES.\n",
|
||||||
|
);
|
||||||
|
let elem = &p.data[0].children[0];
|
||||||
|
assert_eq!(elem.name, "WS-ELEM");
|
||||||
|
assert_eq!(elem.occurs, Some(10));
|
||||||
|
assert_eq!(elem.picture.as_deref(), Some("9(3)"));
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn bad_level_number_is_error() {
|
fn bad_level_number_is_error() {
|
||||||
let toks = lex(
|
let toks = lex(
|
||||||
|
|||||||
@@ -6,21 +6,27 @@ use std::collections::HashMap;
|
|||||||
use charka_ir::{DataModel, FieldKind};
|
use charka_ir::{DataModel, FieldKind};
|
||||||
use charka_runtime::{Num, Picture, Text};
|
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 {
|
pub(crate) enum Cell {
|
||||||
Num(Num),
|
Num(Vec<Num>),
|
||||||
Text(Text),
|
Text(Vec<Text>),
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Materializa los campos del modelo en un mapa `nombre → campo`.
|
/// Materializa los campos del modelo en un mapa `nombre → campo`.
|
||||||
pub(crate) fn build_fields(model: &DataModel) -> HashMap<String, Cell> {
|
pub(crate) fn build_fields(model: &DataModel) -> HashMap<String, Cell> {
|
||||||
let mut map = HashMap::new();
|
let mut map = HashMap::new();
|
||||||
for f in &model.fields {
|
for f in &model.fields {
|
||||||
|
let n = f.occurs.unwrap_or(1).max(1) as usize;
|
||||||
let cell = match f.kind {
|
let cell = match f.kind {
|
||||||
FieldKind::Num { int, frac, signed } => {
|
FieldKind::Num { int, frac, signed } => Cell::Num(vec![
|
||||||
Cell::Num(Num::with_value(Picture::new(int, frac, signed), &f.init))
|
Num::with_value(
|
||||||
}
|
Picture::new(int, frac, signed),
|
||||||
FieldKind::Text { len } => Cell::Text(Text::with_value(len, &f.init)),
|
&f.init
|
||||||
|
);
|
||||||
|
n
|
||||||
|
]),
|
||||||
|
FieldKind::Text { len } => Cell::Text(vec![Text::with_value(len, &f.init); n]),
|
||||||
};
|
};
|
||||||
map.entry(f.name.clone()).or_insert(cell);
|
map.entry(f.name.clone()).or_insert(cell);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -141,12 +141,12 @@ impl<'a> Machine<'a> {
|
|||||||
let sum = self.fold_sum(addends);
|
let sum = self.fold_sum(addends);
|
||||||
if giving.is_empty() {
|
if giving.is_empty() {
|
||||||
for t in to {
|
for t in to {
|
||||||
let cur = self.field_value(t);
|
let cur = self.eval_decimal(t);
|
||||||
self.store(t, cur.add(&sum), *rounded);
|
self.store(t, cur.add(&sum), *rounded);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
let base = match to.first() {
|
let base = match to.first() {
|
||||||
Some(first) => sum.add(&self.field_value(first)),
|
Some(first) => sum.add(&self.eval_decimal(first)),
|
||||||
None => sum,
|
None => sum,
|
||||||
};
|
};
|
||||||
for g in giving {
|
for g in giving {
|
||||||
@@ -164,13 +164,13 @@ impl<'a> Machine<'a> {
|
|||||||
let sum = self.fold_sum(amounts);
|
let sum = self.fold_sum(amounts);
|
||||||
if giving.is_empty() {
|
if giving.is_empty() {
|
||||||
for t in from {
|
for t in from {
|
||||||
let cur = self.field_value(t);
|
let cur = self.eval_decimal(t);
|
||||||
self.store(t, cur.sub(&sum), *rounded);
|
self.store(t, cur.sub(&sum), *rounded);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
let minuend = from
|
let minuend = from
|
||||||
.first()
|
.first()
|
||||||
.map(|f| self.field_value(f))
|
.map(|f| self.eval_decimal(f))
|
||||||
.unwrap_or_else(Decimal::zero);
|
.unwrap_or_else(Decimal::zero);
|
||||||
let value = minuend.sub(&sum);
|
let value = minuend.sub(&sum);
|
||||||
for g in giving {
|
for g in giving {
|
||||||
@@ -187,9 +187,8 @@ impl<'a> Machine<'a> {
|
|||||||
} => {
|
} => {
|
||||||
let value = self.eval_decimal(left).mul(&self.eval_decimal(by));
|
let value = self.eval_decimal(left).mul(&self.eval_decimal(by));
|
||||||
if giving.is_empty() {
|
if giving.is_empty() {
|
||||||
if let Operand::Data(name) = by {
|
// `MULTIPLY a BY b` sin GIVING: b queda con a*b.
|
||||||
self.store(name, value, *rounded);
|
self.store(by, value, *rounded);
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
for g in giving {
|
for g in giving {
|
||||||
self.store(g, value, *rounded);
|
self.store(g, value, *rounded);
|
||||||
@@ -210,10 +209,9 @@ impl<'a> Machine<'a> {
|
|||||||
(self.eval_decimal(right), self.eval_decimal(left))
|
(self.eval_decimal(right), self.eval_decimal(left))
|
||||||
};
|
};
|
||||||
if giving.is_empty() {
|
if giving.is_empty() {
|
||||||
if let Operand::Data(name) = right {
|
// `DIVIDE a INTO b` sin GIVING: b queda con b/a.
|
||||||
let v = divide(num, den, self.target_scale(name));
|
let v = divide(num, den, self.target_scale(right));
|
||||||
self.store(name, v, *rounded);
|
self.store(right, v, *rounded);
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
for g in giving {
|
for g in giving {
|
||||||
let v = divide(num, den, self.target_scale(g));
|
let v = divide(num, den, self.target_scale(g));
|
||||||
@@ -296,8 +294,9 @@ impl<'a> Machine<'a> {
|
|||||||
by,
|
by,
|
||||||
until,
|
until,
|
||||||
} => {
|
} => {
|
||||||
|
let var_op = Operand::Data(var.clone());
|
||||||
let start = self.eval_decimal(from);
|
let start = self.eval_decimal(from);
|
||||||
self.store(var, start, false);
|
self.store(&var_op, start, false);
|
||||||
loop {
|
loop {
|
||||||
if self.tick() {
|
if self.tick() {
|
||||||
return Flow::Stop;
|
return Flow::Stop;
|
||||||
@@ -308,8 +307,8 @@ impl<'a> Machine<'a> {
|
|||||||
if let Flow::Stop = self.run_target(&p.target) {
|
if let Flow::Stop = self.run_target(&p.target) {
|
||||||
return Flow::Stop;
|
return Flow::Stop;
|
||||||
}
|
}
|
||||||
let next = self.field_value(var).add(&self.eval_decimal(by));
|
let next = self.eval_decimal(&var_op).add(&self.eval_decimal(by));
|
||||||
self.store(var, next, false);
|
self.store(&var_op, next, false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -339,44 +338,74 @@ impl<'a> Machine<'a> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// `MOVE from` a un solo campo destino.
|
/// Resuelve una referencia a dato (escalar o elemento de tabla) a
|
||||||
fn do_move(&mut self, from: &Operand, target: &str) {
|
/// su nombre y un índice 0-based. `None` si no es una referencia.
|
||||||
let key = target.to_uppercase();
|
fn resolve(&self, op: &Operand) -> Option<(String, usize)> {
|
||||||
match self.fields.get(&key) {
|
match op {
|
||||||
Some(Cell::Num(_)) => {
|
Operand::Data(name) => Some((name.to_uppercase(), 0)),
|
||||||
let v = self.eval_decimal(from);
|
Operand::Indexed { name, index } => {
|
||||||
if let Some(Cell::Num(n)) = self.fields.get_mut(&key) {
|
// El subíndice de COBOL es 1-based.
|
||||||
n.store(v);
|
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(_)) => {
|
_ => None,
|
||||||
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 => {}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Almacena un valor en un campo, conformándolo a su tipo.
|
/// `MOVE from` a un solo destino (escalar o elemento de tabla).
|
||||||
fn store(&mut self, name: &str, value: Decimal, rounded: bool) {
|
fn do_move(&mut self, from: &Operand, target: &Operand) {
|
||||||
match self.fields.get_mut(&name.to_uppercase()) {
|
let Some((key, idx)) = self.resolve(target) else {
|
||||||
Some(Cell::Num(n)) => {
|
return;
|
||||||
if rounded {
|
};
|
||||||
n.store_rounded(value);
|
let is_num = matches!(self.fields.get(&key), Some(Cell::Num(_)));
|
||||||
} else {
|
if is_num {
|
||||||
n.store(value);
|
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 => {}
|
None => {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -388,13 +417,22 @@ impl<'a> Machine<'a> {
|
|||||||
Operand::Num(n) => Decimal::parse(n).unwrap_or_else(|_| Decimal::zero()),
|
Operand::Num(n) => Decimal::parse(n).unwrap_or_else(|_| Decimal::zero()),
|
||||||
Operand::Str(s) => Decimal::parse(s).unwrap_or_else(|_| Decimal::zero()),
|
Operand::Str(s) => Decimal::parse(s).unwrap_or_else(|_| Decimal::zero()),
|
||||||
Operand::Figurative(_) => Decimal::zero(),
|
Operand::Figurative(_) => Decimal::zero(),
|
||||||
Operand::Data(name) => match self.fields.get(&name.to_uppercase()) {
|
Operand::Data(_) | Operand::Indexed { .. } => {
|
||||||
Some(Cell::Num(n)) => n.value(),
|
let Some((key, idx)) = self.resolve(op) else {
|
||||||
Some(Cell::Text(t)) => {
|
return Decimal::zero();
|
||||||
Decimal::parse(t.as_str().trim()).unwrap_or_else(|_| 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::Str(s) => s.clone(),
|
||||||
Operand::Num(n) => n.clone(),
|
Operand::Num(n) => n.clone(),
|
||||||
Operand::Figurative(f) => figurative_text(*f).to_string(),
|
Operand::Figurative(f) => figurative_text(*f).to_string(),
|
||||||
Operand::Data(name) => match self.fields.get(&name.to_uppercase()) {
|
Operand::Data(_) | Operand::Indexed { .. } => {
|
||||||
Some(Cell::Num(n)) => n.display(),
|
let Some((key, idx)) = self.resolve(op) else {
|
||||||
Some(Cell::Text(t)) => t.display(),
|
return String::new();
|
||||||
None => 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 {
|
fn is_text(&self, op: &Operand) -> bool {
|
||||||
match op {
|
match op {
|
||||||
Operand::Str(_) => true,
|
Operand::Str(_) => true,
|
||||||
Operand::Data(name) => {
|
Operand::Data(_) | Operand::Indexed { .. } => match self.resolve(op) {
|
||||||
matches!(self.fields.get(&name.to_uppercase()), Some(Cell::Text(_)))
|
Some((key, _)) => matches!(self.fields.get(&key), Some(Cell::Text(_))),
|
||||||
}
|
None => false,
|
||||||
|
},
|
||||||
_ => false,
|
_ => false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -486,23 +530,16 @@ impl<'a> Machine<'a> {
|
|||||||
acc
|
acc
|
||||||
}
|
}
|
||||||
|
|
||||||
/// El valor actual de un campo por nombre.
|
/// Los dígitos fraccionarios de un destino numérico.
|
||||||
fn field_value(&self, name: &str) -> Decimal {
|
fn target_scale(&self, op: &Operand) -> u8 {
|
||||||
match self.fields.get(&name.to_uppercase()) {
|
if let Some((key, idx)) = self.resolve(op) {
|
||||||
Some(Cell::Num(n)) => n.value(),
|
if let Some(Cell::Num(arr)) = self.fields.get(&key) {
|
||||||
Some(Cell::Text(t)) => {
|
if let Some(n) = arr.get(idx) {
|
||||||
Decimal::parse(t.as_str().trim()).unwrap_or_else(|_| Decimal::zero())
|
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`.
|
/// 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_08_varying, "08-varying");
|
||||||
corpus_test!(corpus_09_evaluar, "09-evaluar");
|
corpus_test!(corpus_09_evaluar, "09-evaluar");
|
||||||
corpus_test!(corpus_10_condicion, "10-condicion");
|
corpus_test!(corpus_10_condicion, "10-condicion");
|
||||||
|
corpus_test!(corpus_11_tabla, "11-tabla");
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn empty_source_runs_clean() {
|
fn empty_source_runs_clean() {
|
||||||
|
|||||||
@@ -0,0 +1,20 @@
|
|||||||
|
* corpus charka — nivel 6: tablas (OCCURS) y subíndices
|
||||||
|
IDENTIFICATION DIVISION.
|
||||||
|
PROGRAM-ID. TABLAS.
|
||||||
|
DATA DIVISION.
|
||||||
|
WORKING-STORAGE SECTION.
|
||||||
|
01 WS-TABLA.
|
||||||
|
05 WS-ELEM PIC 9(4) OCCURS 5 TIMES.
|
||||||
|
01 WS-I PIC 9(2) VALUE 0.
|
||||||
|
01 WS-TOTAL PIC 9(6) VALUE 0.
|
||||||
|
PROCEDURE DIVISION.
|
||||||
|
MAIN.
|
||||||
|
PERFORM VARYING WS-I FROM 1 BY 1 UNTIL WS-I > 5
|
||||||
|
COMPUTE WS-ELEM(WS-I) = WS-I * WS-I
|
||||||
|
END-PERFORM.
|
||||||
|
PERFORM VARYING WS-I FROM 1 BY 1 UNTIL WS-I > 5
|
||||||
|
ADD WS-ELEM(WS-I) TO WS-TOTAL
|
||||||
|
DISPLAY 'ELEM ' WS-I ' = ' WS-ELEM(WS-I)
|
||||||
|
END-PERFORM.
|
||||||
|
DISPLAY 'SUMA DE CUADRADOS = ' WS-TOTAL.
|
||||||
|
STOP RUN.
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
ELEM 01 = 0001
|
||||||
|
ELEM 02 = 0004
|
||||||
|
ELEM 03 = 0009
|
||||||
|
ELEM 04 = 0016
|
||||||
|
ELEM 05 = 0025
|
||||||
|
SUMA DE CUADRADOS = 000055
|
||||||
@@ -19,6 +19,7 @@ salida correcta, una línea por `DISPLAY`.
|
|||||||
| `08-varying` | 4 | `PERFORM VARYING` — el bucle con variable de control|
|
| `08-varying` | 4 | `PERFORM VARYING` — el bucle con variable de control|
|
||||||
| `09-evaluar` | 5 | `EVALUATE` — el `case` de COBOL, `WHEN` / `OTHER` |
|
| `09-evaluar` | 5 | `EVALUATE` — el `case` de COBOL, `WHEN` / `OTHER` |
|
||||||
| `10-condicion` | 5 | nombres de condición (nivel 88) en `IF` |
|
| `10-condicion` | 5 | nombres de condición (nivel 88) en `IF` |
|
||||||
|
| `11-tabla` | 6 | tablas (`OCCURS`) y referencias con subíndice |
|
||||||
|
|
||||||
## Formato
|
## Formato
|
||||||
|
|
||||||
|
|||||||
@@ -3,6 +3,30 @@
|
|||||||
Transpilador COBOL → Rust. El módulo más grande del ecosistema (Fase D
|
Transpilador COBOL → Rust. El módulo más grande del ecosistema (Fase D
|
||||||
del plan macro) — el parser COBOL completo es un esfuerzo multi-mes.
|
del plan macro) — el parser COBOL completo es un esfuerzo multi-mes.
|
||||||
|
|
||||||
|
### feat(charka): OCCURS — tablas y referencias con subíndice
|
||||||
|
|
||||||
|
Los arrays de COBOL, que antes el transpilador descartaba en silencio.
|
||||||
|
Atraviesa el pipeline entero.
|
||||||
|
|
||||||
|
- Parser: la cláusula `OCCURS n [TIMES]` se captura en `DataItem`.
|
||||||
|
- IR: `Operand::Indexed { name, index }` — una referencia `ELEM(I)`.
|
||||||
|
El subíndice es 1-based, como COBOL. 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) = ...`).
|
||||||
|
`Field` del modelo 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` en datos elementales, una dimensión, subíndice
|
||||||
|
de un solo operando. Fuera: `OCCURS` de grupo, multidimensional,
|
||||||
|
`OCCURS DEPENDING ON`.
|
||||||
|
|
||||||
### feat(charka): nombres de condición (nivel 88) + modelo de datos compartido
|
### feat(charka): nombres de condición (nivel 88) + modelo de datos compartido
|
||||||
|
|
||||||
`IF ES-VALIDO` — los nombres de condición de COBOL, que antes el
|
`IF ES-VALIDO` — los nombres de condición de COBOL, que antes el
|
||||||
|
|||||||
Reference in New Issue
Block a user