From 85156c15090f0ae5116737b7c07cddae23cbf7f3 Mon Sep 17 00:00:00 2001 From: sergio Date: Thu, 21 May 2026 20:27:28 +0000 Subject: [PATCH] =?UTF-8?q?feat(charka):=20charka-runtime=20=E2=80=94=20so?= =?UTF-8?q?porte=20de=20ejecuci=C3=B3n=20(campos=20Num=20y=20Text)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit El soporte que los programas COBOL transpilados enlazan. charka-codegen emitirá Rust que llama a esta biblioteca, no Rust autónomo. - Num: campo numérico (PIC 9(5)V99) — un Decimal conformado a su Picture. store trunca a la escala declarada, store_rounded redondea; al desbordar la parte entera conserva los dígitos de bajo orden (el ON SIZE ERROR de COBOL sin cláusula). display da los dígitos con relleno de ceros y signo. - Text: campo alfanumérico (PIC X(n)) de longitud fija — store justifica a la izquierda y rellena/trunca; fill mueve figurativas. - cobol_text_cmp: comparación alfanumérica con relleno de espacios. - Reexporta Decimal/Picture/Rounding de charka-bcd. Construido antes que charka-codegen (la nota de orden del plan los listaba al revés): el codegen emite contra esta API. 17 tests; fmt + clippy limpios. Co-Authored-By: Claude Opus 4.7 --- Cargo.lock | 7 + Cargo.toml | 1 + crates/modules/charka/SDD.md | 36 +++- .../modules/charka/charka-runtime/Cargo.toml | 12 ++ .../modules/charka/charka-runtime/src/lib.rs | 72 ++++++++ .../modules/charka/charka-runtime/src/num.rs | 158 ++++++++++++++++++ .../modules/charka/charka-runtime/src/text.rs | 110 ++++++++++++ docs/changelog/charka.md | 24 +++ 8 files changed, 414 insertions(+), 6 deletions(-) create mode 100644 crates/modules/charka/charka-runtime/Cargo.toml create mode 100644 crates/modules/charka/charka-runtime/src/lib.rs create mode 100644 crates/modules/charka/charka-runtime/src/num.rs create mode 100644 crates/modules/charka/charka-runtime/src/text.rs diff --git a/Cargo.lock b/Cargo.lock index 543eb95..b7b8723 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2338,6 +2338,13 @@ dependencies = [ "thiserror 2.0.18", ] +[[package]] +name = "charka-runtime" +version = "0.1.0" +dependencies = [ + "charka-bcd", +] + [[package]] name = "chasqui-card" version = "0.1.0" diff --git a/Cargo.toml b/Cargo.toml index 4756036..5c76c2f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -160,6 +160,7 @@ members = [ "crates/modules/charka/charka-lexer", "crates/modules/charka/charka-parser", "crates/modules/charka/charka-ir", + "crates/modules/charka/charka-runtime", # ============================================================ # modules/mirada/ — Compositor Wayland diff --git a/crates/modules/charka/SDD.md b/crates/modules/charka/SDD.md index 8e8c52c..f83fa41 100644 --- a/crates/modules/charka/SDD.md +++ b/crates/modules/charka/SDD.md @@ -12,8 +12,9 @@ embebido, dialectos IBM Enterprise) es un esfuerzo multi-mes. | -------------- | ---- | ------------------------------------------------------------ | | `charka-bcd` | lib | Aritmética decimal de punto fijo con semántica COBOL: `Picture`, `Decimal`, redondeo, `ON SIZE ERROR` | | `charka-lexer` | lib | Tokenizador COBOL: formato fijo (tarjeta de 80 columnas) y libre | -| `charka-parser` | lib | Parser COBOL'85 (subconjunto): tokens → AST (`Program`) | -| `charka-ir` | lib | Representación intermedia: el AST con los statements del PROCEDURE ya tipados | +| `charka-parser` | lib | Parser COBOL'85 (subconjunto): tokens → AST (`Program`) | +| `charka-ir` | lib | Representación intermedia: el AST con los statements del PROCEDURE ya tipados | +| `charka-runtime` | lib | Soporte de ejecución de los programas transpilados: campos `Num` y `Text` | ## charka-bcd @@ -89,16 +90,39 @@ Tercera etapa: `Program` → `Ir`. Aquí se parsea cada `Sentence` cruda - Fuera de alcance v1: `EVALUATE`, `STRING`/`UNSTRING`, E/S de ficheros, `PERFORM VARYING`, CICS, SQL embebido. +## charka-runtime + +El soporte de ejecución: lo que `charka-codegen` emite es Rust que +enlaza contra este crate. Da a un programa transpilado la semántica de +COBOL en tiempo de ejecución. + +- `Num` — campo numérico (`PIC 9(5)V99`): un `Decimal` conformado a su + `Picture`. `store`/`store_rounded` truncan o redondean a la escala + declarada; al desbordar conservan los dígitos de bajo orden (el + `ON SIZE ERROR` sin cláusula). `display` da los dígitos con relleno + de ceros. +- `Text` — campo alfanumérico (`PIC X(20)`) de longitud fija: `store` + justifica a la izquierda y rellena/trunca; `fill` mueve figurativas + (`SPACES`, `ZEROS`). +- `cobol_text_cmp` — comparación alfanumérica con relleno de espacios. +- Reexporta `Decimal`/`Picture`/`Rounding` de `charka-bcd` para que el + código generado sólo necesite `use charka_runtime::*;`. + +Construido **antes** que `charka-codegen` (la nota de orden del plan +los listaba al revés): el codegen emite llamadas contra esta API, así +que el runtime debe existir primero — y es un crate autocontenido, +verificable sin depender del código emitido. + ## Estado `charka-bcd` (22 tests), `charka-lexer` (17 tests), `charka-parser` -(15 tests) y `charka-ir` (17 tests) implementados y verdes. -**Pendiente** — el resto del transpilador (Fase D del plan macro): +(15 tests), `charka-ir` (17 tests) y `charka-runtime` (17 tests) +implementados y verdes. **Pendiente** — el resto del transpilador +(Fase D del plan macro): | crate pendiente | rol | | ----------------- | ---------------------------------------------------- | -| `charka-codegen` | emisión de Rust | +| `charka-codegen` | emisión de Rust (IR → fuente Rust sobre el runtime) | | `charka-shadow` | validador en sombra (original vs transpilado) | -| `charka-runtime` | runtime determinista (sobre `charka-bcd`) | Hito intermedio sugerido: subconjunto COBOL'85 puro antes de CICS/SQL. diff --git a/crates/modules/charka/charka-runtime/Cargo.toml b/crates/modules/charka/charka-runtime/Cargo.toml new file mode 100644 index 0000000..5f3fafa --- /dev/null +++ b/crates/modules/charka/charka-runtime/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "charka-runtime" +version.workspace = true +edition.workspace = true +rust-version.workspace = true +license.workspace = true +authors.workspace = true +publish.workspace = true +description = "charka-runtime — soporte de ejecución de los programas COBOL transpilados: campos numéricos (Num) y alfanuméricos (Text) con la semántica de COBOL." + +[dependencies] +charka-bcd = { path = "../charka-bcd" } diff --git a/crates/modules/charka/charka-runtime/src/lib.rs b/crates/modules/charka/charka-runtime/src/lib.rs new file mode 100644 index 0000000..1ed5b93 --- /dev/null +++ b/crates/modules/charka/charka-runtime/src/lib.rs @@ -0,0 +1,72 @@ +//! `charka-runtime` — el soporte de ejecución de los programas COBOL +//! transpilados. +//! +//! Lo que `charka-codegen` emite no es Rust autónomo: es Rust que +//! enlaza contra esta biblioteca. Aquí viven los tipos que dan a un +//! programa transpilado la semántica de COBOL en tiempo de ejecución: +//! +//! - [`Num`] — un campo numérico (`PIC 9(5)V99`): un [`Decimal`] de +//! punto fijo conformado a su [`Picture`]. Toda asignación trunca a +//! la escala y al tamaño declarados, como el `MOVE` de COBOL. +//! - [`Text`] — un campo alfanumérico (`PIC X(20)`) de longitud fija: +//! toda asignación justifica a la izquierda y rellena o trunca. +//! +//! La aritmética decimal exacta la aporta `charka-bcd`, cuyos tipos +//! ([`Decimal`], [`Picture`], [`Rounding`]) se reexportan para que el +//! código generado sólo necesite `use charka_runtime::*;`. + +#![forbid(unsafe_code)] + +mod num; +mod text; + +pub use charka_bcd::{Decimal, Picture, Rounding}; +pub use num::Num; +pub use text::Text; + +use std::cmp::Ordering; + +/// Compara dos campos alfanuméricos con la semántica de COBOL: el más +/// corto se considera rellenado con espacios a la derecha, de modo que +/// `"AB"` y `"AB "` son iguales. +pub fn cobol_text_cmp(a: &str, b: &str) -> Ordering { + let n = a.chars().count().max(b.chars().count()); + let padded = |s: &str| -> Vec { + let mut v: Vec = s.chars().collect(); + while v.len() < n { + v.push(' '); + } + v + }; + padded(a).cmp(&padded(b)) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn text_cmp_orders_lexically() { + assert_eq!(cobol_text_cmp("ABC", "ABD"), Ordering::Less); + assert_eq!(cobol_text_cmp("ABD", "ABC"), Ordering::Greater); + assert_eq!(cobol_text_cmp("ABC", "ABC"), Ordering::Equal); + } + + #[test] + fn text_cmp_pads_the_shorter_with_spaces() { + assert_eq!(cobol_text_cmp("AB", "AB "), Ordering::Equal); + assert_eq!(cobol_text_cmp("AB", "ABC"), Ordering::Less); + } + + #[test] + fn fields_compose_for_generated_code() { + // Un mini-programa transpilado a mano: WS-CT crece, WS-MSG fijo. + let mut ws_ct = Num::with_value(Picture::new(3, 0, false), "0"); + ws_ct.store(ws_ct.value().add(&Decimal::from_integer(5))); + assert_eq!(ws_ct.display(), "005"); + + let mut ws_msg = Text::new(10); + ws_msg.store("LISTO"); + assert_eq!(ws_msg.as_str(), "LISTO "); + } +} diff --git a/crates/modules/charka/charka-runtime/src/num.rs b/crates/modules/charka/charka-runtime/src/num.rs new file mode 100644 index 0000000..53b9093 --- /dev/null +++ b/crates/modules/charka/charka-runtime/src/num.rs @@ -0,0 +1,158 @@ +//! `Num` — un campo numérico COBOL en tiempo de ejecución. + +use charka_bcd::{Decimal, Picture, Rounding}; + +/// Un campo numérico: un valor [`Decimal`] más la [`Picture`] que lo +/// conforma. Toda asignación pasa por la PICTURE — ese es el `MOVE` de +/// COBOL: el valor se ajusta a la escala y al tamaño declarados. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct Num { + value: Decimal, + pic: Picture, +} + +impl Num { + /// Campo nuevo en cero, con la PICTURE dada. + pub fn new(pic: Picture) -> Self { + Self { + value: Decimal::zero(), + pic, + } + } + + /// Campo con un `VALUE` inicial (el texto del literal). Un literal + /// inválido deja el campo en cero. + pub fn with_value(pic: Picture, literal: &str) -> Self { + let mut n = Self::new(pic); + if let Ok(d) = Decimal::parse(literal) { + n.store(d); + } + n + } + + /// El valor decimal actual. + pub fn value(&self) -> Decimal { + self.value + } + + /// La PICTURE del campo. + pub fn picture(&self) -> Picture { + self.pic + } + + /// Asigna un valor conformándolo a la PICTURE: ajusta la escala + /// truncando los dígitos fraccionarios sobrantes. + pub fn store(&mut self, v: Decimal) { + self.value = fit(v, &self.pic, Rounding::Truncate); + } + + /// Como [`store`](Self::store) pero redondeando — el `ROUNDED`. + pub fn store_rounded(&mut self, v: Decimal) { + self.value = fit(v, &self.pic, Rounding::HalfUp); + } + + /// Representación para `DISPLAY`: los dígitos del campo, rellenados + /// con ceros a la izquierda hasta el total de la PICTURE; con un + /// `-` adelante si el campo lleva signo y el valor es negativo. + pub fn display(&self) -> String { + let total = self.pic.total_digits() as usize; + let abs = self.value.mantissa().unsigned_abs().to_string(); + let digits = if abs.len() >= total { + abs[abs.len() - total..].to_string() + } else { + format!("{}{}", "0".repeat(total - abs.len()), abs) + }; + if self.pic.signed && self.value.is_negative() { + format!("-{digits}") + } else { + digits + } + } +} + +/// Conforma un valor a una PICTURE. Si la parte entera no cabe (el +/// `ON SIZE ERROR` de COBOL) y ninguna cláusula lo captura, COBOL deja +/// los dígitos de bajo orden: reescalamos y enmascaramos. +fn fit(v: Decimal, pic: &Picture, rounding: Rounding) -> Decimal { + if let Ok(d) = v.coerce(pic, rounding) { + return d; + } + let r = v.rescale(pic.fraction_digits, rounding); + let modulus = 10i128.pow(pic.total_digits() as u32); + let mut m = r.mantissa() % modulus; + if !pic.signed && m < 0 { + m = -m; + } + Decimal::new(m, pic.fraction_digits) +} + +#[cfg(test)] +mod tests { + use super::*; + + fn pic(s: &str) -> Picture { + Picture::parse(s).expect("PICTURE válida") + } + + fn dec(s: &str) -> Decimal { + Decimal::parse(s).expect("decimal válido") + } + + #[test] + fn new_field_is_zero() { + let n = Num::new(pic("9(5)")); + assert!(n.value().is_zero()); + assert_eq!(n.display(), "00000"); + } + + #[test] + fn with_value_initializes() { + let n = Num::with_value(pic("9(3)"), "42"); + assert_eq!(n.display(), "042"); + } + + #[test] + fn store_truncates_fraction() { + let mut n = Num::new(pic("9(3)V99")); + n.store(dec("12.3456")); + assert_eq!(n.value(), dec("12.34")); + } + + #[test] + fn store_rounded_rounds_fraction() { + let mut n = Num::new(pic("9(3)V99")); + n.store_rounded(dec("12.3456")); + assert_eq!(n.value(), dec("12.35")); + } + + #[test] + fn store_overflow_keeps_low_order_digits() { + // 1234 no cabe en 9(3): COBOL conserva los 3 dígitos bajos. + let mut n = Num::new(pic("9(3)")); + n.store(dec("1234")); + assert_eq!(n.display(), "234"); + } + + #[test] + fn unsigned_field_stores_magnitude() { + let mut n = Num::new(pic("9(3)")); + n.store(dec("-7")); + assert_eq!(n.display(), "007"); + assert!(!n.value().is_negative()); + } + + #[test] + fn signed_field_keeps_sign_in_display() { + let mut n = Num::new(pic("S9(3)")); + n.store(dec("-7")); + assert_eq!(n.display(), "-007"); + } + + #[test] + fn display_includes_implied_fraction_digits() { + // PIC 9(2)V99, valor 7.5 → dígitos 0750. + let mut n = Num::new(pic("9(2)V99")); + n.store(dec("7.5")); + assert_eq!(n.display(), "0750"); + } +} diff --git a/crates/modules/charka/charka-runtime/src/text.rs b/crates/modules/charka/charka-runtime/src/text.rs new file mode 100644 index 0000000..c059522 --- /dev/null +++ b/crates/modules/charka/charka-runtime/src/text.rs @@ -0,0 +1,110 @@ +//! `Text` — un campo alfanumérico COBOL en tiempo de ejecución. + +/// Un campo alfanumérico de longitud fija (`PIC X(n)`). El contenido +/// se mantiene siempre con exactamente `len` caracteres — toda +/// asignación justifica a la izquierda y rellena o trunca. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct Text { + buf: String, + len: usize, +} + +impl Text { + /// Campo nuevo de `len` caracteres, lleno de espacios. + pub fn new(len: usize) -> Self { + Self { + buf: " ".repeat(len), + len, + } + } + + /// Campo con un `VALUE` inicial. + pub fn with_value(len: usize, literal: &str) -> Self { + let mut t = Self::new(len); + t.store(literal); + t + } + + /// Asigna un texto: lo justifica a la izquierda, y rellena con + /// espacios o trunca hasta `len` — el `MOVE` alfanumérico de COBOL. + pub fn store(&mut self, s: &str) { + let mut chars: Vec = s.chars().take(self.len).collect(); + while chars.len() < self.len { + chars.push(' '); + } + self.buf = chars.into_iter().collect(); + } + + /// Llena el campo entero con un carácter — para mover las + /// constantes figurativas (`SPACES`, `ZEROS`...). + pub fn fill(&mut self, ch: char) { + self.buf = (0..self.len).map(|_| ch).collect(); + } + + /// El contenido actual (siempre exactamente `len` caracteres). + pub fn as_str(&self) -> &str { + &self.buf + } + + /// La longitud declarada del campo. + pub fn len(&self) -> usize { + self.len + } + + /// ¿El campo se declaró con longitud cero? + pub fn is_empty(&self) -> bool { + self.len == 0 + } + + /// Representación para `DISPLAY` — el contenido tal cual, con sus + /// espacios de relleno incluidos. + pub fn display(&self) -> String { + self.buf.clone() + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn new_field_is_all_spaces() { + let t = Text::new(5); + assert_eq!(t.as_str(), " "); + assert_eq!(t.len(), 5); + } + + #[test] + fn with_value_left_justifies_and_pads() { + let t = Text::with_value(5, "AB"); + assert_eq!(t.as_str(), "AB "); + } + + #[test] + fn store_truncates_when_too_long() { + let mut t = Text::new(3); + t.store("HELLO"); + assert_eq!(t.as_str(), "HEL"); + } + + #[test] + fn store_pads_when_too_short() { + let mut t = Text::new(6); + t.store("HI"); + assert_eq!(t.as_str(), "HI "); + } + + #[test] + fn fill_sets_every_position() { + let mut t = Text::new(4); + t.fill('0'); + assert_eq!(t.as_str(), "0000"); + } + + #[test] + fn zero_length_field_is_empty() { + let t = Text::new(0); + assert!(t.is_empty()); + assert_eq!(t.as_str(), ""); + } +} diff --git a/docs/changelog/charka.md b/docs/changelog/charka.md index 4c6d898..9702cf9 100644 --- a/docs/changelog/charka.md +++ b/docs/changelog/charka.md @@ -3,6 +3,30 @@ 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-runtime): soporte de ejecución — campos Num y Text + +Crate nuevo `crates/modules/charka/charka-runtime` — el soporte que los +programas COBOL transpilados enlazan. `charka-codegen` no emitirá Rust +autónomo: emitirá Rust que llama a esta biblioteca. + +- `Num` — campo numérico (`PIC 9(5)V99`): un `Decimal` conformado a su + `Picture`. `store` trunca a la escala declarada; `store_rounded` + redondea; al desbordar la parte entera conserva los dígitos de bajo + orden (el `ON SIZE ERROR` de COBOL sin cláusula). `display` da los + dígitos con relleno de ceros y signo si corresponde. +- `Text` — campo alfanumérico (`PIC X(n)`) de longitud fija: `store` + justifica a la izquierda y rellena con espacios o trunca; `fill` + mueve las constantes figurativas (`SPACES`, `ZEROS`). +- `cobol_text_cmp` — comparación alfanumérica que rellena el más corto + con espacios. +- Reexporta `Decimal`/`Picture`/`Rounding` de `charka-bcd`. +- Construido antes que `charka-codegen` (la nota de orden del plan los + listaba al revés): el codegen emite contra esta API, así que el + runtime debe existir primero — y se verifica solo, sin el codegen. +- 17 tests: campo en cero, `VALUE` inicial, truncado y redondeo, + desbordamiento que conserva bajo orden, magnitud sin signo y signo + con signo, justificación y relleno de texto, `fill`, comparación. + ### feat(charka-ir): representación intermedia — statements tipados Crate nuevo `crates/modules/charka/charka-ir` — la tercera etapa del