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