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>
This commit is contained in:
@@ -85,8 +85,8 @@ Tercera etapa: `Program` → `Ir`. Aquí se parsea cada `Sentence` cruda
|
|||||||
reimplementar la clasificación.
|
reimplementar la clasificación.
|
||||||
- `Procedure { name, body: Vec<Stmt> }`. `Stmt` cubre `Move`,
|
- `Procedure { name, body: Vec<Stmt> }`. `Stmt` cubre `Move`,
|
||||||
`Display`, `Accept`, `Compute`, `Add`/`Subtract`/`Multiply`/`Divide`,
|
`Display`, `Accept`, `Compute`, `Add`/`Subtract`/`Multiply`/`Divide`,
|
||||||
`If`, `Evaluate`, `Perform`, `GoTo`, `StopRun`, `Goback`, `Exit`,
|
`If`, `Evaluate`, `StringConcat`, `Unstring`, `Perform`, `GoTo`,
|
||||||
`Continue`.
|
`StopRun`, `Goback`, `Exit`, `Continue`.
|
||||||
- `Expr` — expresiones aritméticas con precedencia y paréntesis (Pratt:
|
- `Expr` — expresiones aritméticas con precedencia y paréntesis (Pratt:
|
||||||
`+ -` < `* /` < `**` der.). `Cond` — comparaciones (símbolo o forma
|
`+ -` < `* /` < `**` der.). `Cond` — comparaciones (símbolo o forma
|
||||||
palabra) unidas por `AND`/`OR`/`NOT`, más nombres de condición
|
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`.
|
`UNTIL cond` y `VARYING var FROM x BY y UNTIL cond`.
|
||||||
- `EVALUATE subject WHEN ... WHEN OTHER` — el `case` de COBOL, por
|
- `EVALUATE subject WHEN ... WHEN OTHER` — el `case` de COBOL, por
|
||||||
igualdad de valor (no la forma `EVALUATE TRUE` con condiciones).
|
igualdad de valor (no la forma `EVALUATE TRUE` con condiciones).
|
||||||
- Fuera de alcance v1: `STRING`/`UNSTRING`, E/S de ficheros, CICS,
|
- `STRING` (concatenación) y `UNSTRING` (partición por delimitador) —
|
||||||
SQL embebido.
|
el manejo de cadenas.
|
||||||
|
- Fuera de alcance v1: E/S de ficheros, CICS, SQL embebido.
|
||||||
|
|
||||||
## charka-runtime
|
## charka-runtime
|
||||||
|
|
||||||
@@ -173,8 +174,8 @@ que corre el `Ir` directamente sobre `charka-runtime`, sin compilar.
|
|||||||
|
|
||||||
## El corpus
|
## El corpus
|
||||||
|
|
||||||
`crates/modules/charka/corpus/` — 11 programas COBOL graduados
|
`crates/modules/charka/corpus/` — 12 programas COBOL graduados
|
||||||
(`01-hola` … `11-tabla`), cada uno con su `.expected`. Ejercita el
|
(`01-hola` … `12-cadenas`), 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
|
||||||
|
|||||||
@@ -367,6 +367,23 @@ mod tests {
|
|||||||
assert!(out.contains(".saturating_sub(1)]"));
|
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]
|
#[test]
|
||||||
fn empty_program_still_compiles_shape() {
|
fn empty_program_still_compiles_shape() {
|
||||||
let out = gen("");
|
let out = gen("");
|
||||||
|
|||||||
@@ -76,6 +76,12 @@ pub(crate) fn emit_stmt(em: &mut Emitter, sym: &Symbols, stmt: &Stmt) {
|
|||||||
whens,
|
whens,
|
||||||
other,
|
other,
|
||||||
} => emit_evaluate(em, sym, subject, 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::Perform(p) => emit_perform(em, sym, p),
|
||||||
Stmt::GoTo { target } => {
|
Stmt::GoTo { target } => {
|
||||||
em.line(&format!(
|
em.line(&format!(
|
||||||
@@ -366,6 +372,57 @@ fn branch_condition(sym: &Symbols, subject: &Operand, branch: &WhenBranch) -> St
|
|||||||
.join(" || ")
|
.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) {
|
fn emit_perform(em: &mut Emitter, sym: &Symbols, p: &Perform) {
|
||||||
// Emite el "cuerpo": la llamada al párrafo o el bloque en línea.
|
// 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 {
|
let emit_body = |em: &mut Emitter, sym: &Symbols| match &p.target {
|
||||||
|
|||||||
@@ -170,6 +170,19 @@ pub enum Stmt {
|
|||||||
/// El cuerpo de `WHEN OTHER` (vacío si no hay).
|
/// El cuerpo de `WHEN OTHER` (vacío si no hay).
|
||||||
other: Vec<Stmt>,
|
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 ...` — ver [`Perform`].
|
||||||
Perform(Perform),
|
Perform(Perform),
|
||||||
/// `GO TO target`
|
/// `GO TO target`
|
||||||
|
|||||||
@@ -16,10 +16,10 @@
|
|||||||
//! Alcance v1 — los verbos parseados a fondo: `MOVE`, `DISPLAY`,
|
//! Alcance v1 — los verbos parseados a fondo: `MOVE`, `DISPLAY`,
|
||||||
//! `ACCEPT`, `COMPUTE` (con expresiones con precedencia), `ADD`,
|
//! `ACCEPT`, `COMPUTE` (con expresiones con precedencia), `ADD`,
|
||||||
//! `SUBTRACT`, `MULTIPLY`, `DIVIDE`, `IF`/`ELSE`/`END-IF` (con
|
//! `SUBTRACT`, `MULTIPLY`, `DIVIDE`, `IF`/`ELSE`/`END-IF` (con
|
||||||
//! condiciones `AND`/`OR`/`NOT`), `EVALUATE`/`WHEN`, `PERFORM` (fuera
|
//! condiciones `AND`/`OR`/`NOT`), `EVALUATE`/`WHEN`, `STRING`,
|
||||||
//! de línea, en línea, `TIMES`, `UNTIL`, `VARYING`), `GO TO`,
|
//! `UNSTRING`, `PERFORM` (fuera de línea, en línea, `TIMES`, `UNTIL`,
|
||||||
//! `STOP RUN`, `GOBACK`, `EXIT`, `CONTINUE`. Fuera de alcance:
|
//! `VARYING`), `GO TO`, `STOP RUN`, `GOBACK`, `EXIT`, `CONTINUE`.
|
||||||
//! `STRING`/`UNSTRING`, E/S de ficheros, CICS y SQL embebido.
|
//! Fuera de alcance: E/S de ficheros, CICS y SQL embebido.
|
||||||
|
|
||||||
#![forbid(unsafe_code)]
|
#![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]
|
#[test]
|
||||||
fn several_statements_in_one_sentence() {
|
fn several_statements_in_one_sentence() {
|
||||||
let b = body("MOVE 1 TO X DISPLAY X STOP RUN.");
|
let b = body("MOVE 1 TO X DISPLAY X STOP RUN.");
|
||||||
|
|||||||
@@ -39,6 +39,8 @@ fn parse_one_stmt(c: &mut Cursor, stops: &[&str]) -> Stmt {
|
|||||||
"DIVIDE" => parse_divide(c),
|
"DIVIDE" => parse_divide(c),
|
||||||
"IF" => parse_if(c),
|
"IF" => parse_if(c),
|
||||||
"EVALUATE" => parse_evaluate(c),
|
"EVALUATE" => parse_evaluate(c),
|
||||||
|
"STRING" => parse_string(c),
|
||||||
|
"UNSTRING" => parse_unstring(c),
|
||||||
"PERFORM" => parse_perform(c),
|
"PERFORM" => parse_perform(c),
|
||||||
"GO" => parse_goto(c),
|
"GO" => parse_goto(c),
|
||||||
"STOP" => parse_stop(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.
|
/// Lee un único nombre de dato, si lo hay y no es una palabra frontera.
|
||||||
fn parse_one_name(c: &mut Cursor) -> Option<String> {
|
fn parse_one_name(c: &mut Cursor) -> Option<String> {
|
||||||
match c.peek_word() {
|
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 {
|
fn parse_perform(c: &mut Cursor) -> Stmt {
|
||||||
c.bump(); // PERFORM
|
c.bump(); // PERFORM
|
||||||
|
|
||||||
|
|||||||
@@ -247,6 +247,29 @@ impl<'a> Machine<'a> {
|
|||||||
}
|
}
|
||||||
self.exec_block(other)
|
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::Perform(p) => self.exec_perform(p),
|
||||||
Stmt::GoTo { target } => {
|
Stmt::GoTo { target } => {
|
||||||
// Aproximación: ejecuta el destino y sale del párrafo.
|
// 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 ────────────────────────────────────────────────
|
// ── Evaluación ────────────────────────────────────────────────
|
||||||
|
|
||||||
fn eval_decimal(&self, op: &Operand) -> Decimal {
|
fn eval_decimal(&self, op: &Operand) -> Decimal {
|
||||||
|
|||||||
@@ -120,6 +120,7 @@ mod tests {
|
|||||||
corpus_test!(corpus_09_evaluar, "09-evaluar");
|
corpus_test!(corpus_09_evaluar, "09-evaluar");
|
||||||
corpus_test!(corpus_10_condicion, "10-condicion");
|
corpus_test!(corpus_10_condicion, "10-condicion");
|
||||||
corpus_test!(corpus_11_tabla, "11-tabla");
|
corpus_test!(corpus_11_tabla, "11-tabla");
|
||||||
|
corpus_test!(corpus_12_cadenas, "12-cadenas");
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn empty_source_runs_clean() {
|
fn empty_source_runs_clean() {
|
||||||
|
|||||||
@@ -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.
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
COMPLETO=ANAPEREZ
|
||||||
|
C1=ROJO
|
||||||
|
C2=VERDE
|
||||||
|
C3=AZUL
|
||||||
@@ -20,6 +20,7 @@ salida correcta, una línea por `DISPLAY`.
|
|||||||
| `09-evaluar` | 5 | `EVALUATE` — el `case` de COBOL, `WHEN` / `OTHER` |
|
| `09-evaluar` | 5 | `EVALUATE` — el `case` de COBOL, `WHEN` / `OTHER` |
|
||||||
| `10-condicion` | 5 | nombres de condición (nivel 88) en `IF` |
|
| `10-condicion` | 5 | nombres de condición (nivel 88) en `IF` |
|
||||||
| `11-tabla` | 6 | tablas (`OCCURS`) y referencias con subíndice |
|
| `11-tabla` | 6 | tablas (`OCCURS`) y referencias con subíndice |
|
||||||
|
| `12-cadenas` | 6 | `STRING` (concatenar) y `UNSTRING` (partir) |
|
||||||
|
|
||||||
## Formato
|
## Formato
|
||||||
|
|
||||||
|
|||||||
@@ -3,6 +3,22 @@
|
|||||||
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): 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
|
### feat(charka): OCCURS — tablas y referencias con subíndice
|
||||||
|
|
||||||
Los arrays de COBOL, que antes el transpilador descartaba en silencio.
|
Los arrays de COBOL, que antes el transpilador descartaba en silencio.
|
||||||
|
|||||||
Reference in New Issue
Block a user