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:
@@ -1,12 +1,9 @@
|
||||
//! El estado de los datos durante la ejecución sombra: el árbol de
|
||||
//! `DataItem` del IR se aplana a un mapa de campos vivos.
|
||||
//!
|
||||
//! La clasificación de PICTURE refleja la de `charka-codegen` — un
|
||||
//! futuro refactor la unificaría en `charka-runtime`.
|
||||
//! El estado de los datos durante la ejecución sombra: el modelo de
|
||||
//! datos resuelto de `charka-ir` se materializa en campos vivos.
|
||||
|
||||
use std::collections::HashMap;
|
||||
|
||||
use charka_ir::DataItem;
|
||||
use charka_ir::{DataModel, FieldKind};
|
||||
use charka_runtime::{Num, Picture, Text};
|
||||
|
||||
/// Un campo vivo: numérico o alfanumérico.
|
||||
@@ -15,114 +12,17 @@ pub(crate) enum Cell {
|
||||
Text(Text),
|
||||
}
|
||||
|
||||
/// Aplana el árbol de datos en un mapa `nombre COBOL → campo`.
|
||||
pub(crate) fn build_fields(data: &[DataItem]) -> HashMap<String, Cell> {
|
||||
/// Materializa los campos del modelo en un mapa `nombre → campo`.
|
||||
pub(crate) fn build_fields(model: &DataModel) -> HashMap<String, Cell> {
|
||||
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
|
||||
}
|
||||
|
||||
/// 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 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};
|
||||
|
||||
@@ -37,6 +38,7 @@ pub(crate) struct Machine<'a> {
|
||||
ir: &'a Ir,
|
||||
fields: HashMap<String, Cell>,
|
||||
para_index: HashMap<String, usize>,
|
||||
conditions: HashMap<String, ConditionName>,
|
||||
pub output: Vec<String>,
|
||||
budget: u64,
|
||||
pub step_limit_hit: bool,
|
||||
@@ -50,10 +52,17 @@ impl<'a> Machine<'a> {
|
||||
for (i, proc) in ir.procedures.iter().enumerate() {
|
||||
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 {
|
||||
ir,
|
||||
fields: build_fields(&ir.data),
|
||||
fields: build_fields(&ir.model),
|
||||
para_index,
|
||||
conditions,
|
||||
output: Vec::new(),
|
||||
budget: STEP_BUDGET,
|
||||
step_limit_hit: false,
|
||||
@@ -437,7 +446,12 @@ impl<'a> Machine<'a> {
|
||||
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::And(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_08_varying, "08-varying");
|
||||
corpus_test!(corpus_09_evaluar, "09-evaluar");
|
||||
corpus_test!(corpus_10_condicion, "10-condicion");
|
||||
|
||||
#[test]
|
||||
fn empty_source_runs_clean() {
|
||||
|
||||
Reference in New Issue
Block a user