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:
@@ -10,7 +10,6 @@ description = "charka-codegen — emisión de Rust desde el IR de charka: el mod
|
||||
|
||||
[dependencies]
|
||||
charka-ir = { path = "../charka-ir" }
|
||||
charka-bcd = { path = "../charka-bcd" }
|
||||
|
||||
[dev-dependencies]
|
||||
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 {
|
||||
match c {
|
||||
Cond::Compare { lhs, op, rhs } => emit_compare(sym, lhs, *op, rhs),
|
||||
Cond::Named(name) => {
|
||||
format!("false /* charka: condición 88 no soportada: {name} */")
|
||||
}
|
||||
Cond::Named(name) => match sym.condition(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::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)),
|
||||
|
||||
@@ -39,7 +39,7 @@ use sym::{paragraph_method, Field, FieldKind, Symbols};
|
||||
|
||||
/// Transpila un [`Ir`] a un fuente Rust completo (un `main.rs`).
|
||||
pub fn generate(ir: &Ir) -> String {
|
||||
let sym = Symbols::build(&ir.data);
|
||||
let sym = Symbols::build(&ir.model);
|
||||
let mut em = Emitter::new();
|
||||
emit_header(&mut em);
|
||||
emit_struct(&mut em, &sym);
|
||||
@@ -142,53 +142,17 @@ fn emit_main(em: &mut Emitter) {
|
||||
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 {
|
||||
match &f.kind {
|
||||
FieldKind::Num { int, frac, signed } => format!(
|
||||
"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()))
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
/// 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()
|
||||
FieldKind::Text { len } => {
|
||||
format!("Text::with_value({len}, {})", rust_str(&f.init))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -365,6 +329,19 @@ mod tests {
|
||||
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]
|
||||
fn empty_program_still_compiles_shape() {
|
||||
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 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.
|
||||
pub(crate) struct Field {
|
||||
@@ -12,36 +18,46 @@ pub(crate) struct Field {
|
||||
pub ident: String,
|
||||
/// Numérico o alfanumérico.
|
||||
pub kind: FieldKind,
|
||||
/// La cláusula `VALUE`, si la hay.
|
||||
pub value: Option<String>,
|
||||
/// Valor inicial normalizado (de la cláusula `VALUE`).
|
||||
pub init: String,
|
||||
}
|
||||
|
||||
/// El tipo de un campo elemental.
|
||||
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.
|
||||
/// Los campos del programa y sus nombres de condición, indexados.
|
||||
pub(crate) struct Symbols {
|
||||
pub fields: Vec<Field>,
|
||||
by_name: HashMap<String, usize>,
|
||||
conditions: HashMap<String, ConditionName>,
|
||||
}
|
||||
|
||||
impl Symbols {
|
||||
/// Construye la tabla recorriendo el árbol de datos.
|
||||
pub(crate) fn build(data: &[DataItem]) -> Self {
|
||||
let mut fields = Vec::new();
|
||||
collect(data, &mut fields);
|
||||
/// Construye la tabla desde el modelo de datos resuelto.
|
||||
pub(crate) fn build(model: &DataModel) -> Self {
|
||||
let mut fields: Vec<Field> = model
|
||||
.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);
|
||||
let by_name = fields
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(i, f)| (f.cobol.clone(), i))
|
||||
.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).
|
||||
@@ -50,31 +66,10 @@ impl Symbols {
|
||||
.get(&cobol.to_uppercase())
|
||||
.map(|&i| &self.fields[i])
|
||||
}
|
||||
}
|
||||
|
||||
/// Recoge los datos elementales del árbol. Los grupos no son campos —
|
||||
/// se recurre en sus hijos. Se saltan niveles 88/66 y los `FILLER`.
|
||||
fn collect(items: &[DataItem], out: &mut Vec<Field>) {
|
||||
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(),
|
||||
});
|
||||
/// Busca un nombre de condición (un dato de nivel 88).
|
||||
pub(crate) fn condition(&self, name: &str) -> Option<&ConditionName> {
|
||||
self.conditions.get(&name.to_uppercase())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
fn sanitize_ident(name: &str) -> String {
|
||||
let mut s: String = name
|
||||
|
||||
Reference in New Issue
Block a user