tawasuyu

tawasuyu/brahman

Rust 496TOML 195Markdown 55JSON 197001 Definitions

feat(charka): STRING y UNSTRING — manejo de cadenas

Dos verbos comunes de COBOL para construir y partir cadenas.

- IR: Stmt::StringConcat { sources, into } y
  Stmt::Unstring { source, delimiter, into }.
- Parser: STRING a b DELIMITED BY SIZE INTO t END-STRING y
  UNSTRING s DELIMITED BY d INTO a b c END-UNSTRING.
- Codegen: STRING -> format! concatenado; UNSTRING -> un bloque que
  parte con str::split y reparte los trozos a los destinos.
- Shadow: el intérprete concatena / parte el texto y lo reparte.
- Corpus: programa nuevo 12-cadenas. Verificado: el intérprete sombra
  y el crate compilado por scaffold dan la misma salida.

Alcance v1: STRING con DELIMITED BY SIZE (otros delimitadores se
ignoran); sin WITH POINTER ni ON OVERFLOW.

Tests: charka-ir 25, charka-codegen 19, charka-shadow 17. fmt +
clippy limpios.

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

47c49acd47b7a5c501920b532c563796144e1556 · by sergio · (5 months ago) · parent 3902763da · browse the tree at this commit

Semantic delta · 13

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

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

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

  • signature changed enum Stmt pub

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

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

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

12 files · +269 −10

diff --git a/crates/modules/charka/SDD.md b/crates/modules/charka/SDD.md
index f136add..e5b2455 100644
--- a/crates/modules/charka/SDD.md
+++ b/crates/modules/charka/SDD.md
@@ -85,8 +85,8 @@ Tercera etapa: `Program` → `Ir`. Aquí se parsea cada `Sentence` cruda
   reimplementar la clasificación.
 - `Procedure { name, body: Vec<Stmt> }`. `Stmt` cubre `Move`,
   `Display`, `Accept`, `Compute`, `Add`/`Subtract`/`Multiply`/`Divide`,
-  `If`, `Evaluate`, `Perform`, `GoTo`, `StopRun`, `Goback`, `Exit`,
-  `Continue`.
+  `If`, `Evaluate`, `StringConcat`, `Unstring`, `Perform`, `GoTo`,
+  `StopRun`, `Goback`, `Exit`, `Continue`.
 - `Expr` — expresiones aritméticas con precedencia y paréntesis (Pratt:
   `+ -` < `* /` < `**` der.). `Cond` — comparaciones (símbolo o forma
   palabra) unidas por `AND`/`OR`/`NOT`, más nombres de condición
@@ -101,8 +101,9 @@ Tercera etapa: `Program` → `Ir`. Aquí se parsea cada `Sentence` cruda
   `UNTIL cond` y `VARYING var FROM x BY y UNTIL cond`.
 - `EVALUATE subject WHEN ... WHEN OTHER` — el `case` de COBOL, por
   igualdad de valor (no la forma `EVALUATE TRUE` con condiciones).
-- Fuera de alcance v1: `STRING`/`UNSTRING`, E/S de ficheros, CICS,
-  SQL embebido.
+- `STRING` (concatenación) y `UNSTRING` (partición por delimitador) —
+  el manejo de cadenas.
+- Fuera de alcance v1: E/S de ficheros, CICS, SQL embebido.
 
 ## charka-runtime
 
@@ -173,8 +174,8 @@ que corre el `Ir` directamente sobre `charka-runtime`, sin compilar.
 
 ## El corpus
 
-`crates/modules/charka/corpus/` — 11 programas COBOL graduados
-(`01-hola` … `11-tabla`), cada uno con su `.expected`. Ejercita el
+`crates/modules/charka/corpus/` — 12 programas COBOL graduados
+(`01-hola` … `12-cadenas`), cada uno con su `.expected`. Ejercita el
 pipeline completo de punta a punta. Ver su `README.md`.
 
 ## La CLI
