tawasuyu

tawasuyu/brahman

Rust 492TOML 194Markdown 53JSON 176939 Definiciones

feat(charka): PICTURE de edición — Z, coma de millares y punto decimal

El formateo de informes de COBOL: supresión de ceros a la izquierda,
coma de millares e inserción del punto decimal. Rebanada vertical.

- charka-lexer: el punto separador exige un espacio detrás; un punto
  pegado a un carácter (ZZ9.99) ya no es terminador, sino símbolo —
  el parser lo reensambla dentro de la cláusula PICTURE.
- charka-runtime: format_edited(valor, pic) — 9, Z, coma, punto, B.
- charka-ir: Field::edit guarda la PICTURE; el campo es texto.
- charka-codegen / charka-shadow: MOVE a un campo de edición pasa por
  format_edited antes de almacenar.
- Corpus: 19-reporte. Sombra y crate compilado dan la misma salida.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

634a43006ac4257288af40320b5360e9f157ab09 · por sergio · (hace 5 meses) · padre b3278bdb0 · ver el árbol en este commit

Delta semántico · 17

crates/modules/charka/charka-codegen/src/lib.rs

crates/modules/charka/charka-codegen/src/stmt.rs

crates/modules/charka/charka-codegen/src/sym.rs

  • cambió de firma def Field
  • cambió por dentro def build

crates/modules/charka/charka-ir/src/model.rs

  • cambió de firma struct Field pub
  • cambió de firma fn classify
  • cambió por dentro fn walk

crates/modules/charka/charka-lexer/src/lib.rs

crates/modules/charka/charka-runtime/src/edited.rs

crates/modules/charka/charka-runtime/src/lib.rs

crates/modules/charka/charka-shadow/src/interp.rs

15 archivos · +264 −23

