feat(charka): E/S de ficheros — SELECT/FD/OPEN/READ/WRITE/CLOSE

El gran hueco que faltaba para el COBOL real: el procesamiento de
ficheros secuenciales. Una rebanada vertical por los seis crates.

- charka-parser: la ENVIRONMENT division ya no se ignora — se parsea
  FILE-CONTROL (SELECT name ASSIGN TO "ruta"); del FILE SECTION se
  asocia cada FD con su registro 01. Program::files.
- charka-runtime: tipo CobFile — un fichero «line sequential» (cada
  registro una línea). Lectura: carga a memoria. Escritura: acumula y
  vuelca al cerrar.
- charka-ir: Ir::files y los statements Open/Close/Read/Write. READ
  lleva sus bloques AT END / NOT AT END.
- charka-codegen: un campo CobFile por fichero en el struct Program;
  los verbos emiten llamadas al runtime.
- charka-shadow: el intérprete hace E/S de ficheros real.
- Corpus: programa nuevo 18-fichero — escribe tres líneas, las relee
  con READ ... AT END y las muestra. Verificado: el intérprete sombra
  y el crate compilado por scaffold dan la misma salida.

Alcance v1: organización line sequential; sin ficheros indexados ni
relativos, sin FILE STATUS.

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

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
sergio
2026-05-21 22:47:26 +00:00
parent f250fd0765
commit b3278bdb0c
17 changed files with 663 additions and 22 deletions
+27 -1
View File
@@ -1,7 +1,7 @@
//! Los tipos del IR: el programa COBOL con su PROCEDURE division ya
//! parseada a instrucciones tipadas.
pub use charka_parser::{DataItem, Token};
pub use charka_parser::{DataItem, FileEntry, Token};
/// Un programa COBOL en representación intermedia.
#[derive(Debug, Clone, PartialEq, Default)]
@@ -14,6 +14,8 @@ pub struct Ir {
/// El modelo de datos resuelto: los datos elementales aplanados y
/// los nombres de condición (nivel 88).
pub model: crate::model::DataModel,
/// Los ficheros declarados (`SELECT` + `FD`).
pub files: Vec<FileEntry>,
/// Los párrafos del PROCEDURE, con sus statements ya tipados.
pub procedures: Vec<Procedure>,
}
@@ -191,6 +193,21 @@ pub enum Stmt {
/// `SET cond-name... TO TRUE` — hace verdaderos esos nombres de
/// condición (nivel 88): asigna a su dato padre el valor del 88.
SetTrue { conditions: Vec<String> },
/// `OPEN {INPUT|OUTPUT} files...`
Open { mode: FileMode, files: Vec<String> },
/// `CLOSE files...`
Close { files: Vec<String> },
/// `READ file [AT END at_end] [NOT AT END not_at_end] [END-READ]`
Read {
file: String,
at_end: Vec<Stmt>,
not_at_end: Vec<Stmt>,
},
/// `WRITE record [FROM from]`
Write {
record: String,
from: Option<Operand>,
},
/// `PERFORM ...` — ver [`Perform`].
Perform(Perform),
/// `GO TO target`
@@ -208,6 +225,15 @@ pub enum Stmt {
Unknown { verb: String, tokens: Vec<Token> },
}
/// El modo de apertura de un fichero.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum FileMode {
/// `OPEN INPUT` — para lectura.
Input,
/// `OPEN OUTPUT` — para escritura (crea el fichero de cero).
Output,
}
/// La operación de un `INSPECT`.
#[derive(Debug, Clone, PartialEq)]
pub enum InspectOp {
+36 -1
View File
@@ -19,7 +19,8 @@
//! condiciones `AND`/`OR`/`NOT`), `EVALUATE`/`WHEN`, `STRING`,
//! `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.
//! `EXIT`, `CONTINUE`, E/S de ficheros (`OPEN`/`READ`/`WRITE`/`CLOSE`).
//! Fuera de alcance: CICS y SQL embebido.
#![forbid(unsafe_code)]
@@ -58,6 +59,7 @@ pub fn lower(program: &Program) -> Ir {
program_id: program.program_id.clone().unwrap_or_default(),
data: program.data.clone(),
model: model::resolve_data(&program.data),
files: program.files.clone(),
procedures,
}
}
@@ -471,6 +473,39 @@ mod tests {
}
}
#[test]
fn file_io_statements_parse() {
let program = ir("ENVIRONMENT DIVISION.\n\
INPUT-OUTPUT SECTION.\n\
FILE-CONTROL.\n\
SELECT ARCH ASSIGN TO 'datos.dat'.\n\
DATA DIVISION.\n\
FILE SECTION.\n\
FD ARCH.\n\
01 REG PIC X(20).\n\
PROCEDURE DIVISION.\n\
MAIN.\n\
OPEN OUTPUT ARCH.\n\
WRITE REG FROM 'HOLA'.\n\
CLOSE ARCH.\n\
OPEN INPUT ARCH.\n\
READ ARCH AT END CONTINUE NOT AT END DISPLAY REG END-READ.\n\
CLOSE ARCH.\n");
assert_eq!(program.files.len(), 1);
assert_eq!(program.files[0].record, "REG");
let body = &program.procedures[0].body;
assert!(matches!(
body[0],
Stmt::Open {
mode: FileMode::Output,
..
}
));
assert!(matches!(&body[1], Stmt::Write { record, .. } if record == "REG"));
assert!(matches!(body[2], Stmt::Close { .. }));
assert!(matches!(&body[4], Stmt::Read { not_at_end, .. } if not_at_end.len() == 1));
}
#[test]
fn several_statements_in_one_sentence() {
let b = body("MOVE 1 TO X DISPLAY X STOP RUN.");
+79 -1
View File
@@ -6,7 +6,8 @@
use charka_parser::TokenKind;
use crate::ast::{
InspectOp, Operand, Perform, PerformControl, PerformTarget, Stmt, WhenBranch, WhenTest,
FileMode, InspectOp, Operand, Perform, PerformControl, PerformTarget, Stmt, WhenBranch,
WhenTest,
};
use crate::cursor::{parse_operand, Cursor};
use crate::expr::{parse_cond, parse_expr};
@@ -46,6 +47,10 @@ fn parse_one_stmt(c: &mut Cursor, stops: &[&str]) -> Stmt {
"INSPECT" => parse_inspect(c),
"INITIALIZE" => parse_initialize(c),
"SET" => parse_set(c),
"OPEN" => parse_open(c),
"CLOSE" => parse_close(c),
"READ" => parse_read(c),
"WRITE" => parse_write(c),
"PERFORM" => parse_perform(c),
"GO" => parse_goto(c),
"STOP" => parse_stop(c),
@@ -418,6 +423,79 @@ fn parse_initialize(c: &mut Cursor) -> Stmt {
Stmt::Initialize { targets }
}
fn parse_open(c: &mut Cursor) -> Stmt {
c.bump(); // OPEN
let mode = if c.eat_word("OUTPUT") || c.eat_word("EXTEND") {
FileMode::Output
} else {
c.eat_word("INPUT");
c.eat_word("I-O");
FileMode::Input
};
let mut files = Vec::new();
while let Some(w) = c.peek_word() {
if is_boundary(&w) || matches!(w.as_str(), "INPUT" | "OUTPUT" | "EXTEND" | "I-O") {
break;
}
c.bump();
files.push(w);
}
skip_to_stmt_boundary(c);
Stmt::Open { mode, files }
}
fn parse_close(c: &mut Cursor) -> Stmt {
c.bump(); // CLOSE
let mut files = Vec::new();
while let Some(name) = parse_one_name(c) {
files.push(name);
}
Stmt::Close { files }
}
fn parse_read(c: &mut Cursor) -> Stmt {
c.bump(); // READ
let file = parse_one_name(c).unwrap_or_default();
c.eat_word("NEXT");
c.eat_word("RECORD");
if c.eat_word("INTO") {
let _ = parse_operand(c); // `READ ... INTO`: la v1 lo ignora
}
let mut at_end = Vec::new();
let mut not_at_end = Vec::new();
loop {
if c.eat_word("AT") {
c.eat_word("END");
at_end = parse_statements(c, &["NOT", "END-READ"]);
} else if c.eat_word("NOT") {
c.eat_word("AT");
c.eat_word("END");
not_at_end = parse_statements(c, &["END-READ"]);
} else {
break;
}
}
c.eat_word("END-READ");
Stmt::Read {
file,
at_end,
not_at_end,
}
}
fn parse_write(c: &mut Cursor) -> Stmt {
c.bump(); // WRITE
let record = parse_one_name(c).unwrap_or_default();
let from = if c.eat_word("FROM") {
Some(parse_operand(c))
} else {
None
};
skip_to_stmt_boundary(c); // p. ej. `AFTER ADVANCING`
c.eat_word("END-WRITE");
Stmt::Write { record, from }
}
fn parse_inspect(c: &mut Cursor) -> Stmt {
c.bump(); // INSPECT
let target = parse_operand(c);