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:
@@ -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`
|
||||
|
||||
@@ -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.");
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
Reference in New Issue
Block a user