feat(charka): nivel 88 + modelo de datos compartido en charka-ir
Los nombres de condición de COBOL (IF ES-VALIDO), que antes el transpilador evaluaba siempre como false. Y, de paso, se elimina la duplicación de la resolución del modelo de datos. - charka-ir gana un módulo `model`: resolve_data(&[DataItem]) -> DataModel aplana el árbol de datos a campos elementales (Field con FieldKind) y a nombres de condición (ConditionName). El Ir lleva ahora un campo `model` — la fuente única de verdad sobre la clasificación de PICTURE. - charka-codegen y charka-shadow consumen ir.model en vez de reimplementar cada uno la clasificación, el ancho de PICTURE y la normalización de VALUE. charka-codegen ya no depende de charka-bcd. - Cond::Named (un nivel 88) se resuelve a `padre = valor`: el codegen emite la comparación, el intérprete sombra la evalúa. - Corregido: un dato con hijos de nivel 88 antes se perdía como si fuera un grupo; ahora se reconoce como campo elemental. - Corpus: programa nuevo 10-condicion (semáforo con 88 de texto y de número). Verificado: intérprete y crate compilado dan igual salida. Tests: charka-ir 23, charka-codegen 17, charka-shadow 15. fmt + clippy limpios. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
Generated
+1
-1
@@ -2332,7 +2332,6 @@ dependencies = [
|
|||||||
name = "charka-codegen"
|
name = "charka-codegen"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"charka-bcd",
|
|
||||||
"charka-ir",
|
"charka-ir",
|
||||||
"charka-lexer",
|
"charka-lexer",
|
||||||
"charka-parser",
|
"charka-parser",
|
||||||
@@ -2342,6 +2341,7 @@ dependencies = [
|
|||||||
name = "charka-ir"
|
name = "charka-ir"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"charka-bcd",
|
||||||
"charka-lexer",
|
"charka-lexer",
|
||||||
"charka-parser",
|
"charka-parser",
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -75,15 +75,22 @@ Tercera etapa: `Program` → `Ir`. Aquí se parsea cada `Sentence` cruda
|
|||||||
árbol de instrucciones.
|
árbol de instrucciones.
|
||||||
|
|
||||||
- `lower(&Program) -> Ir` — **total y tolerante**, nunca falla.
|
- `lower(&Program) -> Ir` — **total y tolerante**, nunca falla.
|
||||||
- `Ir { program_id, data: Vec<DataItem>, procedures: Vec<Procedure> }`.
|
- `Ir { program_id, data, model, procedures }`. `data` es el árbol de
|
||||||
El modelo de datos pasa tal cual (sirve de tabla de símbolos).
|
`DataItem` con su estructura de grupos; `model` es el **modelo de
|
||||||
|
datos resuelto** (módulo `model`): el árbol aplanado a campos
|
||||||
|
elementales (`Field` con `FieldKind` numérico/alfanumérico y el
|
||||||
|
`VALUE` normalizado) más los nombres de condición (nivel 88). Es la
|
||||||
|
fuente única de verdad sobre «qué tipo de campo describe una
|
||||||
|
PICTURE» — `charka-codegen` y `charka-shadow` lo consumen en vez de
|
||||||
|
reimplementar la clasificación.
|
||||||
- `Procedure { name, body: Vec<Stmt> }`. `Stmt` cubre `Move`,
|
- `Procedure { name, body: Vec<Stmt> }`. `Stmt` cubre `Move`,
|
||||||
`Display`, `Accept`, `Compute`, `Add`/`Subtract`/`Multiply`/`Divide`,
|
`Display`, `Accept`, `Compute`, `Add`/`Subtract`/`Multiply`/`Divide`,
|
||||||
`If`, `Evaluate`, `Perform`, `GoTo`, `StopRun`, `Goback`, `Exit`,
|
`If`, `Evaluate`, `Perform`, `GoTo`, `StopRun`, `Goback`, `Exit`,
|
||||||
`Continue`.
|
`Continue`.
|
||||||
- `Expr` — expresiones aritméticas con precedencia y paréntesis (Pratt:
|
- `Expr` — expresiones aritméticas con precedencia y paréntesis (Pratt:
|
||||||
`+ -` < `* /` < `**` der.). `Cond` — comparaciones (símbolo o forma
|
`+ -` < `* /` < `**` der.). `Cond` — comparaciones (símbolo o forma
|
||||||
palabra) unidas por `AND`/`OR`/`NOT`, más nombres de condición (88).
|
palabra) unidas por `AND`/`OR`/`NOT`, más nombres de condición
|
||||||
|
(nivel 88), que se resuelven contra el `model` a `padre = valor`.
|
||||||
- Un verbo no soportado se conserva como `Stmt::Unknown { verb,
|
- Un verbo no soportado se conserva como `Stmt::Unknown { verb,
|
||||||
tokens }` — el lowering jamás aborta.
|
tokens }` — el lowering jamás aborta.
|
||||||
- COBOL no separa statements con un símbolo: cada uno corta donde
|
- COBOL no separa statements con un símbolo: cada uno corta donde
|
||||||
|
|||||||
@@ -10,7 +10,6 @@ description = "charka-codegen — emisión de Rust desde el IR de charka: el mod
|
|||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
charka-ir = { path = "../charka-ir" }
|
charka-ir = { path = "../charka-ir" }
|
||||||
charka-bcd = { path = "../charka-bcd" }
|
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
charka-parser = { path = "../charka-parser" }
|
charka-parser = { path = "../charka-parser" }
|
||||||
|
|||||||
@@ -114,9 +114,19 @@ pub(crate) fn emit_expr(sym: &Symbols, e: &Expr) -> String {
|
|||||||
pub(crate) fn emit_cond(sym: &Symbols, c: &Cond) -> String {
|
pub(crate) fn emit_cond(sym: &Symbols, c: &Cond) -> String {
|
||||||
match c {
|
match c {
|
||||||
Cond::Compare { lhs, op, rhs } => emit_compare(sym, lhs, *op, rhs),
|
Cond::Compare { lhs, op, rhs } => emit_compare(sym, lhs, *op, rhs),
|
||||||
Cond::Named(name) => {
|
Cond::Named(name) => match sym.condition(name) {
|
||||||
format!("false /* charka: condición 88 no soportada: {name} */")
|
// Un nombre de condición (88) equivale a comparar su dato
|
||||||
}
|
// padre con el valor que la hace verdadera.
|
||||||
|
Some(cn) => emit_cond(
|
||||||
|
sym,
|
||||||
|
&Cond::Compare {
|
||||||
|
lhs: Operand::Data(cn.parent.clone()),
|
||||||
|
op: CmpOp::Eq,
|
||||||
|
rhs: cn.value.clone(),
|
||||||
|
},
|
||||||
|
),
|
||||||
|
None => format!("false /* charka: condición 88 no resuelta: {name} */"),
|
||||||
|
},
|
||||||
Cond::Not(inner) => format!("!({})", emit_cond(sym, inner)),
|
Cond::Not(inner) => format!("!({})", emit_cond(sym, inner)),
|
||||||
Cond::And(a, b) => format!("({}) && ({})", emit_cond(sym, a), emit_cond(sym, b)),
|
Cond::And(a, b) => format!("({}) && ({})", emit_cond(sym, a), emit_cond(sym, b)),
|
||||||
Cond::Or(a, b) => format!("({}) || ({})", emit_cond(sym, a), emit_cond(sym, b)),
|
Cond::Or(a, b) => format!("({}) || ({})", emit_cond(sym, a), emit_cond(sym, b)),
|
||||||
|
|||||||
@@ -39,7 +39,7 @@ use sym::{paragraph_method, Field, FieldKind, Symbols};
|
|||||||
|
|
||||||
/// Transpila un [`Ir`] a un fuente Rust completo (un `main.rs`).
|
/// Transpila un [`Ir`] a un fuente Rust completo (un `main.rs`).
|
||||||
pub fn generate(ir: &Ir) -> String {
|
pub fn generate(ir: &Ir) -> String {
|
||||||
let sym = Symbols::build(&ir.data);
|
let sym = Symbols::build(&ir.model);
|
||||||
let mut em = Emitter::new();
|
let mut em = Emitter::new();
|
||||||
emit_header(&mut em);
|
emit_header(&mut em);
|
||||||
emit_struct(&mut em, &sym);
|
emit_struct(&mut em, &sym);
|
||||||
@@ -142,54 +142,18 @@ fn emit_main(em: &mut Emitter) {
|
|||||||
em.line("}");
|
em.line("}");
|
||||||
}
|
}
|
||||||
|
|
||||||
/// El inicializador de un campo, a partir de su cláusula `VALUE`.
|
/// El inicializador de un campo, a partir de su `VALUE` ya
|
||||||
|
/// normalizado por `charka-ir`.
|
||||||
fn field_init(f: &Field) -> String {
|
fn field_init(f: &Field) -> String {
|
||||||
match &f.kind {
|
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(&numeric_value(f.value.as_deref()))
|
rust_str(&f.init)
|
||||||
),
|
|
||||||
FieldKind::Text { len } => format!(
|
|
||||||
"Text::with_value({len}, {})",
|
|
||||||
rust_str(&text_value(f.value.as_deref()))
|
|
||||||
),
|
),
|
||||||
|
FieldKind::Text { len } => {
|
||||||
|
format!("Text::with_value({len}, {})", rust_str(&f.init))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Normaliza el `VALUE` de un campo numérico a un literal parseable.
|
|
||||||
fn numeric_value(v: Option<&str>) -> String {
|
|
||||||
let Some(raw) = v else {
|
|
||||||
return "0".to_string();
|
|
||||||
};
|
|
||||||
let up = raw.to_uppercase();
|
|
||||||
if matches!(up.as_str(), "ZERO" | "ZEROS" | "ZEROES") {
|
|
||||||
return "0".to_string();
|
|
||||||
}
|
|
||||||
if charka_bcd::Decimal::parse(raw).is_ok() {
|
|
||||||
raw.to_string()
|
|
||||||
} else {
|
|
||||||
"0".to_string()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Normaliza el `VALUE` de un campo de texto. El parser envuelve los
|
|
||||||
/// literales de texto en comillas simples; aquí se desenvuelven.
|
|
||||||
fn text_value(v: Option<&str>) -> String {
|
|
||||||
let Some(raw) = v else {
|
|
||||||
return String::new();
|
|
||||||
};
|
|
||||||
let up = raw.to_uppercase();
|
|
||||||
if matches!(up.as_str(), "SPACE" | "SPACES") {
|
|
||||||
return String::new();
|
|
||||||
}
|
|
||||||
if matches!(up.as_str(), "ZERO" | "ZEROS" | "ZEROES") {
|
|
||||||
return "0".to_string();
|
|
||||||
}
|
|
||||||
if raw.len() >= 2 && raw.starts_with('\'') && raw.ends_with('\'') {
|
|
||||||
raw[1..raw.len() - 1].to_string()
|
|
||||||
} else {
|
|
||||||
raw.to_string()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Asigna a cada párrafo un nombre de método único.
|
/// Asigna a cada párrafo un nombre de método único.
|
||||||
@@ -365,6 +329,19 @@ mod tests {
|
|||||||
assert!(out.contains("} else {"));
|
assert!(out.contains("} else {"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn level_88_condition_resolves_to_a_comparison() {
|
||||||
|
let out = gen("DATA DIVISION.\n\
|
||||||
|
WORKING-STORAGE SECTION.\n\
|
||||||
|
01 WS-FLAG PIC X VALUE 'N'.\n\
|
||||||
|
88 ES-SI VALUE 'Y'.\n\
|
||||||
|
PROCEDURE DIVISION.\n\
|
||||||
|
MAIN.\n\
|
||||||
|
IF ES-SI DISPLAY 'SI' END-IF.\n");
|
||||||
|
// ES-SI equivale a `WS-FLAG = 'Y'` (comparación de texto).
|
||||||
|
assert!(out.contains("cobol_text_cmp(self.ws_flag.display().as_str(), \"Y\").is_eq()"));
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn empty_program_still_compiles_shape() {
|
fn empty_program_still_compiles_shape() {
|
||||||
let out = gen("");
|
let out = gen("");
|
||||||
|
|||||||
@@ -1,8 +1,14 @@
|
|||||||
//! Tabla de símbolos: el modelo de datos COBOL traducido a campos Rust.
|
//! Tabla de símbolos del código generado: los campos del `struct
|
||||||
|
//! Program` y los nombres de condición, derivados del modelo de datos
|
||||||
|
//! resuelto que entrega `charka-ir`.
|
||||||
|
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
|
|
||||||
use charka_ir::DataItem;
|
use charka_ir::{ConditionName, DataModel};
|
||||||
|
|
||||||
|
/// El tipo de campo lo aporta `charka-ir`; se reexporta para que el
|
||||||
|
/// resto del crate lo nombre como `crate::sym::FieldKind`.
|
||||||
|
pub(crate) use charka_ir::FieldKind;
|
||||||
|
|
||||||
/// Un campo del struct `Program` generado.
|
/// Un campo del struct `Program` generado.
|
||||||
pub(crate) struct Field {
|
pub(crate) struct Field {
|
||||||
@@ -12,36 +18,46 @@ pub(crate) struct Field {
|
|||||||
pub ident: String,
|
pub ident: String,
|
||||||
/// Numérico o alfanumérico.
|
/// Numérico o alfanumérico.
|
||||||
pub kind: FieldKind,
|
pub kind: FieldKind,
|
||||||
/// La cláusula `VALUE`, si la hay.
|
/// Valor inicial normalizado (de la cláusula `VALUE`).
|
||||||
pub value: Option<String>,
|
pub init: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// El tipo de un campo elemental.
|
/// Los campos del programa y sus nombres de condición, indexados.
|
||||||
pub(crate) enum FieldKind {
|
|
||||||
/// Campo numérico — se emite como `Num`.
|
|
||||||
Num { int: u8, frac: u8, signed: bool },
|
|
||||||
/// Campo alfanumérico — se emite como `Text`.
|
|
||||||
Text { len: usize },
|
|
||||||
}
|
|
||||||
|
|
||||||
/// El conjunto de campos del programa, indexado por nombre COBOL.
|
|
||||||
pub(crate) struct Symbols {
|
pub(crate) struct Symbols {
|
||||||
pub fields: Vec<Field>,
|
pub fields: Vec<Field>,
|
||||||
by_name: HashMap<String, usize>,
|
by_name: HashMap<String, usize>,
|
||||||
|
conditions: HashMap<String, ConditionName>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Symbols {
|
impl Symbols {
|
||||||
/// Construye la tabla recorriendo el árbol de datos.
|
/// Construye la tabla desde el modelo de datos resuelto.
|
||||||
pub(crate) fn build(data: &[DataItem]) -> Self {
|
pub(crate) fn build(model: &DataModel) -> Self {
|
||||||
let mut fields = Vec::new();
|
let mut fields: Vec<Field> = model
|
||||||
collect(data, &mut fields);
|
.fields
|
||||||
|
.iter()
|
||||||
|
.map(|f| Field {
|
||||||
|
cobol: f.name.clone(),
|
||||||
|
ident: sanitize_ident(&f.name),
|
||||||
|
kind: f.kind,
|
||||||
|
init: f.init.clone(),
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
dedup_idents(&mut fields);
|
dedup_idents(&mut fields);
|
||||||
let by_name = fields
|
let by_name = fields
|
||||||
.iter()
|
.iter()
|
||||||
.enumerate()
|
.enumerate()
|
||||||
.map(|(i, f)| (f.cobol.clone(), i))
|
.map(|(i, f)| (f.cobol.clone(), i))
|
||||||
.collect();
|
.collect();
|
||||||
Self { fields, by_name }
|
let conditions = model
|
||||||
|
.conditions
|
||||||
|
.iter()
|
||||||
|
.map(|c| (c.name.clone(), c.clone()))
|
||||||
|
.collect();
|
||||||
|
Self {
|
||||||
|
fields,
|
||||||
|
by_name,
|
||||||
|
conditions,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Busca un campo por su nombre COBOL (sin distinguir mayúsculas).
|
/// Busca un campo por su nombre COBOL (sin distinguir mayúsculas).
|
||||||
@@ -50,31 +66,10 @@ impl Symbols {
|
|||||||
.get(&cobol.to_uppercase())
|
.get(&cobol.to_uppercase())
|
||||||
.map(|&i| &self.fields[i])
|
.map(|&i| &self.fields[i])
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
/// Recoge los datos elementales del árbol. Los grupos no son campos —
|
/// Busca un nombre de condición (un dato de nivel 88).
|
||||||
/// se recurre en sus hijos. Se saltan niveles 88/66 y los `FILLER`.
|
pub(crate) fn condition(&self, name: &str) -> Option<&ConditionName> {
|
||||||
fn collect(items: &[DataItem], out: &mut Vec<Field>) {
|
self.conditions.get(&name.to_uppercase())
|
||||||
for it in items {
|
|
||||||
if it.level == 88 || it.level == 66 {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if !it.children.is_empty() {
|
|
||||||
collect(&it.children, out);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if it.name == "FILLER" {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
let Some(kind) = classify(it.picture.as_deref()) else {
|
|
||||||
continue;
|
|
||||||
};
|
|
||||||
out.push(Field {
|
|
||||||
cobol: it.name.clone(),
|
|
||||||
ident: sanitize_ident(&it.name),
|
|
||||||
kind,
|
|
||||||
value: it.value.clone(),
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -91,59 +86,6 @@ fn dedup_idents(fields: &mut [Field]) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Clasifica una cláusula PICTURE: alfanumérica si tiene `X`/`A`,
|
|
||||||
/// numérica si `charka-bcd` la parsea; una PICTURE de edición se trata
|
|
||||||
/// como texto de presentación.
|
|
||||||
fn classify(pic: Option<&str>) -> Option<FieldKind> {
|
|
||||||
let up = pic?.to_uppercase();
|
|
||||||
if up.contains('X') || up.contains('A') {
|
|
||||||
return Some(FieldKind::Text {
|
|
||||||
len: pic_width(&up).max(1),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
if let Ok(p) = charka_bcd::Picture::parse(&up) {
|
|
||||||
return Some(FieldKind::Num {
|
|
||||||
int: p.integer_digits,
|
|
||||||
frac: p.fraction_digits,
|
|
||||||
signed: p.signed,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
Some(FieldKind::Text {
|
|
||||||
len: pic_width(&up).max(1),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Cuenta las posiciones de presentación de una PICTURE, expandiendo
|
|
||||||
/// la repetición `C(n)`. `S` y `V` no ocupan posición.
|
|
||||||
fn pic_width(up: &str) -> usize {
|
|
||||||
let chars: Vec<char> = up.chars().collect();
|
|
||||||
let mut i = 0;
|
|
||||||
let mut total = 0usize;
|
|
||||||
while i < chars.len() {
|
|
||||||
let c = chars[i];
|
|
||||||
i += 1;
|
|
||||||
if c == 'S' || c == 'V' {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
let mut count = 1usize;
|
|
||||||
if chars.get(i) == Some(&'(') {
|
|
||||||
i += 1;
|
|
||||||
let start = i;
|
|
||||||
while i < chars.len() && chars[i].is_ascii_digit() {
|
|
||||||
i += 1;
|
|
||||||
}
|
|
||||||
if let Ok(n) = chars[start..i].iter().collect::<String>().parse::<usize>() {
|
|
||||||
count = n;
|
|
||||||
}
|
|
||||||
if chars.get(i) == Some(&')') {
|
|
||||||
i += 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
total += count;
|
|
||||||
}
|
|
||||||
total
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Convierte un nombre COBOL en un identificador Rust válido.
|
/// Convierte un nombre COBOL en un identificador Rust válido.
|
||||||
fn sanitize_ident(name: &str) -> String {
|
fn sanitize_ident(name: &str) -> String {
|
||||||
let mut s: String = name
|
let mut s: String = name
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ description = "charka-ir — representación intermedia: el AST de charka-parser
|
|||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
charka-parser = { path = "../charka-parser" }
|
charka-parser = { path = "../charka-parser" }
|
||||||
|
charka-bcd = { path = "../charka-bcd" }
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
charka-lexer = { path = "../charka-lexer" }
|
charka-lexer = { path = "../charka-lexer" }
|
||||||
|
|||||||
@@ -8,9 +8,12 @@ pub use charka_parser::{DataItem, Token};
|
|||||||
pub struct Ir {
|
pub struct Ir {
|
||||||
/// El `PROGRAM-ID` ("" si el programa no lo declara).
|
/// El `PROGRAM-ID` ("" si el programa no lo declara).
|
||||||
pub program_id: String,
|
pub program_id: String,
|
||||||
/// El modelo de datos — el árbol de [`DataItem`] tal cual lo
|
/// El árbol de [`DataItem`] tal cual lo produjo `charka-parser`,
|
||||||
/// produjo `charka-parser`. Sirve de tabla de símbolos.
|
/// con su estructura de grupos.
|
||||||
pub data: Vec<DataItem>,
|
pub data: Vec<DataItem>,
|
||||||
|
/// El modelo de datos resuelto: los datos elementales aplanados y
|
||||||
|
/// los nombres de condición (nivel 88).
|
||||||
|
pub model: crate::model::DataModel,
|
||||||
/// Los párrafos del PROCEDURE, con sus statements ya tipados.
|
/// Los párrafos del PROCEDURE, con sus statements ya tipados.
|
||||||
pub procedures: Vec<Procedure>,
|
pub procedures: Vec<Procedure>,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,10 +27,12 @@ mod ast;
|
|||||||
mod cursor;
|
mod cursor;
|
||||||
mod expr;
|
mod expr;
|
||||||
mod kw;
|
mod kw;
|
||||||
|
mod model;
|
||||||
mod stmt;
|
mod stmt;
|
||||||
|
|
||||||
pub use ast::*;
|
pub use ast::*;
|
||||||
pub use charka_parser::Program;
|
pub use charka_parser::Program;
|
||||||
|
pub use model::{resolve_data, ConditionName, DataModel, Field, FieldKind};
|
||||||
|
|
||||||
use cursor::Cursor;
|
use cursor::Cursor;
|
||||||
|
|
||||||
@@ -55,6 +57,7 @@ pub fn lower(program: &Program) -> Ir {
|
|||||||
Ir {
|
Ir {
|
||||||
program_id: program.program_id.clone().unwrap_or_default(),
|
program_id: program.program_id.clone().unwrap_or_default(),
|
||||||
data: program.data.clone(),
|
data: program.data.clone(),
|
||||||
|
model: model::resolve_data(&program.data),
|
||||||
procedures,
|
procedures,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,293 @@
|
|||||||
|
//! El modelo de datos resuelto: el árbol de `DataItem` aplanado a una
|
||||||
|
//! lista de campos elementales y a los nombres de condición (nivel 88).
|
||||||
|
//!
|
||||||
|
//! Es la fuente única de verdad sobre «qué tipo de campo describe una
|
||||||
|
//! PICTURE» — `charka-codegen` y `charka-shadow` la consumen en vez de
|
||||||
|
//! reimplementar cada uno la clasificación.
|
||||||
|
|
||||||
|
use charka_bcd::{Decimal, Picture};
|
||||||
|
use charka_parser::DataItem;
|
||||||
|
|
||||||
|
use crate::ast::Operand;
|
||||||
|
|
||||||
|
/// El tipo resuelto de un dato elemental.
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
pub enum FieldKind {
|
||||||
|
/// Numérico: dígitos enteros, fraccionarios y si lleva signo.
|
||||||
|
Num { int: u8, frac: u8, signed: bool },
|
||||||
|
/// Alfanumérico de longitud fija.
|
||||||
|
Text { len: usize },
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Un dato elemental del programa, listo para materializarse.
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
|
pub struct Field {
|
||||||
|
/// Nombre COBOL, en mayúsculas.
|
||||||
|
pub name: String,
|
||||||
|
/// Numérico o alfanumérico.
|
||||||
|
pub kind: FieldKind,
|
||||||
|
/// Valor inicial ya normalizado (de la cláusula `VALUE`).
|
||||||
|
pub init: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Un nombre de condición — un dato de nivel 88. `IF <name>` equivale
|
||||||
|
/// a comparar `parent` con `value`.
|
||||||
|
#[derive(Debug, Clone, PartialEq)]
|
||||||
|
pub struct ConditionName {
|
||||||
|
/// Nombre del 88, en mayúsculas.
|
||||||
|
pub name: String,
|
||||||
|
/// El dato sobre el que se prueba la condición.
|
||||||
|
pub parent: String,
|
||||||
|
/// El valor que hace verdadera la condición.
|
||||||
|
pub value: Operand,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// El modelo de datos resuelto de un programa.
|
||||||
|
#[derive(Debug, Clone, PartialEq, Default)]
|
||||||
|
pub struct DataModel {
|
||||||
|
/// Los datos elementales, en orden de declaración.
|
||||||
|
pub fields: Vec<Field>,
|
||||||
|
/// Los nombres de condición (nivel 88).
|
||||||
|
pub conditions: Vec<ConditionName>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl DataModel {
|
||||||
|
/// Busca un campo por su nombre COBOL (sin distinguir mayúsculas).
|
||||||
|
pub fn field(&self, name: &str) -> Option<&Field> {
|
||||||
|
let up = name.to_uppercase();
|
||||||
|
self.fields.iter().find(|f| f.name == up)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Busca un nombre de condición.
|
||||||
|
pub fn condition(&self, name: &str) -> Option<&ConditionName> {
|
||||||
|
let up = name.to_uppercase();
|
||||||
|
self.conditions.iter().find(|c| c.name == up)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Aplana el árbol de datos en un [`DataModel`].
|
||||||
|
pub fn resolve_data(data: &[DataItem]) -> DataModel {
|
||||||
|
let mut model = DataModel::default();
|
||||||
|
walk(data, &mut model);
|
||||||
|
model
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Recorre el árbol: registra los 88 como condiciones sobre su dato
|
||||||
|
/// padre, recurre en los grupos y emite los datos elementales.
|
||||||
|
fn walk(items: &[DataItem], model: &mut DataModel) {
|
||||||
|
for it in items {
|
||||||
|
if it.level == 66 || it.level == 88 {
|
||||||
|
// Los 88 los registra su dato padre; los 66 se omiten.
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
// Los hijos de nivel 88 son condiciones sobre este dato.
|
||||||
|
for child in &it.children {
|
||||||
|
if child.level == 88 {
|
||||||
|
model.conditions.push(ConditionName {
|
||||||
|
name: child.name.to_uppercase(),
|
||||||
|
parent: it.name.to_uppercase(),
|
||||||
|
value: condition_value(child.value.as_deref()),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Un dato con hijos «reales» (no 88/66) es un grupo.
|
||||||
|
let is_group = it.children.iter().any(|c| c.level != 88 && c.level != 66);
|
||||||
|
if is_group {
|
||||||
|
walk(&it.children, model);
|
||||||
|
} else if it.name != "FILLER" {
|
||||||
|
if let Some(kind) = classify(it.picture.as_deref()) {
|
||||||
|
let init = match kind {
|
||||||
|
FieldKind::Num { .. } => numeric_value(it.value.as_deref()),
|
||||||
|
FieldKind::Text { .. } => text_value(it.value.as_deref()),
|
||||||
|
};
|
||||||
|
model.fields.push(Field {
|
||||||
|
name: it.name.to_uppercase(),
|
||||||
|
kind,
|
||||||
|
init,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Clasifica una cláusula PICTURE: alfanumérica si tiene `X`/`A`,
|
||||||
|
/// numérica si `charka-bcd` la parsea; una PICTURE de edición se trata
|
||||||
|
/// como texto de presentación.
|
||||||
|
fn classify(pic: Option<&str>) -> Option<FieldKind> {
|
||||||
|
let up = pic?.to_uppercase();
|
||||||
|
if up.contains('X') || up.contains('A') {
|
||||||
|
return Some(FieldKind::Text {
|
||||||
|
len: pic_width(&up).max(1),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if let Ok(p) = Picture::parse(&up) {
|
||||||
|
return Some(FieldKind::Num {
|
||||||
|
int: p.integer_digits,
|
||||||
|
frac: p.fraction_digits,
|
||||||
|
signed: p.signed,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
Some(FieldKind::Text {
|
||||||
|
len: pic_width(&up).max(1),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Cuenta las posiciones de presentación de una PICTURE, expandiendo
|
||||||
|
/// la repetición `C(n)`. `S` y `V` no ocupan posición.
|
||||||
|
fn pic_width(up: &str) -> usize {
|
||||||
|
let chars: Vec<char> = up.chars().collect();
|
||||||
|
let mut i = 0;
|
||||||
|
let mut total = 0usize;
|
||||||
|
while i < chars.len() {
|
||||||
|
let c = chars[i];
|
||||||
|
i += 1;
|
||||||
|
if c == 'S' || c == 'V' {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let mut count = 1usize;
|
||||||
|
if chars.get(i) == Some(&'(') {
|
||||||
|
i += 1;
|
||||||
|
let start = i;
|
||||||
|
while i < chars.len() && chars[i].is_ascii_digit() {
|
||||||
|
i += 1;
|
||||||
|
}
|
||||||
|
if let Ok(n) = chars[start..i].iter().collect::<String>().parse::<usize>() {
|
||||||
|
count = n;
|
||||||
|
}
|
||||||
|
if chars.get(i) == Some(&')') {
|
||||||
|
i += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
total += count;
|
||||||
|
}
|
||||||
|
total
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Normaliza el `VALUE` de un campo numérico a un literal parseable.
|
||||||
|
fn numeric_value(v: Option<&str>) -> String {
|
||||||
|
let Some(raw) = v else {
|
||||||
|
return "0".to_string();
|
||||||
|
};
|
||||||
|
if matches!(raw.to_uppercase().as_str(), "ZERO" | "ZEROS" | "ZEROES") {
|
||||||
|
return "0".to_string();
|
||||||
|
}
|
||||||
|
if Decimal::parse(raw).is_ok() {
|
||||||
|
raw.to_string()
|
||||||
|
} else {
|
||||||
|
"0".to_string()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Normaliza el `VALUE` de un campo de texto. El parser envuelve los
|
||||||
|
/// literales de texto en comillas simples; aquí se desenvuelven.
|
||||||
|
fn text_value(v: Option<&str>) -> String {
|
||||||
|
let Some(raw) = v else {
|
||||||
|
return String::new();
|
||||||
|
};
|
||||||
|
let up = raw.to_uppercase();
|
||||||
|
if matches!(up.as_str(), "SPACE" | "SPACES") {
|
||||||
|
return String::new();
|
||||||
|
}
|
||||||
|
if matches!(up.as_str(), "ZERO" | "ZEROS" | "ZEROES") {
|
||||||
|
return "0".to_string();
|
||||||
|
}
|
||||||
|
if raw.len() >= 2 && raw.starts_with('\'') && raw.ends_with('\'') {
|
||||||
|
raw[1..raw.len() - 1].to_string()
|
||||||
|
} else {
|
||||||
|
raw.to_string()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// El valor de un nivel 88 como [`Operand`]: literal de texto entre
|
||||||
|
/// comillas, número, o (si no es ninguno) texto crudo.
|
||||||
|
fn condition_value(value: Option<&str>) -> Operand {
|
||||||
|
let Some(raw) = value else {
|
||||||
|
return Operand::Num("0".to_string());
|
||||||
|
};
|
||||||
|
if raw.len() >= 2 && raw.starts_with('\'') && raw.ends_with('\'') {
|
||||||
|
return Operand::Str(raw[1..raw.len() - 1].to_string());
|
||||||
|
}
|
||||||
|
if Decimal::parse(raw).is_ok() {
|
||||||
|
Operand::Num(raw.to_string())
|
||||||
|
} else {
|
||||||
|
Operand::Str(raw.to_string())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use charka_lexer::{lex, SourceFormat};
|
||||||
|
|
||||||
|
fn model_of(src: &str) -> DataModel {
|
||||||
|
let toks = lex(src, SourceFormat::Free).unwrap();
|
||||||
|
let program = charka_parser::parse(&toks).unwrap();
|
||||||
|
resolve_data(&program.data)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn flattens_elementary_fields() {
|
||||||
|
let m = model_of(
|
||||||
|
"DATA DIVISION.\n\
|
||||||
|
01 WS-N PIC 9(3) VALUE 7.\n\
|
||||||
|
01 WS-T PIC X(4) VALUE 'AB'.\n",
|
||||||
|
);
|
||||||
|
assert_eq!(m.fields.len(), 2);
|
||||||
|
assert_eq!(
|
||||||
|
m.field("WS-N").unwrap().kind,
|
||||||
|
FieldKind::Num {
|
||||||
|
int: 3,
|
||||||
|
frac: 0,
|
||||||
|
signed: false
|
||||||
|
}
|
||||||
|
);
|
||||||
|
assert_eq!(m.field("WS-N").unwrap().init, "7");
|
||||||
|
assert_eq!(m.field("WS-T").unwrap().kind, FieldKind::Text { len: 4 });
|
||||||
|
assert_eq!(m.field("WS-T").unwrap().init, "AB");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn group_items_are_not_fields_but_their_children_are() {
|
||||||
|
let m = model_of(
|
||||||
|
"DATA DIVISION.\n\
|
||||||
|
01 WS-REC.\n\
|
||||||
|
05 WS-A PIC 9(2).\n\
|
||||||
|
05 WS-B PIC X(3).\n",
|
||||||
|
);
|
||||||
|
assert!(m.field("WS-REC").is_none());
|
||||||
|
assert!(m.field("WS-A").is_some());
|
||||||
|
assert!(m.field("WS-B").is_some());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn level_88_becomes_a_condition_on_its_parent() {
|
||||||
|
let m = model_of(
|
||||||
|
"DATA DIVISION.\n\
|
||||||
|
01 WS-FLAG PIC X VALUE 'N'.\n\
|
||||||
|
88 ES-SI VALUE 'Y'.\n\
|
||||||
|
88 ES-NO VALUE 'N'.\n",
|
||||||
|
);
|
||||||
|
// El dato con hijos 88 sigue siendo un campo.
|
||||||
|
assert!(m.field("WS-FLAG").is_some());
|
||||||
|
let si = m.condition("ES-SI").unwrap();
|
||||||
|
assert_eq!(si.parent, "WS-FLAG");
|
||||||
|
assert_eq!(si.value, Operand::Str("Y".into()));
|
||||||
|
assert_eq!(
|
||||||
|
m.condition("ES-NO").unwrap().value,
|
||||||
|
Operand::Str("N".into())
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn numeric_level_88_value() {
|
||||||
|
let m = model_of(
|
||||||
|
"DATA DIVISION.\n\
|
||||||
|
01 WS-COD PIC 9(2) VALUE 0.\n\
|
||||||
|
88 ES-OK VALUE 0.\n",
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
m.condition("ES-OK").unwrap().value,
|
||||||
|
Operand::Num("0".into())
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,12 +1,9 @@
|
|||||||
//! El estado de los datos durante la ejecución sombra: el árbol de
|
//! El estado de los datos durante la ejecución sombra: el modelo de
|
||||||
//! `DataItem` del IR se aplana a un mapa de campos vivos.
|
//! datos resuelto de `charka-ir` se materializa en campos vivos.
|
||||||
//!
|
|
||||||
//! La clasificación de PICTURE refleja la de `charka-codegen` — un
|
|
||||||
//! futuro refactor la unificaría en `charka-runtime`.
|
|
||||||
|
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
|
|
||||||
use charka_ir::DataItem;
|
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: numérico o alfanumérico.
|
||||||
@@ -15,114 +12,17 @@ pub(crate) enum Cell {
|
|||||||
Text(Text),
|
Text(Text),
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Aplana el árbol de datos en un mapa `nombre COBOL → campo`.
|
/// Materializa los campos del modelo en un mapa `nombre → campo`.
|
||||||
pub(crate) fn build_fields(data: &[DataItem]) -> HashMap<String, Cell> {
|
pub(crate) fn build_fields(model: &DataModel) -> HashMap<String, Cell> {
|
||||||
let mut map = HashMap::new();
|
let mut map = HashMap::new();
|
||||||
collect(data, &mut map);
|
for f in &model.fields {
|
||||||
|
let cell = match f.kind {
|
||||||
|
FieldKind::Num { int, frac, signed } => {
|
||||||
|
Cell::Num(Num::with_value(Picture::new(int, frac, signed), &f.init))
|
||||||
|
}
|
||||||
|
FieldKind::Text { len } => Cell::Text(Text::with_value(len, &f.init)),
|
||||||
|
};
|
||||||
|
map.entry(f.name.clone()).or_insert(cell);
|
||||||
|
}
|
||||||
map
|
map
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Recorre el árbol: los grupos no son campos (se recurre en sus
|
|
||||||
/// hijos); se saltan los niveles 88/66 y los `FILLER`.
|
|
||||||
fn collect(items: &[DataItem], map: &mut HashMap<String, Cell>) {
|
|
||||||
for it in items {
|
|
||||||
if it.level == 88 || it.level == 66 {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if !it.children.is_empty() {
|
|
||||||
collect(&it.children, map);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if it.name == "FILLER" {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if let Some(cell) = make_cell(it.picture.as_deref(), it.value.as_deref()) {
|
|
||||||
map.entry(it.name.to_uppercase()).or_insert(cell);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Construye un campo desde su PICTURE y su cláusula `VALUE`.
|
|
||||||
fn make_cell(pic: Option<&str>, value: Option<&str>) -> Option<Cell> {
|
|
||||||
let up = pic?.to_uppercase();
|
|
||||||
if up.contains('X') || up.contains('A') {
|
|
||||||
return Some(Cell::Text(Text::with_value(
|
|
||||||
pic_width(&up).max(1),
|
|
||||||
&text_value(value),
|
|
||||||
)));
|
|
||||||
}
|
|
||||||
if let Ok(p) = Picture::parse(&up) {
|
|
||||||
return Some(Cell::Num(Num::with_value(p, &numeric_value(value))));
|
|
||||||
}
|
|
||||||
// PICTURE de edición → campo de texto de presentación.
|
|
||||||
Some(Cell::Text(Text::with_value(
|
|
||||||
pic_width(&up).max(1),
|
|
||||||
&text_value(value),
|
|
||||||
)))
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Cuenta las posiciones de presentación de una PICTURE, expandiendo
|
|
||||||
/// la repetición `C(n)`. `S` y `V` no ocupan posición.
|
|
||||||
fn pic_width(up: &str) -> usize {
|
|
||||||
let chars: Vec<char> = up.chars().collect();
|
|
||||||
let mut i = 0;
|
|
||||||
let mut total = 0usize;
|
|
||||||
while i < chars.len() {
|
|
||||||
let c = chars[i];
|
|
||||||
i += 1;
|
|
||||||
if c == 'S' || c == 'V' {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
let mut count = 1usize;
|
|
||||||
if chars.get(i) == Some(&'(') {
|
|
||||||
i += 1;
|
|
||||||
let start = i;
|
|
||||||
while i < chars.len() && chars[i].is_ascii_digit() {
|
|
||||||
i += 1;
|
|
||||||
}
|
|
||||||
if let Ok(n) = chars[start..i].iter().collect::<String>().parse::<usize>() {
|
|
||||||
count = n;
|
|
||||||
}
|
|
||||||
if chars.get(i) == Some(&')') {
|
|
||||||
i += 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
total += count;
|
|
||||||
}
|
|
||||||
total
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Normaliza el `VALUE` de un campo numérico a un literal parseable.
|
|
||||||
fn numeric_value(v: Option<&str>) -> String {
|
|
||||||
let Some(raw) = v else {
|
|
||||||
return "0".to_string();
|
|
||||||
};
|
|
||||||
if matches!(raw.to_uppercase().as_str(), "ZERO" | "ZEROS" | "ZEROES") {
|
|
||||||
return "0".to_string();
|
|
||||||
}
|
|
||||||
if charka_runtime::Decimal::parse(raw).is_ok() {
|
|
||||||
raw.to_string()
|
|
||||||
} else {
|
|
||||||
"0".to_string()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Normaliza el `VALUE` de un campo de texto. El parser envuelve los
|
|
||||||
/// literales de texto en comillas simples; aquí se desenvuelven.
|
|
||||||
fn text_value(v: Option<&str>) -> String {
|
|
||||||
let Some(raw) = v else {
|
|
||||||
return String::new();
|
|
||||||
};
|
|
||||||
let up = raw.to_uppercase();
|
|
||||||
if matches!(up.as_str(), "SPACE" | "SPACES") {
|
|
||||||
return String::new();
|
|
||||||
}
|
|
||||||
if matches!(up.as_str(), "ZERO" | "ZEROS" | "ZEROES") {
|
|
||||||
return "0".to_string();
|
|
||||||
}
|
|
||||||
if raw.len() >= 2 && raw.starts_with('\'') && raw.ends_with('\'') {
|
|
||||||
raw[1..raw.len() - 1].to_string()
|
|
||||||
} else {
|
|
||||||
raw.to_string()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -8,7 +8,8 @@
|
|||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
|
|
||||||
use charka_ir::{
|
use charka_ir::{
|
||||||
BinOp, CmpOp, Cond, Expr, Figurative, Ir, Operand, Perform, PerformControl, PerformTarget, Stmt,
|
BinOp, CmpOp, Cond, ConditionName, Expr, Figurative, Ir, Operand, Perform, PerformControl,
|
||||||
|
PerformTarget, Stmt,
|
||||||
};
|
};
|
||||||
use charka_runtime::{cobol_text_cmp, Decimal, Rounding};
|
use charka_runtime::{cobol_text_cmp, Decimal, Rounding};
|
||||||
|
|
||||||
@@ -37,6 +38,7 @@ pub(crate) struct Machine<'a> {
|
|||||||
ir: &'a Ir,
|
ir: &'a Ir,
|
||||||
fields: HashMap<String, Cell>,
|
fields: HashMap<String, Cell>,
|
||||||
para_index: HashMap<String, usize>,
|
para_index: HashMap<String, usize>,
|
||||||
|
conditions: HashMap<String, ConditionName>,
|
||||||
pub output: Vec<String>,
|
pub output: Vec<String>,
|
||||||
budget: u64,
|
budget: u64,
|
||||||
pub step_limit_hit: bool,
|
pub step_limit_hit: bool,
|
||||||
@@ -50,10 +52,17 @@ impl<'a> Machine<'a> {
|
|||||||
for (i, proc) in ir.procedures.iter().enumerate() {
|
for (i, proc) in ir.procedures.iter().enumerate() {
|
||||||
para_index.entry(proc.name.to_uppercase()).or_insert(i);
|
para_index.entry(proc.name.to_uppercase()).or_insert(i);
|
||||||
}
|
}
|
||||||
|
let conditions = ir
|
||||||
|
.model
|
||||||
|
.conditions
|
||||||
|
.iter()
|
||||||
|
.map(|c| (c.name.clone(), c.clone()))
|
||||||
|
.collect();
|
||||||
Self {
|
Self {
|
||||||
ir,
|
ir,
|
||||||
fields: build_fields(&ir.data),
|
fields: build_fields(&ir.model),
|
||||||
para_index,
|
para_index,
|
||||||
|
conditions,
|
||||||
output: Vec::new(),
|
output: Vec::new(),
|
||||||
budget: STEP_BUDGET,
|
budget: STEP_BUDGET,
|
||||||
step_limit_hit: false,
|
step_limit_hit: false,
|
||||||
@@ -437,7 +446,12 @@ impl<'a> Machine<'a> {
|
|||||||
CmpOp::Ge => ord.is_ge(),
|
CmpOp::Ge => ord.is_ge(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Cond::Named(_) => false, // nombres de condición (88): no soportado
|
Cond::Named(name) => match self.conditions.get(&name.to_uppercase()) {
|
||||||
|
// Un nombre de condición (88): el dato padre igual al
|
||||||
|
// valor que la hace verdadera.
|
||||||
|
Some(cn) => self.operands_equal(&Operand::Data(cn.parent.clone()), &cn.value),
|
||||||
|
None => false,
|
||||||
|
},
|
||||||
Cond::Not(inner) => !self.eval_cond(inner),
|
Cond::Not(inner) => !self.eval_cond(inner),
|
||||||
Cond::And(a, b) => self.eval_cond(a) && self.eval_cond(b),
|
Cond::And(a, b) => self.eval_cond(a) && self.eval_cond(b),
|
||||||
Cond::Or(a, b) => self.eval_cond(a) || self.eval_cond(b),
|
Cond::Or(a, b) => self.eval_cond(a) || self.eval_cond(b),
|
||||||
|
|||||||
@@ -118,6 +118,7 @@ mod tests {
|
|||||||
corpus_test!(corpus_07_clasificar, "07-clasificar");
|
corpus_test!(corpus_07_clasificar, "07-clasificar");
|
||||||
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");
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn empty_source_runs_clean() {
|
fn empty_source_runs_clean() {
|
||||||
|
|||||||
@@ -0,0 +1,29 @@
|
|||||||
|
* corpus charka — nivel 5: nombres de condición (nivel 88)
|
||||||
|
IDENTIFICATION DIVISION.
|
||||||
|
PROGRAM-ID. SEMAFORO.
|
||||||
|
DATA DIVISION.
|
||||||
|
WORKING-STORAGE SECTION.
|
||||||
|
01 WS-LUZ PIC X(5) VALUE 'ROJO'.
|
||||||
|
88 ES-PARE VALUE 'ROJO'.
|
||||||
|
88 ES-SIGA VALUE 'VERDE'.
|
||||||
|
01 WS-CODIGO PIC 9(1) VALUE 0.
|
||||||
|
88 ES-EXITO VALUE 0.
|
||||||
|
PROCEDURE DIVISION.
|
||||||
|
MAIN.
|
||||||
|
IF ES-PARE
|
||||||
|
DISPLAY 'LUZ ROJA: DETENERSE'
|
||||||
|
END-IF.
|
||||||
|
IF ES-EXITO
|
||||||
|
DISPLAY 'CODIGO OK'
|
||||||
|
END-IF.
|
||||||
|
MOVE 'VERDE' TO WS-LUZ.
|
||||||
|
MOVE 9 TO WS-CODIGO.
|
||||||
|
IF ES-SIGA
|
||||||
|
DISPLAY 'LUZ VERDE: AVANZAR'
|
||||||
|
END-IF.
|
||||||
|
IF ES-EXITO
|
||||||
|
DISPLAY 'CODIGO OK'
|
||||||
|
ELSE
|
||||||
|
DISPLAY 'CODIGO DE ERROR'
|
||||||
|
END-IF.
|
||||||
|
STOP RUN.
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
LUZ ROJA: DETENERSE
|
||||||
|
CODIGO OK
|
||||||
|
LUZ VERDE: AVANZAR
|
||||||
|
CODIGO DE ERROR
|
||||||
@@ -18,6 +18,7 @@ salida correcta, una línea por `DISPLAY`.
|
|||||||
| `07-clasificar` | 5 | `IF` anidado, condiciones con `AND` |
|
| `07-clasificar` | 5 | `IF` anidado, condiciones con `AND` |
|
||||||
| `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` |
|
||||||
|
|
||||||
## 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): nombres de condición (nivel 88) + modelo de datos compartido
|
||||||
|
|
||||||
|
`IF ES-VALIDO` — los nombres de condición de COBOL, que antes el
|
||||||
|
transpilador evaluaba siempre como `false`. Y, de paso, se elimina una
|
||||||
|
duplicación: la resolución del modelo de datos.
|
||||||
|
|
||||||
|
- `charka-ir` gana un módulo `model`: `resolve_data(&[DataItem]) ->
|
||||||
|
DataModel` aplana el árbol de datos a campos elementales (`Field` con
|
||||||
|
`FieldKind`) y a nombres de condición (`ConditionName`). El `Ir`
|
||||||
|
ahora lleva un campo `model`. Es la fuente única de verdad sobre la
|
||||||
|
clasificación de PICTURE.
|
||||||
|
- `charka-codegen` y `charka-shadow` consumen `ir.model` en vez de
|
||||||
|
reimplementar cada uno la clasificación numérico/alfanumérico, el
|
||||||
|
ancho de PICTURE y la normalización de `VALUE`. `charka-codegen` ya
|
||||||
|
no depende de `charka-bcd`.
|
||||||
|
- `Cond::Named` (un nivel 88) se resuelve a `padre = valor`: el codegen
|
||||||
|
emite la comparación, el intérprete sombra la evalúa.
|
||||||
|
- Corregido de paso: un dato con hijos de nivel 88 (p. ej.
|
||||||
|
`01 WS-FLAG PIC X. 88 ES-SI VALUE 'Y'.`) antes se perdía como si
|
||||||
|
fuera un grupo; ahora se reconoce como campo elemental.
|
||||||
|
- Corpus: programa nuevo `10-condicion` (un semáforo con 88 de texto y
|
||||||
|
de número, en `IF` e `IF/ELSE`). Verificado: intérprete y crate
|
||||||
|
compilado dan la misma salida.
|
||||||
|
|
||||||
### feat(charka): EVALUATE — el case de COBOL
|
### feat(charka): EVALUATE — el case de COBOL
|
||||||
|
|
||||||
`EVALUATE` atraviesa el pipeline entero — antes el parser lo guardaba
|
`EVALUATE` atraviesa el pipeline entero — antes el parser lo guardaba
|
||||||
|
|||||||
Reference in New Issue
Block a user