diff --git a/crates/modules/charka/charka-codegen/src/lib.rs b/crates/modules/charka/charka-codegen/src/lib.rs
index 18b38d7..c5f9e4a 100644
--- a/crates/modules/charka/charka-codegen/src/lib.rs
+++ b/crates/modules/charka/charka-codegen/src/lib.rs
@@ -367,6 +367,23 @@ mod tests {
         assert!(out.contains(".saturating_sub(1)]"));
     }
 
+    #[test]
+    fn string_concatenates_and_unstring_splits() {
+        let out = gen("DATA DIVISION.\n\
+             WORKING-STORAGE SECTION.\n\
+             01 WS-A PIC X(4).\n\
+             01 WS-B PIC X(4).\n\
+             01 WS-OUT PIC X(10).\n\
+             01 WS-SRC PIC X(10).\n\
+             PROCEDURE DIVISION.\n\
+             MAIN.\n\
+                 STRING WS-A WS-B DELIMITED BY SIZE INTO WS-OUT END-STRING.\n\
+                 UNSTRING WS-SRC DELIMITED BY ',' INTO WS-A WS-B END-UNSTRING.\n");
+        assert!(out.contains("self.ws_out.store(&format!("));
+        assert!(out.contains("__src.split(__delim.as_str())"));
+        assert!(out.contains("__it.next().unwrap_or(\"\")"));
+    }
+
     #[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 e6543fe..100761a 100644
--- a/crates/modules/charka/charka-codegen/src/stmt.rs
+++ b/crates/modules/charka/charka-codegen/src/stmt.rs
@@ -76,6 +76,12 @@ pub(crate) fn emit_stmt(em: &mut Emitter, sym: &Symbols, stmt: &Stmt) {
             whens,
             other,
         } => emit_evaluate(em, sym, subject, whens, other),
