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
@@ -8,10 +8,10 @@
use std::collections::HashMap;
use charka_ir::{
BinOp, CmpOp, Cond, ConditionName, Expr, Figurative, InspectOp, Ir, Operand, Perform,
BinOp, CmpOp, Cond, ConditionName, Expr, Figurative, FileMode, InspectOp, Ir, Operand, Perform,
PerformControl, PerformTarget, Stmt, WhenTest,
};
use charka_runtime::{cobol_text_cmp, Decimal, Num, Rounding, Text};
use charka_runtime::{cobol_text_cmp, CobFile, Decimal, Num, Rounding, Text};
use crate::field::{build_fields, Cell};
@@ -39,6 +39,7 @@ pub(crate) struct Machine<'a> {
fields: HashMap<String, Cell>,
para_index: HashMap<String, usize>,
conditions: HashMap<String, ConditionName>,
files: HashMap<String, CobFile>,
pub output: Vec<String>,
budget: u64,
pub step_limit_hit: bool,
@@ -58,11 +59,17 @@ impl<'a> Machine<'a> {
.iter()
.map(|c| (c.name.clone(), c.clone()))
.collect();
let files = ir
.files
.iter()
.map(|f| (f.name.to_uppercase(), CobFile::new(&f.path)))
.collect();
Self {
ir,
fields: build_fields(&ir.model),
para_index,
conditions,
files,
output: Vec::new(),
budget: STEP_BUDGET,
step_limit_hit: false,
@@ -320,6 +327,69 @@ impl<'a> Machine<'a> {
}
Flow::Normal
}
Stmt::Open { mode, files } => {
for f in files {
if let Some(cf) = self.files.get_mut(&f.to_uppercase()) {
match mode {
FileMode::Input => cf.open_input(),
FileMode::Output => cf.open_output(),
}
}
}
Flow::Normal
}
Stmt::Close { files } => {
for f in files {
if let Some(cf) = self.files.get_mut(&f.to_uppercase()) {
cf.close();
}
}
Flow::Normal
}
Stmt::Read {
file,
at_end,
not_at_end,
} => {
let line = self
.files
.get_mut(&file.to_uppercase())
.and_then(|cf| cf.read());
match line {
Some(text) => {
let record = self
.ir
.files
.iter()
.find(|f| f.name.eq_ignore_ascii_case(file))
.map(|f| f.record.clone());
if let Some(rec) = record {
self.store_text(&Operand::Data(rec), &text);
}
self.exec_block(not_at_end)
}
None => self.exec_block(at_end),
}
}
Stmt::Write { record, from } => {
if let Some(src) = from {
let text = self.eval_text(src);
self.store_text(&Operand::Data(record.clone()), &text);
}
let file = self
.ir
.files
.iter()
.find(|f| f.record.eq_ignore_ascii_case(record))
.map(|f| f.name.to_uppercase());
if let Some(file) = file {
let line = self.eval_text(&Operand::Data(record.clone()));
if let Some(cf) = self.files.get_mut(&file) {
cf.write(&line);
}
}
Flow::Normal
}
Stmt::Perform(p) => self.exec_perform(p),
Stmt::GoTo { target } => {
// Aproximación: ejecuta el destino y sale del párrafo.