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
@@ -81,6 +81,9 @@ fn emit_struct(em: &mut Emitter, sym: &Symbols) {
};
em.line(&format!("{}: {ty},", f.ident));
}
for fs in &sym.files {
em.line(&format!("{}: CobFile,", fs.ident));
}
em.dedent();
em.line("}");
em.blank();
@@ -99,6 +102,13 @@ fn emit_impl(em: &mut Emitter, sym: &Symbols, ir: &Ir) {
for f in &sym.fields {
em.line(&format!("{}: {},", f.ident, field_init(f)));
}
for fs in &sym.files {
em.line(&format!(
"{}: CobFile::new({}),",
fs.ident,
rust_str(&fs.path)
));
}
em.dedent();
em.line("}");
em.dedent();
@@ -438,6 +448,32 @@ mod tests {
assert!(out.matches("self.p_b();").count() >= 2);
}
#[test]
fn file_io_emits_open_read_write_close() {
let out = gen("ENVIRONMENT DIVISION.\n\
INPUT-OUTPUT SECTION.\n\
FILE-CONTROL.\n\
SELECT ARCH ASSIGN TO 'x.dat'.\n\
DATA DIVISION.\n\
FILE SECTION.\n\
FD ARCH.\n\
01 REG PIC X(10).\n\
PROCEDURE DIVISION.\n\
MAIN.\n\
OPEN OUTPUT ARCH.\n\
WRITE REG FROM 'HI'.\n\
CLOSE ARCH.\n\
OPEN INPUT ARCH.\n\
READ ARCH AT END CONTINUE END-READ.\n\
CLOSE ARCH.\n");
assert!(out.contains("file_arch: CobFile,"));
assert!(out.contains("CobFile::new(\"x.dat\")"));
assert!(out.contains("self.file_arch.open_output();"));
assert!(out.contains("self.file_arch.write("));
assert!(out.contains("self.file_arch.read()"));
assert!(out.contains("self.file_arch.close();"));
}
#[test]
fn empty_program_still_compiles_shape() {
let out = gen("");
@@ -2,8 +2,8 @@
//! una o varias líneas de código Rust sobre `charka-runtime`.
use charka_ir::{
CmpOp, Cond, InspectOp, Operand, Perform, PerformControl, PerformTarget, Stmt, WhenBranch,
WhenTest,
CmpOp, Cond, FileMode, InspectOp, Operand, Perform, PerformControl, PerformTarget, Stmt,
WhenBranch, WhenTest,
};
use crate::emit::Emitter;
@@ -88,6 +88,14 @@ pub(crate) fn emit_stmt(em: &mut Emitter, sym: &Symbols, stmt: &Stmt) {
Stmt::Inspect { target, op } => emit_inspect(em, sym, target, op),
Stmt::Initialize { targets } => emit_initialize(em, sym, targets),
Stmt::SetTrue { conditions } => emit_set_true(em, sym, conditions),
Stmt::Open { mode, files } => emit_open(em, sym, *mode, files),
Stmt::Close { files } => emit_close(em, sym, files),
Stmt::Read {
file,
at_end,
not_at_end,
} => emit_read(em, sym, file, at_end, not_at_end),
Stmt::Write { record, from } => emit_write(em, sym, record, from.as_ref()),
Stmt::Perform(p) => emit_perform(em, sym, p),
Stmt::GoTo { target } => {
em.line(&format!(
@@ -493,6 +501,77 @@ fn emit_initialize(em: &mut Emitter, sym: &Symbols, targets: &[Operand]) {
}
}
/// `OPEN {INPUT|OUTPUT} files...`
fn emit_open(em: &mut Emitter, sym: &Symbols, mode: FileMode, files: &[String]) {
let method = match mode {
FileMode::Input => "open_input",
FileMode::Output => "open_output",
};
for f in files {
match sym.file(f) {
Some(fs) => em.line(&format!("self.{}.{method}();", fs.ident)),
None => em.line("// charka: OPEN de fichero no resuelto"),
}
}
}
/// `CLOSE files...`
fn emit_close(em: &mut Emitter, sym: &Symbols, files: &[String]) {
for f in files {
match sym.file(f) {
Some(fs) => em.line(&format!("self.{}.close();", fs.ident)),
None => em.line("// charka: CLOSE de fichero no resuelto"),
}
}
}
/// `READ file [AT END ...] [NOT AT END ...]` — lee la línea siguiente
/// en el registro del fichero.
fn emit_read(em: &mut Emitter, sym: &Symbols, file: &str, at_end: &[Stmt], not_at_end: &[Stmt]) {
let Some(fs) = sym.file(file) else {
em.line("// charka: READ de fichero no resuelto");
return;
};
let record_ident = sym.lookup(&fs.record).map(|r| r.ident.clone());
em.line(&format!("match self.{}.read() {{", fs.ident));
em.indent();
em.line("Some(__line) => {");
em.indent();
if let Some(rec) = &record_ident {
em.line(&format!("self.{rec}.store(__line.as_str());"));
}
emit_block(em, sym, not_at_end);
em.dedent();
em.line("}");
em.line("None => {");
em.indent();
emit_block(em, sym, at_end);
em.dedent();
em.line("}");
em.dedent();
em.line("}");
}
/// `WRITE record [FROM from]` — escribe el registro en su fichero.
fn emit_write(em: &mut Emitter, sym: &Symbols, record: &str, from: Option<&Operand>) {
if let Some(src) = from {
if let Some((lref, _)) = field_ref(sym, &Operand::Data(record.to_string())) {
em.line(&format!("{lref}.store({});", operand_str(sym, src)));
}
}
match sym.file_of_record(record) {
Some(fs) => {
if let Some(rec) = sym.lookup(record) {
em.line(&format!(
"self.{}.write(&self.{}.display());",
fs.ident, rec.ident
));
}
}
None => em.line("// charka: WRITE de registro no resuelto"),
}
}
/// `SET cond... TO TRUE` — asigna a cada dato padre el valor que hace
/// verdadero su nombre de condición (nivel 88).
fn emit_set_true(em: &mut Emitter, sym: &Symbols, conditions: &[String]) {
@@ -24,8 +24,20 @@ pub(crate) struct Field {
pub occurs: Option<u32>,
}
/// Los campos del programa, sus nombres de condición, sus grupos y
/// sus párrafos.
/// Un fichero del programa generado.
pub(crate) struct FileSym {
/// Nombre COBOL del fichero.
pub cobol: String,
/// Identificador Rust del campo `CobFile` (prefijo `file_`).
pub ident: String,
/// Ruta a la que está asignado.
pub path: String,
/// Nombre COBOL del registro asociado (su `FD`).
pub record: String,
}
/// Los campos del programa, sus nombres de condición, sus grupos, sus
/// párrafos y sus ficheros.
pub(crate) struct Symbols {
pub fields: Vec<Field>,
by_name: HashMap<String, usize>,
@@ -33,6 +45,8 @@ pub(crate) struct Symbols {
groups: HashMap<String, Vec<String>>,
/// Los párrafos en orden: `(nombre COBOL, nombre de método Rust)`.
pub paragraphs: Vec<(String, String)>,
/// Los ficheros declarados.
pub files: Vec<FileSym>,
}
impl Symbols {
@@ -79,15 +93,38 @@ impl Symbols {
(proc.name.to_uppercase(), method)
})
.collect();
let files = ir
.files
.iter()
.map(|f| FileSym {
cobol: f.name.clone(),
ident: format!("file_{}", sanitize_ident(&f.name)),
path: f.path.clone(),
record: f.record.clone(),
})
.collect();
Self {
fields,
by_name,
conditions,
groups,
paragraphs,
files,
}
}
/// Busca un fichero por su nombre COBOL.
pub(crate) fn file(&self, name: &str) -> Option<&FileSym> {
let up = name.to_uppercase();
self.files.iter().find(|f| f.cobol == up)
}
/// Busca el fichero cuyo registro `FD` es `record`.
pub(crate) fn file_of_record(&self, record: &str) -> Option<&FileSym> {
let up = record.to_uppercase();
self.files.iter().find(|f| f.record == up)
}
/// Los métodos a llamar para un `PERFORM name [THRU thru]`: el
/// rango de párrafos desde `name` hasta `thru` inclusive.
pub(crate) fn paragraph_range(&self, name: &str, thru: Option<&str>) -> Vec<String> {