+        Stmt::StringConcat { sources, into } => emit_string(em, sym, sources, into),
+        Stmt::Unstring {
+            source,
+            delimiter,
+            into,
+        } => emit_unstring(em, sym, source, delimiter, into),
         Stmt::Perform(p) => emit_perform(em, sym, p),
         Stmt::GoTo { target } => {
             em.line(&format!(
@@ -366,6 +372,57 @@ fn branch_condition(sym: &Symbols, subject: &Operand, branch: &WhenBranch) -> St
         .join(" || ")
 }
 
+/// Almacena una expresión `&str` en un destino: directo si es de
+/// texto, parseado a `Decimal` si es numérico.
+fn emit_store_text(em: &mut Emitter, sym: &Symbols, target: &Operand, text: &str) {
+    match field_ref(sym, target) {
+        Some((lref, FieldKind::Text { .. })) => {
+            em.line(&format!("{lref}.store({text});"));
+        }
+        Some((lref, FieldKind::Num { .. })) => {
+            em.line(&format!(
+                "{lref}.store(Decimal::parse(({text}).trim())\
+                 .unwrap_or_else(|_| Decimal::zero()));"
+            ));
+        }
+        None => em.line("// charka: destino no resuelto"),
+    }
+}
+
+/// `STRING` — concatena el texto de las fuentes en el destino.
+fn emit_string(em: &mut Emitter, sym: &Symbols, sources: &[Operand], into: &Operand) {
+    let fmt = "{}".repeat(sources.len());
+    let args: Vec<String> = sources.iter().map(|s| operand_display(sym, s)).collect();
+    let concat = format!("&format!(\"{fmt}\", {})", args.join(", "));
+    emit_store_text(em, sym, into, &concat);
+}
+
+/// `UNSTRING` — parte el texto de la fuente y reparte los trozos.
+fn emit_unstring(
+    em: &mut Emitter,
+    sym: &Symbols,
+    source: &Operand,
+    delimiter: &Operand,
+    into: &[Operand],
+) {
+    em.line("{");
+    em.indent();
+    em.line(&format!(
+        "let __src = ({}).to_string();",
+        operand_display(sym, source)
+    ));
+    em.line(&format!(
+        "let __delim = ({}).to_string();",
+        operand_display(sym, delimiter)
+    ));
+    em.line("let mut __it = __src.split(__delim.as_str());");
+    for t in into {
+        emit_store_text(em, sym, t, "__it.next().unwrap_or(\"\")");
+    }
+    em.dedent();
+    em.line("}");
+}
+
 fn emit_perform(em: &mut Emitter, sym: &Symbols, p: &Perform) {
     // Emite el "cuerpo": la llamada al párrafo o el bloque en línea.
     let emit_body = |em: &mut Emitter, sym: &Symbols| match &p.target {
diff --git a/crates/modules/charka/charka-ir/src/ast.rs b/crates/modules/charka/charka-ir/src/ast.rs
index 1d0e106..c3f50d9 100644
--- a/crates/modules/charka/charka-ir/src/ast.rs
+++ b/crates/modules/charka/charka-ir/src/ast.rs
@@ -170,6 +170,19 @@ pub enum Stmt {
         /// El cuerpo de `WHEN OTHER` (vacío si no hay).
         other: Vec<Stmt>,
     },
+    /// `STRING sources... DELIMITED BY SIZE INTO into` — concatena el
+    /// texto de los `sources` en `into`.
+    StringConcat {
+        sources: Vec<Operand>,
+        into: Operand,
+    },
+    /// `UNSTRING source DELIMITED BY delimiter INTO into...` — parte el
+    /// texto de `source` por `delimiter` y reparte los trozos.
+    Unstring {
+        source: Operand,
+        delimiter: Operand,
+        into: Vec<Operand>,
+    },
     /// `PERFORM ...` — ver [`Perform`].
     Perform(Perform),
     /// `GO TO target`
diff --git a/crates/modules/charka/charka-ir/src/lib.rs b/crates/modules/charka/charka-ir/src/lib.rs
index 515ee5b..2e1a445 100644
--- a/crates/modules/charka/charka-ir/src/lib.rs
+++ b/crates/modules/charka/charka-ir/src/lib.rs
@@ -16,10 +16,10 @@
 //! Alcance v1 — los verbos parseados a fondo: `MOVE`, `DISPLAY`,
 //! `ACCEPT`, `COMPUTE` (con expresiones con precedencia), `ADD`,
 //! `SUBTRACT`, `MULTIPLY`, `DIVIDE`, `IF`/`ELSE`/`END-IF` (con
-//! condiciones `AND`/`OR`/`NOT`), `EVALUATE`/`WHEN`, `PERFORM` (fuera
-//! de línea, en línea, `TIMES`, `UNTIL`, `VARYING`), `GO TO`,
-//! `STOP RUN`, `GOBACK`, `EXIT`, `CONTINUE`. Fuera de alcance:
-//! `STRING`/`UNSTRING`, E/S de ficheros, CICS y SQL embebido.
+//! condiciones `AND`/`OR`/`NOT`), `EVALUATE`/`WHEN`, `STRING`,
+//! `UNSTRING`, `PERFORM` (fuera de línea, en línea, `TIMES`, `UNTIL`,
+//! `VARYING`), `GO TO`, `STOP RUN`, `GOBACK`, `EXIT`, `CONTINUE`.
+//! Fuera de alcance: E/S de ficheros, CICS y SQL embebido.
 
 #![forbid(unsafe_code)]
 
@@ -358,6 +358,31 @@ mod tests {
         }
     }
 
+    #[test]
+    fn string_and_unstring_parse() {
+        let b = body("STRING WS-A WS-B DELIMITED BY SIZE INTO WS-OUT END-STRING.");
+        match &b[0] {
+            Stmt::StringConcat { sources, into } => {
+                assert_eq!(sources.len(), 2);
+                assert_eq!(into, &Operand::Data("WS-OUT".into()));
+            }
+            other => panic!("se esperaba STRING, vino {other:?}"),
+        }
+        let b = body("UNSTRING WS-SRC DELIMITED BY ',' INTO WS-A WS-B END-UNSTRING.");
+        match &b[0] {
+            Stmt::Unstring {
+                source,
+                delimiter,
+                into,
+            } => {
+                assert_eq!(source, &Operand::Data("WS-SRC".into()));
+                assert_eq!(delimiter, &Operand::Str(",".into()));
+                assert_eq!(into.len(), 2);
+            }
+            other => panic!("se esperaba UNSTRING, vino {other:?}"),
+        }
+    }
+
     #[test]
     fn several_statements_in_one_sentence() {
         let b = body("MOVE 1 TO X DISPLAY X STOP RUN.");
diff --git a/crates/modules/charka/charka-ir/src/stmt.rs b/crates/modules/charka/charka-ir/src/stmt.rs
index fcc7f9b..6ec13a9 100644
--- a/crates/modules/charka/charka-ir/src/stmt.rs
+++ b/crates/modules/charka/charka-ir/src/stmt.rs
@@ -39,6 +39,8 @@ fn parse_one_stmt(c: &mut Cursor, stops: &[&str]) -> Stmt {
         "DIVIDE" => parse_divide(c),
         "IF" => parse_if(c),
         "EVALUATE" => parse_evaluate(c),
+        "STRING" => parse_string(c),
+        "UNSTRING" => parse_unstring(c),
         "PERFORM" => parse_perform(c),
         "GO" => parse_goto(c),
         "STOP" => parse_stop(c),
@@ -115,6 +117,16 @@ fn skip_to_stmt_boundary(c: &mut Cursor) {
     }
 }
 
+/// ¿El token actual puede iniciar un operando?
+fn is_operand_start(c: &Cursor) -> bool {
+    match c.peek().map(|t| t.kind) {
+        Some(TokenKind::Number | TokenKind::String) => true,
+        Some(TokenKind::Word) => c.peek_word().map(|w| !is_boundary(&w)).unwrap_or(false),
+        Some(TokenKind::Symbol) => c.at_sym("-") || c.at_sym("+"),
+        _ => false,
+    }
+}
+
 /// Lee un único nombre de dato, si lo hay y no es una palabra frontera.
 fn parse_one_name(c: &mut Cursor) -> Option<String> {
     match c.peek_word() {
@@ -322,6 +334,50 @@ fn parse_evaluate(c: &mut Cursor) -> Stmt {
     }
 }
 
+fn parse_string(c: &mut Cursor) -> Stmt {
+    c.bump(); // STRING
+    let mut sources = Vec::new();
+    while !c.done() && !c.at_word("INTO") && !c.at_word("END-STRING") {
+        if c.eat_word("DELIMITED") {
+            c.eat_word("BY");
+            if !c.eat_word("SIZE") {
+                let _ = parse_operand(c); // delimitador: la v1 lo ignora
+            }
+        } else if is_operand_start(c) {
+            sources.push(parse_operand(c));
+        } else {
+            break;
+        }
+    }
+    c.eat_word("INTO");
+    let into = parse_operand(c);
+    skip_to_stmt_boundary(c); // p. ej. `WITH POINTER`, `ON OVERFLOW`
+    c.eat_word("END-STRING");
+    Stmt::StringConcat { sources, into }
+}
+
+fn parse_unstring(c: &mut Cursor) -> Stmt {
+    c.bump(); // UNSTRING
+    let source = parse_operand(c);
+    let delimiter = if c.eat_word("DELIMITED") {
+        c.eat_word("BY");
+        c.eat_word("ALL");
+        parse_operand(c)
+    } else {
+        Operand::Str(" ".to_string())
+    };
+    c.eat_word("INTO");
+    let mut rounded = false;
+    let into = parse_targets(c, &mut rounded);
+    skip_to_stmt_boundary(c); // p. ej. `DELIMITER IN`, `COUNT IN`
+    c.eat_word("END-UNSTRING");
+    Stmt::Unstring {
+        source,
+        delimiter,
+        into,
+    }
+}
+
 fn parse_perform(c: &mut Cursor) -> Stmt {
     c.bump(); // PERFORM
 
diff --git a/crates/modules/charka/charka-shadow/src/interp.rs b/crates/modules/charka/charka-shadow/src/interp.rs
index 2cf170f..ddde260 100644
--- a/crates/modules/charka/charka-shadow/src/interp.rs
+++ b/crates/modules/charka/charka-shadow/src/interp.rs
@@ -247,6 +247,29 @@ impl<'a> Machine<'a> {
                 }
                 self.exec_block(other)
             }
+            Stmt::StringConcat { sources, into } => {
+                let s: String = sources.iter().map(|o| self.eval_text(o)).collect();
+                self.store_text(into, &s);
+                Flow::Normal
+            }
+            Stmt::Unstring {
+                source,
+                delimiter,
+                into,
+            } => {
+                let src = self.eval_text(source);
+                let delim = self.eval_text(delimiter);
+                let parts: Vec<String> = if delim.is_empty() {
+                    vec![src]
+                } else {
+                    src.split(delim.as_str()).map(|p| p.to_string()).collect()
+                };
+                for (i, target) in into.iter().enumerate() {
+                    let piece = parts.get(i).cloned().unwrap_or_default();
+                    self.store_text(target, &piece);
+                }
+                Flow::Normal
+            }
             Stmt::Perform(p) => self.exec_perform(p),
             Stmt::GoTo { target } => {
                 // Aproximación: ejecuta el destino y sale del párrafo.
@@ -410,6 +433,26 @@ impl<'a> Machine<'a> {
         }
     }
 
+    /// Almacena un texto en un destino, conformándolo a su tipo.
+    fn store_text(&mut self, target: &Operand, text: &str) {
+        let Some((key, idx)) = self.resolve(target) else {
+            return;
+        };
+        match self.fields.get_mut(&key) {
+            Some(Cell::Text(arr)) => {
+                if let Some(t) = arr.get_mut(idx) {
+                    t.store(text);
+                }
+            }
+            Some(Cell::Num(arr)) => {
+                if let Some(n) = arr.get_mut(idx) {
+                    n.store(Decimal::parse(text.trim()).unwrap_or_else(|_| Decimal::zero()));
+                }
+            }
+            None => {}
+        }
+    }
+
     // ── Evaluación ────────────────────────────────────────────────
 
     fn eval_decimal(&self, op: &Operand) -> Decimal {
diff --git a/crates/modules/charka/charka-shadow/src/lib.rs b/crates/modules/charka/charka-shadow/src/lib.rs
index 38e50a3..2d1b901 100644
--- a/crates/modules/charka/charka-shadow/src/lib.rs
+++ b/crates/modules/charka/charka-shadow/src/lib.rs
@@ -120,6 +120,7 @@ mod tests {
     corpus_test!(corpus_09_evaluar, "09-evaluar");
     corpus_test!(corpus_10_condicion, "10-condicion");
     corpus_test!(corpus_11_tabla, "11-tabla");
+    corpus_test!(corpus_12_cadenas, "12-cadenas");
 
     #[test]
     fn empty_source_runs_clean() {
diff --git a/crates/modules/charka/corpus/12-cadenas.cob b/crates/modules/charka/corpus/12-cadenas.cob
new file mode 100644
index 0000000..9b1b99b
--- /dev/null
+++ b/crates/modules/charka/corpus/12-cadenas.cob
@@ -0,0 +1,25 @@
+* corpus charka — nivel 6: STRING y UNSTRING
+IDENTIFICATION DIVISION.
+PROGRAM-ID. CADENAS.
+DATA DIVISION.
+WORKING-STORAGE SECTION.
+01 WS-NOMBRE   PIC X(3)  VALUE 'ANA'.
+01 WS-APELLIDO PIC X(5)  VALUE 'PEREZ'.
+01 WS-COMPLETO PIC X(20).
+01 WS-CSV      PIC X(15) VALUE 'ROJO,VERDE,AZUL'.
+01 WS-C1       PIC X(8).
+01 WS-C2       PIC X(8).
+01 WS-C3       PIC X(8).
+PROCEDURE DIVISION.
+MAIN.
+    STRING WS-NOMBRE DELIMITED BY SIZE
+           WS-APELLIDO DELIMITED BY SIZE
+           INTO WS-COMPLETO
+    END-STRING.
+    DISPLAY 'COMPLETO=' WS-COMPLETO.
+    UNSTRING WS-CSV DELIMITED BY ',' INTO WS-C1 WS-C2 WS-C3
+    END-UNSTRING.
+    DISPLAY 'C1=' WS-C1.
+    DISPLAY 'C2=' WS-C2.
+    DISPLAY 'C3=' WS-C3.
+    STOP RUN.
diff --git a/crates/modules/charka/corpus/12-cadenas.expected b/crates/modules/charka/corpus/12-cadenas.expected
new file mode 100644
index 0000000..3451dcd
--- /dev/null
+++ b/crates/modules/charka/corpus/12-cadenas.expected
@@ -0,0 +1,4 @@
+COMPLETO=ANAPEREZ
+C1=ROJO
+C2=VERDE
+C3=AZUL
diff --git a/crates/modules/charka/corpus/README.md b/crates/modules/charka/corpus/README.md
index acaf473..cb7e1a8 100644
--- a/crates/modules/charka/corpus/README.md
+++ b/crates/modules/charka/corpus/README.md
@@ -20,6 +20,7 @@ salida correcta, una línea por `DISPLAY`.
 | `09-evaluar`        | 5     | `EVALUATE` — el `case` de COBOL, `WHEN` / `OTHER`  |
 | `10-condicion`      | 5     | nombres de condición (nivel 88) en `IF`            |
 | `11-tabla`          | 6     | tablas (`OCCURS`) y referencias con subíndice      |
+| `12-cadenas`        | 6     | `STRING` (concatenar) y `UNSTRING` (partir)        |
 
 ## Formato
 
diff --git a/docs/changelog/charka.md b/docs/changelog/charka.md
index 612a5be..5f71d1d 100644
--- a/docs/changelog/charka.md
+++ b/docs/changelog/charka.md
@@ -3,6 +3,22 @@
 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): STRING y UNSTRING — manejo de cadenas
+
+Dos verbos comunes de COBOL para construir y partir cadenas.
+
+- IR: `Stmt::StringConcat { sources, into }` y
+  `Stmt::Unstring { source, delimiter, into }`.
+- Parser: `STRING a b DELIMITED BY SIZE INTO t END-STRING` y
+  `UNSTRING s DELIMITED BY d INTO a b c END-UNSTRING`.
+- Codegen: `STRING` → `format!` concatenado; `UNSTRING` → un bloque
+  que parte con `str::split` y reparte los trozos.
+- Shadow: el intérprete concatena / parte el texto y lo reparte.
+- Corpus: programa nuevo `12-cadenas`. Verificado: el intérprete
+  sombra y el crate compilado dan la misma salida.
+- Alcance v1: `STRING` con `DELIMITED BY SIZE` (los demás
+  delimitadores se ignoran); sin `WITH POINTER` ni `ON OVERFLOW`.
+
 ### feat(charka): OCCURS — tablas y referencias con subíndice
 
 Los arrays de COBOL, que antes el transpilador descartaba en silencio.