feat(charka): INSPECT — contar y reemplazar caracteres

El verbo de COBOL para analizar y limpiar campos de texto.

- IR: Stmt::Inspect { target, op } con InspectOp::TallyingForAll
  (cuenta apariciones y las suma a un contador) y
  InspectOp::ReplacingAll (reemplaza apariciones).
- Parser: INSPECT t TALLYING n FOR ALL lit y
  INSPECT t REPLACING ALL a BY b. Una forma no soportada cae a
  Stmt::Unknown.
- Codegen: TALLYING -> str::matches(..).count(); REPLACING ->
  str::replace.
- Shadow: el intérprete cuenta / reemplaza el texto.
- Corpus: programa nuevo 13-inspeccion. Verificado: el intérprete
  sombra y el crate compilado por scaffold dan la misma salida.

Alcance v1: TALLYING FOR ALL y REPLACING ALL; sin LEADING, FIRST,
CHARACTERS, BEFORE/AFTER.

Tests: charka-ir 26, charka-codegen 20, charka-shadow 18. fmt +
clippy limpios.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
sergio
2026-05-21 22:17:47 +00:00
parent 47c49acd47
commit 2728698f5e
13 changed files with 205 additions and 18 deletions
@@ -183,6 +183,8 @@ pub enum Stmt {
delimiter: Operand,
into: Vec<Operand>,
},
/// `INSPECT target ...` — cuenta o reemplaza caracteres.
Inspect { target: Operand, op: InspectOp },
/// `PERFORM ...` — ver [`Perform`].
Perform(Perform),
/// `GO TO target`
@@ -200,6 +202,16 @@ pub enum Stmt {
Unknown { verb: String, tokens: Vec<Token> },
}
/// La operación de un `INSPECT`.
#[derive(Debug, Clone, PartialEq)]
pub enum InspectOp {
/// `TALLYING counter FOR ALL search` — suma a `counter` la cantidad
/// de apariciones de `search` en el destino.
TallyingForAll { counter: Operand, search: Operand },
/// `REPLACING ALL from BY to` — reemplaza las apariciones de `from`.
ReplacingAll { from: Operand, to: Operand },
}
/// Una rama `WHEN` de un `EVALUATE`: los valores que la disparan
/// (varios `WHEN` apilados comparten cuerpo) y el cuerpo a ejecutar.
#[derive(Debug, Clone, PartialEq)]
+32 -5
View File
@@ -17,9 +17,9 @@
//! `ACCEPT`, `COMPUTE` (con expresiones con precedencia), `ADD`,
//! `SUBTRACT`, `MULTIPLY`, `DIVIDE`, `IF`/`ELSE`/`END-IF` (con
//! 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.
//! `UNSTRING`, `INSPECT`, `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.
#![forbid(unsafe_code)]
@@ -383,6 +383,33 @@ mod tests {
}
}
#[test]
fn inspect_tallying_and_replacing_parse() {
let b = body("INSPECT WS-T TALLYING WS-N FOR ALL 'A'.");
match &b[0] {
Stmt::Inspect {
target,
op: InspectOp::TallyingForAll { counter, search },
} => {
assert_eq!(target, &Operand::Data("WS-T".into()));
assert_eq!(counter, &Operand::Data("WS-N".into()));
assert_eq!(search, &Operand::Str("A".into()));
}
other => panic!("se esperaba INSPECT TALLYING, vino {other:?}"),
}
let b = body("INSPECT WS-T REPLACING ALL 'A' BY 'O'.");
match &b[0] {
Stmt::Inspect {
op: InspectOp::ReplacingAll { from, to },
..
} => {
assert_eq!(from, &Operand::Str("A".into()));
assert_eq!(to, &Operand::Str("O".into()));
}
other => panic!("se esperaba INSPECT REPLACING, vino {other:?}"),
}
}
#[test]
fn several_statements_in_one_sentence() {
let b = body("MOVE 1 TO X DISPLAY X STOP RUN.");
@@ -394,10 +421,10 @@ mod tests {
#[test]
fn unrecognized_verb_becomes_unknown() {
let b = body("INSPECT WS-X TALLYING WS-N FOR ALL ' '.");
let b = body("INITIALIZE WS-X WS-Y.");
match &b[0] {
Stmt::Unknown { verb, tokens } => {
assert_eq!(verb, "INSPECT");
assert_eq!(verb, "INITIALIZE");
assert!(!tokens.is_empty());
}
other => panic!("se esperaba Unknown, vino {other:?}"),
+35 -1
View File
@@ -5,7 +5,7 @@
use charka_parser::TokenKind;
use crate::ast::{Operand, Perform, PerformControl, PerformTarget, Stmt, WhenBranch};
use crate::ast::{InspectOp, Operand, Perform, PerformControl, PerformTarget, Stmt, WhenBranch};
use crate::cursor::{parse_operand, Cursor};
use crate::expr::{parse_cond, parse_expr};
use crate::kw::{is_boundary, is_terminator, is_verb};
@@ -41,6 +41,7 @@ fn parse_one_stmt(c: &mut Cursor, stops: &[&str]) -> Stmt {
"EVALUATE" => parse_evaluate(c),
"STRING" => parse_string(c),
"UNSTRING" => parse_unstring(c),
"INSPECT" => parse_inspect(c),
"PERFORM" => parse_perform(c),
"GO" => parse_goto(c),
"STOP" => parse_stop(c),
@@ -378,6 +379,39 @@ fn parse_unstring(c: &mut Cursor) -> Stmt {
}
}
fn parse_inspect(c: &mut Cursor) -> Stmt {
c.bump(); // INSPECT
let target = parse_operand(c);
if c.eat_word("TALLYING") {
let counter = parse_operand(c);
c.eat_word("FOR");
c.eat_word("ALL");
let search = parse_operand(c);
skip_to_stmt_boundary(c);
Stmt::Inspect {
target,
op: InspectOp::TallyingForAll { counter, search },
}
} else if c.eat_word("REPLACING") {
c.eat_word("ALL");
let from = parse_operand(c);
c.eat_word("BY");
let to = parse_operand(c);
skip_to_stmt_boundary(c);
Stmt::Inspect {
target,
op: InspectOp::ReplacingAll { from, to },
}
} else {
// Forma de INSPECT que la v1 no modela.
skip_to_stmt_boundary(c);
Stmt::Unknown {
verb: "INSPECT".to_string(),
tokens: Vec::new(),
}
}
}
fn parse_perform(c: &mut Cursor) -> Stmt {
c.bump(); // PERFORM