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>
This commit is contained in:
@@ -240,9 +240,7 @@ fn collect_unknowns(stmts: &[Stmt], out: &mut Vec<String>) {
|
|||||||
collect_unknowns(other, out);
|
collect_unknowns(other, out);
|
||||||
}
|
}
|
||||||
Stmt::Read {
|
Stmt::Read {
|
||||||
at_end,
|
at_end, not_at_end, ..
|
||||||
not_at_end,
|
|
||||||
..
|
|
||||||
} => {
|
} => {
|
||||||
collect_unknowns(at_end, out);
|
collect_unknowns(at_end, out);
|
||||||
collect_unknowns(not_at_end, out);
|
collect_unknowns(not_at_end, out);
|
||||||
|
|||||||
@@ -189,8 +189,8 @@ que corre el `Ir` directamente sobre `charka-runtime`, sin compilar.
|
|||||||
|
|
||||||
## El corpus
|
## El corpus
|
||||||
|
|
||||||
`crates/modules/charka/corpus/` — 18 programas COBOL graduados
|
`crates/modules/charka/corpus/` — 19 programas COBOL graduados
|
||||||
(`01-hola` … `18-fichero`), cada uno con su `.expected`. Ejercita el
|
(`01-hola` … `19-reporte`), cada uno con su `.expected`. Ejercita el
|
||||||
pipeline completo de punta a punta. Ver su `README.md`.
|
pipeline completo de punta a punta. Ver su `README.md`.
|
||||||
|
|
||||||
## La CLI
|
## La CLI
|
||||||
@@ -203,9 +203,9 @@ no transpilados.
|
|||||||
|
|
||||||
## Estado
|
## Estado
|
||||||
|
|
||||||
Pipeline **completo** — `charka-bcd` (22 tests), `charka-lexer` (17),
|
Pipeline **completo** — `charka-bcd` (22 tests), `charka-lexer` (18),
|
||||||
`charka-parser` (15), `charka-ir` (17), `charka-runtime` (17),
|
`charka-parser` (17), `charka-ir` (30), `charka-runtime` (22),
|
||||||
`charka-codegen` (14), `charka-shadow` (11) y la CLI `charka` (4)
|
`charka-codegen` (26), `charka-shadow` (24) y la CLI `charka` (4)
|
||||||
implementados y verdes. COBOL → Rust corre de punta a punta, validado
|
implementados y verdes. COBOL → Rust corre de punta a punta, validado
|
||||||
contra el corpus. El crate que genera `scaffold` compila y su salida
|
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
|
coincide con la del intérprete sombra — las dos rutas de ejecución
|
||||||
|
|||||||
@@ -474,6 +474,21 @@ mod tests {
|
|||||||
assert!(out.contains("self.file_arch.close();"));
|
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]
|
#[test]
|
||||||
fn empty_program_still_compiles_shape() {
|
fn empty_program_still_compiles_shape() {
|
||||||
let out = gen("");
|
let out = gen("");
|
||||||
|
|||||||
@@ -8,7 +8,8 @@ use charka_ir::{
|
|||||||
|
|
||||||
use crate::emit::Emitter;
|
use crate::emit::Emitter;
|
||||||
use crate::expr::{
|
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};
|
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]) {
|
fn emit_move(em: &mut Emitter, sym: &Symbols, from: &Operand, to: &[Operand]) {
|
||||||
for t in to {
|
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) {
|
match field_ref(sym, t) {
|
||||||
Some((lref, FieldKind::Num { .. })) => {
|
Some((lref, FieldKind::Num { .. })) => {
|
||||||
em.line(&format!("{lref}.store({});", operand_decimal(sym, from)));
|
em.line(&format!("{lref}.store({});", operand_decimal(sym, from)));
|
||||||
|
|||||||
@@ -22,6 +22,8 @@ pub(crate) struct Field {
|
|||||||
pub init: String,
|
pub init: String,
|
||||||
/// Si es una tabla (`OCCURS n`), su número de elementos.
|
/// Si es una tabla (`OCCURS n`), su número de elementos.
|
||||||
pub occurs: Option<u32>,
|
pub occurs: Option<u32>,
|
||||||
|
/// Si es un campo de edición, su PICTURE.
|
||||||
|
pub edit: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Un fichero del programa generado.
|
/// Un fichero del programa generado.
|
||||||
@@ -62,6 +64,7 @@ impl Symbols {
|
|||||||
kind: f.kind,
|
kind: f.kind,
|
||||||
init: f.init.clone(),
|
init: f.init.clone(),
|
||||||
occurs: f.occurs,
|
occurs: f.occurs,
|
||||||
|
edit: f.edit.clone(),
|
||||||
})
|
})
|
||||||
.collect();
|
.collect();
|
||||||
dedup_idents(&mut fields);
|
dedup_idents(&mut fields);
|
||||||
|
|||||||
@@ -31,6 +31,9 @@ pub struct Field {
|
|||||||
/// Si es una tabla (`OCCURS n`), su número de elementos; `None`
|
/// Si es una tabla (`OCCURS n`), su número de elementos; `None`
|
||||||
/// para un dato escalar.
|
/// para un dato escalar.
|
||||||
pub occurs: Option<u32>,
|
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
|
/// 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);
|
produced.extend(members);
|
||||||
} else if it.name != "FILLER" {
|
} 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 {
|
let init = match kind {
|
||||||
FieldKind::Num { .. } => numeric_value(it.value.as_deref()),
|
FieldKind::Num { .. } => numeric_value(it.value.as_deref()),
|
||||||
FieldKind::Text { .. } => text_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,
|
kind,
|
||||||
init,
|
init,
|
||||||
occurs: it.occurs,
|
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`,
|
/// 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
|
/// numérica si `charka-bcd` la parsea; una PICTURE de edición se trata
|
||||||
/// como texto de presentación.
|
/// 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();
|
let up = pic?.to_uppercase();
|
||||||
if up.contains('X') || up.contains('A') {
|
if up.contains('X') || up.contains('A') {
|
||||||
return Some(FieldKind::Text {
|
return Some((
|
||||||
len: pic_width(&up).max(1),
|
FieldKind::Text {
|
||||||
});
|
len: pic_width(&up).max(1),
|
||||||
|
},
|
||||||
|
None,
|
||||||
|
));
|
||||||
}
|
}
|
||||||
if let Ok(p) = Picture::parse(&up) {
|
if let Ok(p) = Picture::parse(&up) {
|
||||||
return Some(FieldKind::Num {
|
return Some((
|
||||||
int: p.integer_digits,
|
FieldKind::Num {
|
||||||
frac: p.fraction_digits,
|
int: p.integer_digits,
|
||||||
signed: p.signed,
|
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),
|
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
|
/// Cuenta las posiciones de presentación de una PICTURE, expandiendo
|
||||||
|
|||||||
@@ -149,8 +149,20 @@ fn lex_line(content: &str, line: u32, base_col: u32, out: &mut Vec<Token>) -> Re
|
|||||||
});
|
});
|
||||||
i = next;
|
i = next;
|
||||||
} else if c == '.' {
|
} 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 {
|
out.push(Token {
|
||||||
kind: TokenKind::Period,
|
kind: if is_separator {
|
||||||
|
TokenKind::Period
|
||||||
|
} else {
|
||||||
|
TokenKind::Symbol
|
||||||
|
},
|
||||||
text: ".".into(),
|
text: ".".into(),
|
||||||
line,
|
line,
|
||||||
col,
|
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]
|
#[test]
|
||||||
fn two_and_one_char_operators() {
|
fn two_and_one_char_operators() {
|
||||||
let toks = kinds("A <= B >= C <> D ** E + F", SourceFormat::Free);
|
let toks = kinds("A <= B >= C <> D ** E + F", SourceFormat::Free);
|
||||||
|
|||||||
@@ -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"), " ");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -17,11 +17,13 @@
|
|||||||
|
|
||||||
#![forbid(unsafe_code)]
|
#![forbid(unsafe_code)]
|
||||||
|
|
||||||
|
mod edited;
|
||||||
mod file;
|
mod file;
|
||||||
mod num;
|
mod num;
|
||||||
mod text;
|
mod text;
|
||||||
|
|
||||||
pub use charka_bcd::{Decimal, Picture, Rounding};
|
pub use charka_bcd::{Decimal, Picture, Rounding};
|
||||||
|
pub use edited::format_edited;
|
||||||
pub use file::CobFile;
|
pub use file::CobFile;
|
||||||
pub use num::Num;
|
pub use num::Num;
|
||||||
pub use text::Text;
|
pub use text::Text;
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ use charka_ir::{
|
|||||||
BinOp, CmpOp, Cond, ConditionName, Expr, Figurative, FileMode, InspectOp, Ir, Operand, Perform,
|
BinOp, CmpOp, Cond, ConditionName, Expr, Figurative, FileMode, InspectOp, Ir, Operand, Perform,
|
||||||
PerformControl, PerformTarget, Stmt, WhenTest,
|
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};
|
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).
|
/// `MOVE from` a un solo destino (escalar o elemento de tabla).
|
||||||
fn do_move(&mut self, from: &Operand, target: &Operand) {
|
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 {
|
let Some((key, idx)) = self.resolve(target) else {
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -127,6 +127,7 @@ mod tests {
|
|||||||
corpus_test!(corpus_16_bandera, "16-bandera");
|
corpus_test!(corpus_16_bandera, "16-bandera");
|
||||||
corpus_test!(corpus_17_rangopar, "17-rangopar");
|
corpus_test!(corpus_17_rangopar, "17-rangopar");
|
||||||
corpus_test!(corpus_18_fichero, "18-fichero");
|
corpus_test!(corpus_18_fichero, "18-fichero");
|
||||||
|
corpus_test!(corpus_19_reporte, "19-reporte");
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn empty_source_runs_clean() {
|
fn empty_source_runs_clean() {
|
||||||
|
|||||||
@@ -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.
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
[ 1,234.50]
|
||||||
|
[ 7.00]
|
||||||
|
[ 0.00]
|
||||||
|
[234,567]
|
||||||
|
[ 89]
|
||||||
@@ -27,6 +27,7 @@ salida correcta, una línea por `DISPLAY`.
|
|||||||
| `16-bandera` | 5 | `SET` de nombres de condición (nivel 88) a `TRUE` |
|
| `16-bandera` | 5 | `SET` de nombres de condición (nivel 88) a `TRUE` |
|
||||||
| `17-rangopar` | 5 | `PERFORM ... THRU` — un rango de párrafos |
|
| `17-rangopar` | 5 | `PERFORM ... THRU` — un rango de párrafos |
|
||||||
| `18-fichero` | 7 | E/S de ficheros: `SELECT`/`FD`/`OPEN`/`READ`/`WRITE`|
|
| `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
|
## Formato
|
||||||
|
|
||||||
|
|||||||
@@ -3,6 +3,29 @@
|
|||||||
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): 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
|
### feat(charka): E/S de ficheros — SELECT / FD / OPEN / READ / WRITE / CLOSE
|
||||||
|
|
||||||
El gran hueco que faltaba para el COBOL real: el procesamiento de
|
El gran hueco que faltaba para el COBOL real: el procesamiento de
|
||||||
|
|||||||
Reference in New Issue
Block a user