//! El intérprete del IR: la ejecución «sombra» del programa COBOL. //! //! Ejecuta el [`Ir`] directamente sobre los tipos de `charka-runtime`, //! sin compilar nada. Es una segunda ruta de ejecución, independiente //! del código que emite `charka-codegen` — eso es lo que lo hace un //! validador: si el intérprete y el transpilado divergen, hay un bug. use std::collections::HashMap; use charka_ir::{ BinOp, CmpOp, Cond, ConditionName, Expr, Figurative, FileMode, InspectOp, Ir, Operand, Perform, PerformControl, PerformTarget, Stmt, WhenTest, }; use charka_runtime::{cobol_text_cmp, format_edited, CobFile, Decimal, Num, Rounding, Text}; use crate::field::{build_fields, Cell}; /// Tope de pasos: corta los bucles que no terminan (un `PERFORM UNTIL` /// con una condición que nunca se cumple) en vez de colgarse. const STEP_BUDGET: u64 = 5_000_000; /// Escala intermedia de la división dentro de una expresión. const DIV_SCALE: u8 = 9; /// El resultado de ejecutar un statement: cómo sigue el control. #[derive(Debug, Clone, Copy, PartialEq, Eq)] enum Flow { /// Sigue con el statement siguiente. Normal, /// Sale del párrafo actual (`EXIT`). Exit, /// Termina el programa (`STOP RUN`, `GOBACK`, tope de pasos). Stop, } /// La máquina sombra: el estado y el motor de ejecución. pub(crate) struct Machine<'a> { ir: &'a Ir, fields: HashMap, para_index: HashMap, conditions: HashMap, files: HashMap, pub output: Vec, budget: u64, pub step_limit_hit: bool, pub stopped: bool, } impl<'a> Machine<'a> { /// Prepara la máquina: aplana los datos e indexa los párrafos. pub(crate) fn new(ir: &'a Ir) -> Self { let mut para_index = HashMap::new(); for (i, proc) in ir.procedures.iter().enumerate() { para_index.entry(proc.name.to_uppercase()).or_insert(i); } let conditions = ir .model .conditions .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, stopped: false, } } /// Corre el programa: encadena los párrafos en orden (el «caer» de /// COBOL) hasta un `STOP RUN` o el final. pub(crate) fn run(&mut self) { let ir = self.ir; for i in 0..ir.procedures.len() { if let Flow::Stop = self.exec_block(&ir.procedures[i].body) { self.stopped = true; break; } } } // ── Ejecución ───────────────────────────────────────────────── /// Consume un paso del presupuesto. `true` si se agotó. fn tick(&mut self) -> bool { if self.budget == 0 { self.step_limit_hit = true; return true; } self.budget -= 1; false } fn exec_block(&mut self, stmts: &'a [Stmt]) -> Flow { for s in stmts { match self.exec_stmt(s) { Flow::Normal => {} other => return other, } } Flow::Normal } fn exec_stmt(&mut self, stmt: &'a Stmt) -> Flow { if self.tick() { return Flow::Stop; } match stmt { Stmt::Move { from, to } => { for t in to { self.do_move(from, t); } Flow::Normal } Stmt::Display { items } => { let line: String = items.iter().map(|o| self.eval_text(o)).collect(); self.output.push(line); Flow::Normal } Stmt::Accept { .. } => Flow::Normal, // sin entrada: deja el campo igual Stmt::Compute { targets, rounded, expr, } => { let value = self.eval_expr(expr); for t in targets { self.store(t, value, *rounded); } Flow::Normal } Stmt::Add { addends, to, giving, rounded, } => { let sum = self.fold_sum(addends); if giving.is_empty() { for t in to { let cur = self.eval_decimal(t); self.store(t, cur.add(&sum), *rounded); } } else { let base = match to.first() { Some(first) => sum.add(&self.eval_decimal(first)), None => sum, }; for g in giving { self.store(g, base, *rounded); } } Flow::Normal } Stmt::Subtract { amounts, from, giving, rounded, } => { let sum = self.fold_sum(amounts); if giving.is_empty() { for t in from { let cur = self.eval_decimal(t); self.store(t, cur.sub(&sum), *rounded); } } else { let minuend = from .first() .map(|f| self.eval_decimal(f)) .unwrap_or_else(Decimal::zero); let value = minuend.sub(&sum); for g in giving { self.store(g, value, *rounded); } } Flow::Normal } Stmt::Multiply { left, by, giving, rounded, } => { let value = self.eval_decimal(left).mul(&self.eval_decimal(by)); if giving.is_empty() { // `MULTIPLY a BY b` sin GIVING: b queda con a*b. self.store(by, value, *rounded); } else { for g in giving { self.store(g, value, *rounded); } } Flow::Normal } Stmt::Divide { left, right, by_form, giving, rounded, } => { let (num, den) = if *by_form { (self.eval_decimal(left), self.eval_decimal(right)) } else { (self.eval_decimal(right), self.eval_decimal(left)) }; if giving.is_empty() { // `DIVIDE a INTO b` sin GIVING: b queda con b/a. let v = divide(num, den, self.target_scale(right)); self.store(right, v, *rounded); } else { for g in giving { let v = divide(num, den, self.target_scale(g)); self.store(g, v, *rounded); } } Flow::Normal } Stmt::If { cond, then_branch, else_branch, } => { if self.eval_cond(cond) { self.exec_block(then_branch) } else { self.exec_block(else_branch) } } Stmt::Evaluate { subject, whens, other, } => { for branch in whens { if branch.tests.iter().any(|t| self.when_test(subject, t)) { return self.exec_block(&branch.body); } } self.exec_block(other) } Stmt::StringConcat { sources, into } => { let s: String = sources.iter().map(|o| self.eval_text(o)).collect(); self.store_text(into, &s); Flow::Normal } Stmt::Unstring { source, delimiter, into, } => { let src = self.eval_text(source); let delim = self.eval_text(delimiter); let parts: Vec = if delim.is_empty() { vec![src] } else { src.split(delim.as_str()).map(|p| p.to_string()).collect() }; for (i, target) in into.iter().enumerate() { let piece = parts.get(i).cloned().unwrap_or_default(); self.store_text(target, &piece); } Flow::Normal } Stmt::Inspect { target, op } => { match op { InspectOp::TallyingForAll { counter, search } => { let hay = self.eval_text(target); let needle = self.eval_text(search); let n = if needle.is_empty() { 0 } else { hay.matches(needle.as_str()).count() }; let cur = self.eval_decimal(counter); self.store(counter, cur.add(&Decimal::from_integer(n as i128)), false); } InspectOp::ReplacingAll { from, to } => { let hay = self.eval_text(target); let f = self.eval_text(from); let t = self.eval_text(to); let new = if f.is_empty() { hay } else { hay.replace(f.as_str(), t.as_str()) }; self.store_text(target, &new); } } Flow::Normal } Stmt::Initialize { targets } => { for t in targets { match t { Operand::Data(name) => { match self.ir.model.group(name).map(|g| g.members.clone()) { Some(members) => { for m in &members { self.reset_field(m); } } None => self.reset_field(name), } } Operand::Indexed { .. } => self.reset_element(t), _ => {} } } Flow::Normal } Stmt::SetTrue { conditions } => { for name in conditions { if let Some(cn) = self.conditions.get(&name.to_uppercase()).cloned() { self.do_move(&cn.value, &Operand::Data(cn.parent)); } } 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. match self.run_paragraph(target) { Flow::Stop => Flow::Stop, _ => Flow::Exit, } } Stmt::StopRun | Stmt::Goback => Flow::Stop, Stmt::Exit => Flow::Exit, Stmt::Continue => Flow::Normal, Stmt::Unknown { .. } => Flow::Normal, // verbo no soportado: se omite } } fn exec_perform(&mut self, p: &'a Perform) -> Flow { match &p.control { PerformControl::Once => self.run_target(&p.target), PerformControl::Times(n) => { let count = self.count_of(n); for _ in 0..count { if self.tick() { return Flow::Stop; } if let Flow::Stop = self.run_target(&p.target) { return Flow::Stop; } } Flow::Normal } PerformControl::Until(cond) => loop { if self.tick() { return Flow::Stop; } if self.eval_cond(cond) { return Flow::Normal; } if let Flow::Stop = self.run_target(&p.target) { return Flow::Stop; } }, PerformControl::Varying { var, from, by, until, } => { let var_op = Operand::Data(var.clone()); let start = self.eval_decimal(from); self.store(&var_op, start, false); loop { if self.tick() { return Flow::Stop; } if self.eval_cond(until) { return Flow::Normal; } if let Flow::Stop = self.run_target(&p.target) { return Flow::Stop; } let next = self.eval_decimal(&var_op).add(&self.eval_decimal(by)); self.store(&var_op, next, false); } } } } /// Ejecuta una vez el cuerpo de un `PERFORM`. Un `EXIT` dentro de /// él termina esa pasada, no el programa. fn run_target(&mut self, target: &'a PerformTarget) -> Flow { let flow = match target { PerformTarget::Paragraph { name, thru } => { self.run_paragraph_range(name, thru.as_deref()) } PerformTarget::Inline(body) => self.exec_block(body), }; match flow { Flow::Stop => Flow::Stop, _ => Flow::Normal, } } fn run_paragraph(&mut self, name: &str) -> Flow { let Some(&idx) = self.para_index.get(&name.to_uppercase()) else { return Flow::Normal; }; let ir = self.ir; match self.exec_block(&ir.procedures[idx].body) { Flow::Stop => Flow::Stop, _ => Flow::Normal, } } /// Ejecuta el rango de párrafos de `name` a `thru` inclusive (el /// `PERFORM name THRU thru`); sólo `name` si `thru` es `None`. fn run_paragraph_range(&mut self, name: &str, thru: Option<&str>) -> Flow { let Some(&start) = self.para_index.get(&name.to_uppercase()) else { return Flow::Normal; }; let end = match thru { Some(t) => self .para_index .get(&t.to_uppercase()) .copied() .unwrap_or(start), None => start, }; let (lo, hi) = if start <= end { (start, end) } else { (end, start) }; let ir = self.ir; for i in lo..=hi { if let Flow::Stop = self.exec_block(&ir.procedures[i].body) { return Flow::Stop; } } Flow::Normal } /// Resuelve una referencia a dato (escalar o elemento de tabla) a /// su nombre y un índice 0-based. `None` si no es una referencia. fn resolve(&self, op: &Operand) -> Option<(String, usize)> { match op { Operand::Data(name) => Some((name.to_uppercase(), 0)), Operand::Indexed { name, index } => { // El subíndice de COBOL es 1-based. let i = self .eval_decimal(index) .rescale(0, Rounding::Truncate) .mantissa(); let idx = if i < 1 { 0 } else { (i - 1) as usize }; Some((name.to_uppercase(), idx)) } _ => None, } } /// `MOVE from` a un solo destino (escalar o elemento de tabla). fn do_move(&mut self, from: &Operand, target: &Operand) { // Un destino con PICTURE de edición formatea el valor numérico. if let Operand::Data(name) = target { if let Some(pic) = self.ir.model.field(name).and_then(|f| f.edit.clone()) { let value = self.eval_decimal(from); let text = format_edited(value, &pic); self.store_text(target, &text); return; } } let Some((key, idx)) = self.resolve(target) else { return; }; let is_num = matches!(self.fields.get(&key), Some(Cell::Num(_))); if is_num { let v = self.eval_decimal(from); if let Some(Cell::Num(arr)) = self.fields.get_mut(&key) { if let Some(n) = arr.get_mut(idx) { n.store(v); } } } else if let Operand::Figurative(fig) = from { let ch = figurative_fill(*fig); if let Some(Cell::Text(arr)) = self.fields.get_mut(&key) { if let Some(t) = arr.get_mut(idx) { t.fill(ch); } } } else { let s = self.eval_text(from); if let Some(Cell::Text(arr)) = self.fields.get_mut(&key) { if let Some(t) = arr.get_mut(idx) { t.store(&s); } } } } /// Almacena un valor en un destino, conformándolo a su tipo. fn store(&mut self, target: &Operand, value: Decimal, rounded: bool) { let Some((key, idx)) = self.resolve(target) else { return; }; match self.fields.get_mut(&key) { Some(Cell::Num(arr)) => { if let Some(n) = arr.get_mut(idx) { if rounded { n.store_rounded(value); } else { n.store(value); } } } Some(Cell::Text(arr)) => { if let Some(t) = arr.get_mut(idx) { t.store(&value.to_string()); } } None => {} } } /// Resetea un campo completo (escalar o tabla) a su valor por /// defecto: 0 si es numérico, espacios si es alfanumérico. fn reset_field(&mut self, name: &str) { match self.fields.get_mut(&name.to_uppercase()) { Some(Cell::Num(arr)) => { for n in arr.iter_mut() { *n = Num::new(n.picture()); } } Some(Cell::Text(arr)) => { for t in arr.iter_mut() { *t = Text::new(t.len()); } } None => {} } } /// Resetea un solo elemento de tabla a su valor por defecto. fn reset_element(&mut self, op: &Operand) { let Some((key, idx)) = self.resolve(op) else { return; }; match self.fields.get_mut(&key) { Some(Cell::Num(arr)) => { if let Some(n) = arr.get_mut(idx) { *n = Num::new(n.picture()); } } Some(Cell::Text(arr)) => { if let Some(t) = arr.get_mut(idx) { *t = Text::new(t.len()); } } None => {} } } /// Almacena un texto en un destino, conformándolo a su tipo. fn store_text(&mut self, target: &Operand, text: &str) { let Some((key, idx)) = self.resolve(target) else { return; }; match self.fields.get_mut(&key) { Some(Cell::Text(arr)) => { if let Some(t) = arr.get_mut(idx) { t.store(text); } } Some(Cell::Num(arr)) => { if let Some(n) = arr.get_mut(idx) { n.store(Decimal::parse(text.trim()).unwrap_or_else(|_| Decimal::zero())); } } None => {} } } // ── Evaluación ──────────────────────────────────────────────── fn eval_decimal(&self, op: &Operand) -> Decimal { match op { Operand::Num(n) => Decimal::parse(n).unwrap_or_else(|_| Decimal::zero()), Operand::Str(s) => Decimal::parse(s).unwrap_or_else(|_| Decimal::zero()), Operand::Figurative(_) => Decimal::zero(), Operand::Data(_) | Operand::Indexed { .. } => { let Some((key, idx)) = self.resolve(op) else { return Decimal::zero(); }; match self.fields.get(&key) { Some(Cell::Num(arr)) => arr .get(idx) .map(|n| n.value()) .unwrap_or_else(Decimal::zero), Some(Cell::Text(arr)) => arr .get(idx) .and_then(|t| Decimal::parse(t.as_str().trim()).ok()) .unwrap_or_else(Decimal::zero), None => Decimal::zero(), } } } } fn eval_text(&self, op: &Operand) -> String { match op { Operand::Str(s) => s.clone(), Operand::Num(n) => n.clone(), Operand::Figurative(f) => figurative_text(*f).to_string(), Operand::Data(_) | Operand::Indexed { .. } => { let Some((key, idx)) = self.resolve(op) else { return String::new(); }; match self.fields.get(&key) { Some(Cell::Num(arr)) => arr.get(idx).map(|n| n.display()).unwrap_or_default(), Some(Cell::Text(arr)) => arr.get(idx).map(|t| t.display()).unwrap_or_default(), None => String::new(), } } } } fn eval_expr(&self, e: &Expr) -> Decimal { match e { Expr::Operand(op) => self.eval_decimal(op), Expr::Neg(inner) => Decimal::zero().sub(&self.eval_expr(inner)), Expr::Binary { op, lhs, rhs } => { let l = self.eval_expr(lhs); let r = self.eval_expr(rhs); match op { BinOp::Add => l.add(&r), BinOp::Sub => l.sub(&r), BinOp::Mul => l.mul(&r), BinOp::Div => divide(l, r, DIV_SCALE), BinOp::Pow => pow(&l, &r), } } } } fn eval_cond(&self, c: &Cond) -> bool { match c { Cond::Compare { lhs, op, rhs } => { let ord = if self.is_text(lhs) || self.is_text(rhs) { cobol_text_cmp(&self.eval_text(lhs), &self.eval_text(rhs)) } else { self.eval_decimal(lhs).cmp(&self.eval_decimal(rhs)) }; match op { CmpOp::Eq => ord.is_eq(), CmpOp::Ne => ord.is_ne(), CmpOp::Lt => ord.is_lt(), CmpOp::Gt => ord.is_gt(), CmpOp::Le => ord.is_le(), CmpOp::Ge => ord.is_ge(), } } Cond::Named(name) => match self.conditions.get(&name.to_uppercase()) { // Un nombre de condición (88): el dato padre igual al // valor que la hace verdadera. Some(cn) => self.operands_equal(&Operand::Data(cn.parent.clone()), &cn.value), None => false, }, Cond::Not(inner) => !self.eval_cond(inner), Cond::And(a, b) => self.eval_cond(a) && self.eval_cond(b), Cond::Or(a, b) => self.eval_cond(a) || self.eval_cond(b), } } fn is_text(&self, op: &Operand) -> bool { match op { Operand::Str(_) => true, Operand::Data(_) | Operand::Indexed { .. } => match self.resolve(op) { Some((key, _)) => matches!(self.fields.get(&key), Some(Cell::Text(_))), None => false, }, _ => false, } } /// ¿Se cumple una prueba `WHEN` para el sujeto dado? fn when_test(&self, subject: &Operand, test: &WhenTest) -> bool { match test { WhenTest::Value(v) => self.operands_equal(subject, v), WhenTest::Range(lo, hi) => { let s = self.eval_decimal(subject); s >= self.eval_decimal(lo) && s <= self.eval_decimal(hi) } WhenTest::Cond(cond) => self.eval_cond(cond), } } /// ¿Son iguales dos operandos? (Para las ramas `WHEN` del `EVALUATE`.) fn operands_equal(&self, a: &Operand, b: &Operand) -> bool { if self.is_text(a) || self.is_text(b) { cobol_text_cmp(&self.eval_text(a), &self.eval_text(b)).is_eq() } else { self.eval_decimal(a) == self.eval_decimal(b) } } /// La suma de una lista de operandos. fn fold_sum(&self, ops: &[Operand]) -> Decimal { let mut acc = Decimal::zero(); for o in ops { acc = acc.add(&self.eval_decimal(o)); } acc } /// Los dígitos fraccionarios de un destino numérico. fn target_scale(&self, op: &Operand) -> u8 { if let Some((key, idx)) = self.resolve(op) { if let Some(Cell::Num(arr)) = self.fields.get(&key) { if let Some(n) = arr.get(idx) { return n.picture().fraction_digits; } } } 4 } /// El número de repeticiones de un `PERFORM ... TIMES`. fn count_of(&self, op: &Operand) -> usize { let m = self .eval_decimal(op) .rescale(0, Rounding::Truncate) .mantissa(); if m < 0 { 0 } else { m as usize } } } /// División con escala fija; una división por cero da cero. fn divide(num: Decimal, den: Decimal, scale: u8) -> Decimal { num.div(&den, scale, Rounding::Truncate) .unwrap_or_else(|_| Decimal::zero()) } /// Potencia con exponente entero no negativo; en otro caso da 1. fn pow(base: &Decimal, exp: &Decimal) -> Decimal { let e = exp.rescale(0, Rounding::Truncate).mantissa(); if !(0..=256).contains(&e) { return Decimal::from_integer(1); } let mut acc = Decimal::from_integer(1); for _ in 0..e { acc = acc.mul(base); } acc } /// El texto que representa una constante figurativa. fn figurative_text(f: Figurative) -> &'static str { match f { Figurative::Zero => "0", Figurative::Space => " ", Figurative::Quote => "\"", Figurative::HighValue | Figurative::LowValue | Figurative::Null => "", } } /// El carácter de relleno de una figurativa, para `Text::fill`. fn figurative_fill(f: Figurative) -> char { match f { Figurative::Zero => '0', Figurative::Quote => '"', _ => ' ', } }