diff --git a/crates/apps/charka/src/main.rs b/crates/apps/charka/src/main.rs
index aada11e..2983faf 100644
--- a/crates/apps/charka/src/main.rs
+++ b/crates/apps/charka/src/main.rs
@@ -240,9 +240,7 @@ fn collect_unknowns(stmts: &[Stmt], out: &mut Vec<String>) {
                 collect_unknowns(other, out);
             }
             Stmt::Read {
-                at_end,
-                not_at_end,
-                ..
+                at_end, not_at_end, ..
             } => {
                 collect_unknowns(at_end, out);
                 collect_unknowns(not_at_end, out);
diff --git a/crates/modules/charka/SDD.md b/crates/modules/charka/SDD.md
index 9e2e894..7e40570 100644
--- a/crates/modules/charka/SDD.md
+++ b/crates/modules/charka/SDD.md
@@ -189,8 +189,8 @@ que corre el `Ir` directamente sobre `charka-runtime`, sin compilar.
 
 ## El corpus
 
-`crates/modules/charka/corpus/` — 18 programas COBOL graduados
-(`01-hola` … `18-fichero`), cada uno con su `.expected`. Ejercita el
+`crates/modules/charka/corpus/` — 19 programas COBOL graduados
+(`01-hola` … `19-reporte`), cada uno con su `.expected`. Ejercita el
 pipeline completo de punta a punta. Ver su `README.md`.
 
 ## La CLI
@@ -203,9 +203,9 @@ no transpilados.
 
 ## Estado
 
-Pipeline **completo** — `charka-bcd` (22 tests), `charka-lexer` (17),
-`charka-parser` (15), `charka-ir` (17), `charka-runtime` (17),
-`charka-codegen` (14), `charka-shadow` (11) y la CLI `charka` (4)
+Pipeline **completo** — `charka-bcd` (22 tests), `charka-lexer` (18),
+`charka-parser` (17), `charka-ir` (30), `charka-runtime` (22),
+`charka-codegen` (26), `charka-shadow` (24) y la CLI `charka` (4)
 implementados y verdes. COBOL → Rust corre de punta a punta, validado
 contra el corpus. El crate que genera `scaffold` compila y su salida
 coincide con la del intérprete sombra — las dos rutas de ejecución
diff --git a/crates/modules/charka/charka-codegen/src/lib.rs b/crates/modules/charka/charka-codegen/src/lib.rs
index a728d42..8fb39c3 100644
--- a/crates/modules/charka/charka-codegen/src/lib.rs
+++ b/crates/modules/charka/charka-codegen/src/lib.rs
@@ -474,6 +474,21 @@ mod tests {
         assert!(out.contains("self.file_arch.close();"));
     }
 
+    #[test]
+    fn edited_field_is_text_and_move_formats_it() {
+        let out = gen("DATA DIVISION.\n\
+             WORKING-STORAGE SECTION.\n\
+             01 WS-N PIC 9(5).\n\
+             01 WS-E PIC Z,ZZ9.99.\n\
+             PROCEDURE DIVISION.\n\
+             MAIN.\n\
+                 MOVE WS-N TO WS-E.\n");
+        // El campo de edición se materializa como texto de presentación.
+        assert!(out.contains("ws_e: Text,"));
+        // El MOVE pasa por `format_edited` con la PICTURE de edición.
+        assert!(out.contains("self.ws_e.store(&format_edited(self.ws_n.value(), \"Z,ZZ9.99\"));"));
+    }
+
     #[test]
     fn empty_program_still_compiles_shape() {
         let out = gen("");
diff --git a/crates/modules/charka/charka-codegen/src/stmt.rs b/crates/modules/charka/charka-codegen/src/stmt.rs
index 1b1f3d7..7804cdf 100644
--- a/crates/modules/charka/charka-codegen/src/stmt.rs
+++ b/crates/modules/charka/charka-codegen/src/stmt.rs
@@ -8,7 +8,8 @@ use charka_ir::{
 
 use crate::emit::Emitter;
 use crate::expr::{
-    emit_cond, emit_expr, field_ref, figurative_fill, operand_decimal, operand_display, operand_str,
+    emit_cond, emit_expr, field_ref, figurative_fill, operand_decimal, operand_display,
+    operand_str, rust_str,
 };
 use crate::sym::{paragraph_method, FieldKind, Symbols};
 
@@ -136,6 +137,21 @@ fn emit_store(em: &mut Emitter, sym: &Symbols, target: &Operand, value: &str, ro
 
 fn emit_move(em: &mut Emitter, sym: &Symbols, from: &Operand, to: &[Operand]) {
     for t in to {
+        // Un destino con PICTURE de edición formatea el valor numérico.
+        if let Operand::Data(name) = t {
+            if let Some(pic) = sym.lookup(name).and_then(|f| f.edit.clone()) {
+                let ident = sym
+                    .lookup(name)
+                    .map(|f| f.ident.clone())
+                    .unwrap_or_default();
+                em.line(&format!(
+                    "self.{ident}.store(&format_edited({}, {}));",
+                    operand_decimal(sym, from),
+                    rust_str(&pic)
+                ));
+                continue;
+            }
+        }
         match field_ref(sym, t) {
             Some((lref, FieldKind::Num { .. })) => {
                 em.line(&format!("{lref}.store({});", operand_decimal(sym, from)));
diff --git a/crates/modules/charka/charka-codegen/src/sym.rs b/crates/modules/charka/charka-codegen/src/sym.rs
index 1fb56fa..7b5bc69 100644
--- a/crates/modules/charka/charka-codegen/src/sym.rs
+++ b/crates/modules/charka/charka-codegen/src/sym.rs
@@ -22,6 +22,8 @@ pub(crate) struct Field {
     pub init: String,
     /// Si es una tabla (`OCCURS n`), su número de elementos.
     pub occurs: Option<u32>,
+    /// Si es un campo de edición, su PICTURE.
+    pub edit: Option<String>,
 }
 
 /// Un fichero del programa generado.
@@ -62,6 +64,7 @@ impl Symbols {
                 kind: f.kind,
                 init: f.init.clone(),
                 occurs: f.occurs,
+                edit: f.edit.clone(),
             })
             .collect();
         dedup_idents(&mut fields);
diff --git a/crates/modules/charka/charka-ir/src/model.rs b/crates/modules/charka/charka-ir/src/model.rs
index 44ec964..b9f82a2 100644
--- a/crates/modules/charka/charka-ir/src/model.rs
+++ b/crates/modules/charka/charka-ir/src/model.rs
@@ -31,6 +31,9 @@ pub struct Field {
     /// Si es una tabla (`OCCURS n`), su número de elementos; `None`
     /// para un dato escalar.
     pub occurs: Option<u32>,
+    /// Si es un campo de edición (`ZZ,ZZ9.99`), su PICTURE — para
+    /// formatear el valor al moverlo. El campo se almacena como texto.
+    pub edit: Option<String>,
 }
 
 /// Un nombre de condición — un dato de nivel 88. `IF <name>` equivale
@@ -126,7 +129,7 @@ fn walk(items: &[DataItem], model: &mut DataModel) -> Vec<String> {
             }
             produced.extend(members);
         } else if it.name != "FILLER" {
-            if let Some(kind) = classify(it.picture.as_deref()) {
+            if let Some((kind, edit)) = 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()),
@@ -137,6 +140,7 @@ fn walk(items: &[DataItem], model: &mut DataModel) -> Vec<String> {
                     kind,
                     init,
                     occurs: it.occurs,
+                    edit,
                 });
             }
         }
@@ -147,23 +151,37 @@ fn walk(items: &[DataItem], model: &mut DataModel) -> Vec<String> {
 /// 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> {
+fn classify(pic: Option<&str>) -> Option<(FieldKind, Option<String>)> {
     let up = pic?.to_uppercase();
     if up.contains('X') || up.contains('A') {
-        return Some(FieldKind::Text {
-            len: pic_width(&up).max(1),
-        });
+        return Some((
+            FieldKind::Text {
+                len: pic_width(&up).max(1),
+            },
+            None,
+        ));
     }
     if let Ok(p) = Picture::parse(&up) {
-        return Some(FieldKind::Num {
-            int: p.integer_digits,
-            frac: p.fraction_digits,
-            signed: p.signed,
-        });
+        return Some((
+            FieldKind::Num {
+                int: p.integer_digits,
+                frac: p.fraction_digits,
+                signed: p.signed,
+            },
+            None,
+        ));
     }
-    Some(FieldKind::Text {
+    // ¿PICTURE de edición? — tiene un símbolo de edición y un dígito.
+    let kind = FieldKind::Text {
         len: pic_width(&up).max(1),
-    })
+    };
+    let has_edit = up.contains(['Z', ',', '.', '*']);
+    let has_digit = up.contains(['9', 'Z']);
+    if has_edit && has_digit {
+        Some((kind, Some(up)))
+    } else {
+        Some((kind, None))
+    }
 }
 
 /// Cuenta las posiciones de presentación de una PICTURE, expandiendo
diff --git a/crates/modules/charka/charka-lexer/src/lib.rs b/crates/modules/charka/charka-lexer/src/lib.rs
index 017ee34..84d52bf 100644
--- a/crates/modules/charka/charka-lexer/src/lib.rs
+++ b/crates/modules/charka/charka-lexer/src/lib.rs
@@ -149,8 +149,20 @@ fn lex_line(content: &str, line: u32, base_col: u32, out: &mut Vec<Token>) -> Re
             });
             i = next;
         } else if c == '.' {
+            // El separador de sentencia COBOL siempre lleva un espacio
+            // (o el fin de línea) detrás. Un punto pegado a un carácter
+            // —`ZZ9.99`— no es separador: pertenece a una PICTURE de
+            // edición y se emite como símbolo para que el parser lo
+            // reensamble dentro de la cláusula.
+            let is_separator = chars
+                .get(i + 1)
+                .map_or(true, |n| n.is_whitespace());
             out.push(Token {
-                kind: TokenKind::Period,
+                kind: if is_separator {
+                    TokenKind::Period
+                } else {
+                    TokenKind::Symbol
+                },
                 text: ".".into(),
                 line,
                 col,
@@ -329,6 +341,19 @@ mod tests {
         );
     }
 
+    #[test]
+    fn period_inside_an_edit_picture_is_not_a_separator() {
+        // El punto de `ZZ9.99` va pegado a un dígito: es símbolo, no
+        // terminador. El punto final, con espacio detrás, sí termina.
+        let toks = kinds("PIC Z,ZZ9.99 .", SourceFormat::Free);
+        let dots: Vec<TokenKind> = toks
+            .iter()
+            .filter(|(_, t)| t == ".")
+            .map(|(k, _)| *k)
+            .collect();
+        assert_eq!(dots, vec![TokenKind::Symbol, TokenKind::Period]);
+    }
+
     #[test]
     fn two_and_one_char_operators() {
         let toks = kinds("A <= B >= C <> D ** E + F", SourceFormat::Free);
diff --git a/crates/modules/charka/charka-runtime/src/edited.rs b/crates/modules/charka/charka-runtime/src/edited.rs
new file mode 100644
index 0000000..6fd34df
--- /dev/null
+++ b/crates/modules/charka/charka-runtime/src/edited.rs
@@ -0,0 +1,103 @@
+//! `format_edited` — el formateo de un valor numérico según una
+//! PICTURE de edición (`ZZ,ZZ9.99`).
+
+use charka_bcd::{Decimal, Rounding};
+
+/// Formatea `value` según una PICTURE de edición. Soporta `9` (dígito),
+/// `Z` (dígito con supresión de ceros a la izquierda), `,` (coma de
+/// millares, en blanco dentro de la zona suprimida), `.` (punto
+/// decimal) y `B` (espacio). El signo se descarta.
+pub fn format_edited(value: Decimal, pic: &str) -> String {
+    let up = pic.to_uppercase();
+    let (int_pic, frac_pic) = match up.split_once('.') {
+        Some((a, b)) => (a, b),
+        None => (up.as_str(), ""),
+    };
+    let count_digits = |s: &str| s.chars().filter(|c| *c == '9' || *c == 'Z').count();
+    let int_digits = count_digits(int_pic);
+    let frac_digits = count_digits(frac_pic);
+    let total = int_digits + frac_digits;
+
+    // Los dígitos del valor, con exactamente `frac_digits` decimales.
+    let mantissa = value
+        .rescale(frac_digits as u8, Rounding::Truncate)
+        .mantissa()
+        .unsigned_abs();
+    let mut digits = mantissa.to_string();
+    if digits.len() < total {
+        digits = format!("{}{}", "0".repeat(total - digits.len()), digits);
+    } else if digits.len() > total {
+        digits = digits[digits.len() - total..].to_string();
+    }
+    let int_part = &digits.as_bytes()[..int_digits];
+    let frac_part = &digits.as_bytes()[int_digits..];
+
+    let mut out = String::new();
+    let mut di = 0;
+    let mut seen = false;
+    for ch in int_pic.chars() {
+        match ch {
+            '9' => {
+                out.push(int_part[di] as char);
+                di += 1;
+                seen = true;
+            }
+            'Z' => {
+                let d = int_part[di];
+                di += 1;
+                if seen || d != b'0' {
+                    out.push(d as char);
+                    seen = true;
+                } else {
+                    out.push(' ');
+                }
+            }
+            ',' => out.push(if seen { ',' } else { ' ' }),
+            'B' => out.push(' '),
+            other => out.push(other),
+        }
+    }
+    if !frac_pic.is_empty() {
+        out.push('.');
+        let mut fi = 0;
+        for ch in frac_pic.chars() {
+            match ch {
+                '9' | 'Z' => {
+                    out.push(frac_part[fi] as char);
+                    fi += 1;
+                }
+                other => out.push(other),
+            }
+        }
+    }
+    out
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+
+    fn dec(s: &str) -> Decimal {
+        Decimal::parse(s).unwrap()
+    }
+
+    #[test]
+    fn suppresses_leading_zeros_and_inserts_commas() {
+        assert_eq!(format_edited(dec("1234.5"), "Z,ZZ9.99"), "1,234.50");
+        assert_eq!(format_edited(dec("7"), "Z,ZZ9.99"), "    7.00");
+        assert_eq!(format_edited(dec("0"), "Z,ZZ9.99"), "    0.00");
+    }
+
+    #[test]
+    fn comma_in_suppressed_zone_is_blank() {
+        // El millar va en blanco si no hay dígito significativo antes.
+        assert_eq!(format_edited(dec("42"), "ZZ,ZZ9"), "    42");
+        assert_eq!(format_edited(dec("12345"), "ZZ,ZZ9"), "12,345");
+    }
+
+    #[test]
+    fn nine_positions_always_show() {
+        assert_eq!(format_edited(dec("0"), "999"), "000");
+        assert_eq!(format_edited(dec("0"), "ZZZ"), "   ");
+    }
+}
diff --git a/crates/modules/charka/charka-runtime/src/lib.rs b/crates/modules/charka/charka-runtime/src/lib.rs
index 0d86920..965b730 100644
--- a/crates/modules/charka/charka-runtime/src/lib.rs
+++ b/crates/modules/charka/charka-runtime/src/lib.rs
@@ -17,11 +17,13 @@
 
 #![forbid(unsafe_code)]
 
+mod edited;
 mod file;
 mod num;
 mod text;
 
 pub use charka_bcd::{Decimal, Picture, Rounding};
+pub use edited::format_edited;
 pub use file::CobFile;
 pub use num::Num;
 pub use text::Text;
diff --git a/crates/modules/charka/charka-shadow/src/interp.rs b/crates/modules/charka/charka-shadow/src/interp.rs
index 5eb0e22..a94004a 100644
--- a/crates/modules/charka/charka-shadow/src/interp.rs
+++ b/crates/modules/charka/charka-shadow/src/interp.rs
@@ -11,7 +11,7 @@ use charka_ir::{
     BinOp, CmpOp, Cond, ConditionName, Expr, Figurative, FileMode, InspectOp, Ir, Operand, Perform,
     PerformControl, PerformTarget, Stmt, WhenTest,
 };
-use charka_runtime::{cobol_text_cmp, CobFile, Decimal, Num, Rounding, Text};
+use charka_runtime::{cobol_text_cmp, format_edited, CobFile, Decimal, Num, Rounding, Text};
 
 use crate::field::{build_fields, Cell};
 
@@ -531,6 +531,15 @@ impl<'a> Machine<'a> {
 
     /// `MOVE from` a un solo destino (escalar o elemento de tabla).
     fn do_move(&mut self, from: &Operand, target: &Operand) {
+        // Un destino con PICTURE de edición formatea el valor numérico.
+        if let Operand::Data(name) = target {
+            if let Some(pic) = self.ir.model.field(name).and_then(|f| f.edit.clone()) {
+                let value = self.eval_decimal(from);
+                let text = format_edited(value, &pic);
+                self.store_text(target, &text);
+                return;
+            }
+        }
         let Some((key, idx)) = self.resolve(target) else {
             return;
         };
diff --git a/crates/modules/charka/charka-shadow/src/lib.rs b/crates/modules/charka/charka-shadow/src/lib.rs
index 4329f06..ea7cae6 100644
--- a/crates/modules/charka/charka-shadow/src/lib.rs
+++ b/crates/modules/charka/charka-shadow/src/lib.rs
@@ -127,6 +127,7 @@ mod tests {
     corpus_test!(corpus_16_bandera, "16-bandera");
     corpus_test!(corpus_17_rangopar, "17-rangopar");
     corpus_test!(corpus_18_fichero, "18-fichero");
+    corpus_test!(corpus_19_reporte, "19-reporte");
 
     #[test]
     fn empty_source_runs_clean() {
diff --git a/crates/modules/charka/corpus/19-reporte.cob b/crates/modules/charka/corpus/19-reporte.cob
new file mode 100644
index 0000000..79816a7
--- /dev/null
+++ b/crates/modules/charka/corpus/19-reporte.cob
@@ -0,0 +1,22 @@
+* corpus charka — nivel 19: PICTURE de edición (Z, coma, punto)
+IDENTIFICATION DIVISION.
+PROGRAM-ID. REPORTE.
+DATA DIVISION.
+WORKING-STORAGE SECTION.
+01 WS-A      PIC 9(5)V99 VALUE 1234.5.
+01 WS-B      PIC 9(5)V99 VALUE 7.
+01 WS-EDIT   PIC Z,ZZZ,ZZ9.99.
+01 WS-CONT   PIC ZZZ,ZZ9.
+PROCEDURE DIVISION.
+MAIN.
+    MOVE WS-A TO WS-EDIT.
+    DISPLAY '[' WS-EDIT ']'.
+    MOVE WS-B TO WS-EDIT.
+    DISPLAY '[' WS-EDIT ']'.
+    MOVE 0 TO WS-EDIT.
+    DISPLAY '[' WS-EDIT ']'.
+    MOVE 234567 TO WS-CONT.
+    DISPLAY '[' WS-CONT ']'.
+    MOVE 89 TO WS-CONT.
+    DISPLAY '[' WS-CONT ']'.
+    STOP RUN.
diff --git a/crates/modules/charka/corpus/19-reporte.expected b/crates/modules/charka/corpus/19-reporte.expected
new file mode 100644
index 0000000..a0bdfc7
--- /dev/null
+++ b/crates/modules/charka/corpus/19-reporte.expected
@@ -0,0 +1,5 @@
+[    1,234.50]
+[        7.00]
+[        0.00]
+[234,567]
+[     89]
diff --git a/crates/modules/charka/corpus/README.md b/crates/modules/charka/corpus/README.md
index 639c2da..f17198b 100644
--- a/crates/modules/charka/corpus/README.md
+++ b/crates/modules/charka/corpus/README.md
@@ -27,6 +27,7 @@ salida correcta, una línea por `DISPLAY`.
 | `16-bandera`        | 5     | `SET` de nombres de condición (nivel 88) a `TRUE`  |
 | `17-rangopar`       | 5     | `PERFORM ... THRU` — un rango de párrafos          |
 | `18-fichero`        | 7     | E/S de ficheros: `SELECT`/`FD`/`OPEN`/`READ`/`WRITE`|
+| `19-reporte`        | 6     | PICTURE de edición (`Z,ZZ9.99`) — formato de informe|
 
 ## Formato
 
diff --git a/docs/changelog/charka.md b/docs/changelog/charka.md
index 4c77013..5257a78 100644
--- a/docs/changelog/charka.md
+++ b/docs/changelog/charka.md
@@ -3,6 +3,29 @@
 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.
 
+### feat(charka): PICTURE de edición — `Z`, coma de millares y punto decimal
+
+Las PICTURE de edición (`Z,ZZ9.99`) — el formateo de informes de
+COBOL: supresión de ceros a la izquierda, coma de millares e inserción
+del punto decimal. Una rebanada vertical por el pipeline.
+
+- `charka-lexer`: el punto separador de COBOL siempre lleva un espacio
+  detrás. Un punto pegado a un carácter —`ZZ9.99`— ya no se toma como
+  terminador de sentencia: se emite como símbolo, para que el parser
+  lo reensamble dentro de la cláusula PICTURE.
+- `charka-runtime`: `format_edited(valor, pic)` — formatea un decimal
+  según una PICTURE de edición: `9` (dígito), `Z` (dígito con
+  supresión de ceros), `,` (coma, en blanco en la zona suprimida),
+  `.` (punto decimal) y `B` (espacio).
+- `charka-ir`: el modelo de datos distingue una PICTURE de edición —
+  `Field::edit` guarda la PICTURE; el campo se materializa como texto
+  de presentación.
+- `charka-codegen` y `charka-shadow`: un `MOVE` a un campo de edición
+  pasa el valor por `format_edited` antes de almacenarlo.
+- Corpus: programa nuevo `19-reporte` — formatea importes y contadores
+  con `Z,ZZZ,ZZ9.99` y `ZZZ,ZZ9`. Verificado: el intérprete sombra y
+  el crate compilado dan la misma salida.
+
 ### feat(charka): E/S de ficheros — SELECT / FD / OPEN / READ / WRITE / CLOSE
 
 El gran hueco que faltaba para el COBOL real: el procesamiento de