tawasuyu

tawasuyu/brahman

Rust 467TOML 184Markdown 49JSON 176411 Definiciones

feat(charka): charka-runtime — soporte de ejecución (campos Num y Text)

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 <noreply@anthropic.com>

85156c15090f0ae5116737b7c07cddae23cbf7f3 · por sergio · (hace 5 meses) · padre 71a4068d1 · ver el árbol en este commit

Delta semántico · 43

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

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

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

8 archivos · +414 −6

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<char> {
+        let mut v: Vec<char> = 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<char> = 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