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) { 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, + /// Si es un campo de edición, su PICTURE. + pub edit: Option, } /// 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, + /// 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, } /// Un nombre de condición — un dato de nivel 88. `IF ` equivale @@ -126,7 +129,7 @@ fn walk(items: &[DataItem], model: &mut DataModel) -> Vec { } 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 { kind, init, occurs: it.occurs, + edit, }); } } @@ -147,23 +151,37 @@ fn walk(items: &[DataItem], model: &mut DataModel) -> Vec { /// 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 { +fn classify(pic: Option<&str>) -> Option<(FieldKind, Option)> { 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) -> 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 = 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