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` 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 = 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, }, + /// `STRING sources... DELIMITED BY SIZE INTO into` — concatena el + /// texto de los `sources` en `into`. + StringConcat { + sources: Vec, + 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, + }, /// `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 { 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 = 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.