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:
@@ -0,0 +1,102 @@
|
||||
//! `CobFile` — un fichero secuencial de líneas para el runtime COBOL.
|
||||
|
||||
use std::collections::VecDeque;
|
||||
|
||||
/// Un fichero de organización «line sequential»: cada registro es una
|
||||
/// línea de texto. La lectura carga el fichero entero a memoria; la
|
||||
/// escritura acumula líneas y las vuelca al cerrar.
|
||||
#[derive(Debug)]
|
||||
pub struct CobFile {
|
||||
path: String,
|
||||
state: State,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
enum State {
|
||||
Closed,
|
||||
/// Abierto para lectura: las líneas que faltan por leer.
|
||||
Reading(VecDeque<String>),
|
||||
/// Abierto para escritura: las líneas acumuladas.
|
||||
Writing(Vec<String>),
|
||||
}
|
||||
|
||||
impl CobFile {
|
||||
/// Un fichero nuevo, cerrado, asignado a la ruta `path`.
|
||||
pub fn new(path: &str) -> Self {
|
||||
Self {
|
||||
path: path.to_string(),
|
||||
state: State::Closed,
|
||||
}
|
||||
}
|
||||
|
||||
/// `OPEN INPUT`: carga el fichero a memoria. Si no existe, queda
|
||||
/// abierto y vacío (la primera lectura dará fin de fichero).
|
||||
pub fn open_input(&mut self) {
|
||||
let lines = std::fs::read_to_string(&self.path)
|
||||
.map(|s| s.lines().map(str::to_string).collect())
|
||||
.unwrap_or_default();
|
||||
self.state = State::Reading(lines);
|
||||
}
|
||||
|
||||
/// `OPEN OUTPUT`: empieza un fichero nuevo y vacío.
|
||||
pub fn open_output(&mut self) {
|
||||
self.state = State::Writing(Vec::new());
|
||||
}
|
||||
|
||||
/// `READ`: la siguiente línea, o `None` en fin de fichero.
|
||||
pub fn read(&mut self) -> Option<String> {
|
||||
match &mut self.state {
|
||||
State::Reading(lines) => lines.pop_front(),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// `WRITE`: agrega una línea (sólo si está abierto para escritura).
|
||||
pub fn write(&mut self, line: &str) {
|
||||
if let State::Writing(buf) = &mut self.state {
|
||||
buf.push(line.to_string());
|
||||
}
|
||||
}
|
||||
|
||||
/// `CLOSE`: si estaba escribiendo, vuelca el contenido al disco.
|
||||
pub fn close(&mut self) {
|
||||
if let State::Writing(buf) = &self.state {
|
||||
let body: String = buf.iter().map(|l| format!("{l}\n")).collect();
|
||||
let _ = std::fs::write(&self.path, body);
|
||||
}
|
||||
self.state = State::Closed;
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn write_then_read_roundtrip() {
|
||||
let path = std::env::temp_dir().join("charka-cobfile-test.dat");
|
||||
let path = path.to_str().unwrap();
|
||||
|
||||
let mut f = CobFile::new(path);
|
||||
f.open_output();
|
||||
f.write("PRIMERA");
|
||||
f.write("SEGUNDA");
|
||||
f.close();
|
||||
|
||||
let mut g = CobFile::new(path);
|
||||
g.open_input();
|
||||
assert_eq!(g.read().as_deref(), Some("PRIMERA"));
|
||||
assert_eq!(g.read().as_deref(), Some("SEGUNDA"));
|
||||
assert_eq!(g.read(), None); // fin de fichero
|
||||
g.close();
|
||||
|
||||
let _ = std::fs::remove_file(path);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn missing_file_reads_as_empty() {
|
||||
let mut f = CobFile::new("/charka/no/existe/jamas.dat");
|
||||
f.open_input();
|
||||
assert_eq!(f.read(), None);
|
||||
}
|
||||
}
|
||||
@@ -17,10 +17,12 @@
|
||||
|
||||
#![forbid(unsafe_code)]
|
||||
|
||||
mod file;
|
||||
mod num;
|
||||
mod text;
|
||||
|
||||
pub use charka_bcd::{Decimal, Picture, Rounding};
|
||||
pub use file::CobFile;
|
||||
pub use num::Num;
|
||||
pub use text::Text;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user