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