feat(charka): charka-ir — representación intermedia con statements tipados

Tercera etapa del transpilador: Program -> Ir. El PROCEDURE division
pasa de sentencias con tokens crudos a un árbol de instrucciones
tipadas.

- lower(&Program) -> Ir: total y tolerante, nunca falla. La DATA
  division pasa tal cual y sirve de tabla de símbolos.
- Stmt cubre MOVE, DISPLAY, ACCEPT, COMPUTE, ADD, SUBTRACT, MULTIPLY,
  DIVIDE, IF/ELSE/END-IF, PERFORM (fuera de línea, en línea, TIMES,
  UNTIL), GO TO, STOP RUN, GOBACK, EXIT, CONTINUE.
- Expresiones de COMPUTE con precedencia y paréntesis (Pratt).
  Condiciones con comparadores símbolo/palabra, AND/OR/NOT y nombres
  de condición (nivel 88).
- Delimita statements por palabras frontera (COBOL no los separa con
  un símbolo). Verbo no soportado -> Stmt::Unknown con tokens crudos.
- Módulos: ast / kw / cursor / expr / stmt. 17 tests; fmt + clippy
  limpios.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
sergio
2026-05-21 20:23:19 +00:00
parent b95383b01a
commit 71a4068d12
11 changed files with 1462 additions and 4 deletions
+28 -4
View File
@@ -13,6 +13,7 @@ embebido, dialectos IBM Enterprise) es un esfuerzo multi-mes.
| `charka-bcd` | lib | Aritmética decimal de punto fijo con semántica COBOL: `Picture`, `Decimal`, redondeo, `ON SIZE ERROR` |
| `charka-lexer` | lib | Tokenizador COBOL: formato fijo (tarjeta de 80 columnas) y libre |
| `charka-parser` | lib | Parser COBOL'85 (subconjunto): tokens → AST (`Program`) |
| `charka-ir` | lib | Representación intermedia: el AST con los statements del PROCEDURE ya tipados |
## charka-bcd
@@ -64,15 +65,38 @@ Segunda etapa: tokens → AST. Alcance v1 — el **esqueleto del programa**.
- Limitación v1: no parsea statements, ni la ENVIRONMENT division, ni
CICS / SQL embebido / dialectos IBM.
## charka-ir
Tercera etapa: `Program``Ir`. Aquí se parsea cada `Sentence` cruda
(tokens) a statements tipados — el PROCEDURE division pasa de tokens a
árbol de instrucciones.
- `lower(&Program) -> Ir`**total y tolerante**, nunca falla.
- `Ir { program_id, data: Vec<DataItem>, procedures: Vec<Procedure> }`.
El modelo de datos pasa tal cual (sirve de tabla de símbolos).
- `Procedure { name, body: Vec<Stmt> }`. `Stmt` cubre `Move`,
`Display`, `Accept`, `Compute`, `Add`/`Subtract`/`Multiply`/`Divide`,
`If`, `Perform`, `GoTo`, `StopRun`, `Goback`, `Exit`, `Continue`.
- `Expr` — expresiones aritméticas con precedencia y paréntesis (Pratt:
`+ -` < `* /` < `**` der.). `Cond` — comparaciones (símbolo o forma
palabra) unidas por `AND`/`OR`/`NOT`, más nombres de condición (88).
- Un verbo no soportado se conserva como `Stmt::Unknown { verb,
tokens }` — el lowering jamás aborta.
- COBOL no separa statements con un símbolo: cada uno corta donde
empieza el verbo del siguiente. El parser usa palabras "frontera"
(verbos + terminadores `END-*`/`ELSE` + conectores `TO`/`GIVING`...)
para delimitar listas de operandos.
- Fuera de alcance v1: `EVALUATE`, `STRING`/`UNSTRING`, E/S de
ficheros, `PERFORM VARYING`, CICS, SQL embebido.
## Estado
`charka-bcd` (22 tests), `charka-lexer` (17 tests) y `charka-parser`
(15 tests) implementados y verdes. **Pendiente** — el resto del
transpilador (Fase D del plan macro):
`charka-bcd` (22 tests), `charka-lexer` (17 tests), `charka-parser`
(15 tests) y `charka-ir` (17 tests) implementados y verdes.
**Pendiente** — el resto del transpilador (Fase D del plan macro):
| crate pendiente | rol |
| ----------------- | ---------------------------------------------------- |
| `charka-ir` | representación intermedia (parseo de statements) |
| `charka-codegen` | emisión de Rust |
| `charka-shadow` | validador en sombra (original vs transpilado) |
| `charka-runtime` | runtime determinista (sobre `charka-bcd`) |
@@ -0,0 +1,15 @@
[package]
name = "charka-ir"
version.workspace = true
edition.workspace = true
rust-version.workspace = true
license.workspace = true
authors.workspace = true
publish.workspace = true
description = "charka-ir — representación intermedia: el AST de charka-parser con los statements del PROCEDURE ya parseados a instrucciones tipadas (MOVE, IF, PERFORM, COMPUTE...)."
[dependencies]
charka-parser = { path = "../charka-parser" }
[dev-dependencies]
charka-lexer = { path = "../charka-lexer" }
+200
View File
@@ -0,0 +1,200 @@
//! Los tipos del IR: el programa COBOL con su PROCEDURE division ya
//! parseada a instrucciones tipadas.
pub use charka_parser::{DataItem, Token};
/// Un programa COBOL en representación intermedia.
#[derive(Debug, Clone, PartialEq, Default)]
pub struct Ir {
/// El `PROGRAM-ID` ("" si el programa no lo declara).
pub program_id: String,
/// El modelo de datos — el árbol de [`DataItem`] tal cual lo
/// produjo `charka-parser`. Sirve de tabla de símbolos.
pub data: Vec<DataItem>,
/// Los párrafos del PROCEDURE, con sus statements ya tipados.
pub procedures: Vec<Procedure>,
}
/// Un párrafo del PROCEDURE: un nombre y un cuerpo de statements.
#[derive(Debug, Clone, PartialEq, Default)]
pub struct Procedure {
/// Nombre del párrafo en mayúsculas; "" para el párrafo implícito.
pub name: String,
/// Los statements del párrafo, en orden.
pub body: Vec<Stmt>,
}
/// Un operando: lo que puede ir donde se espera un valor.
#[derive(Debug, Clone, PartialEq)]
pub enum Operand {
/// Referencia a un dato, por nombre (en mayúsculas).
Data(String),
/// Literal numérico (texto, posiblemente con signo).
Num(String),
/// Literal de texto.
Str(String),
/// Constante figurativa (`ZERO`, `SPACE`...).
Figurative(Figurative),
}
/// Las constantes figurativas de COBOL.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Figurative {
Zero,
Space,
HighValue,
LowValue,
Quote,
Null,
}
/// Una expresión aritmética (la parte derecha de un `COMPUTE`).
#[derive(Debug, Clone, PartialEq)]
pub enum Expr {
/// Un operando hoja.
Operand(Operand),
/// Negación unaria.
Neg(Box<Expr>),
/// Operación binaria.
Binary {
op: BinOp,
lhs: Box<Expr>,
rhs: Box<Expr>,
},
}
/// Operador aritmético binario.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum BinOp {
Add,
Sub,
Mul,
Div,
/// Exponenciación (`**`).
Pow,
}
/// Una condición (la guarda de un `IF` o de un `PERFORM UNTIL`).
#[derive(Debug, Clone, PartialEq)]
pub enum Cond {
/// Comparación relacional `lhs op rhs`.
Compare {
lhs: Operand,
op: CmpOp,
rhs: Operand,
},
/// Un nombre de condición (un dato de nivel 88) usado solo.
Named(String),
/// Negación lógica.
Not(Box<Cond>),
/// Conjunción lógica.
And(Box<Cond>, Box<Cond>),
/// Disyunción lógica.
Or(Box<Cond>, Box<Cond>),
}
/// Operador relacional.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum CmpOp {
Eq,
Ne,
Lt,
Gt,
Le,
Ge,
}
/// Un statement del PROCEDURE division.
#[derive(Debug, Clone, PartialEq)]
pub enum Stmt {
/// `MOVE from TO to...`
Move { from: Operand, to: Vec<String> },
/// `DISPLAY items...`
Display { items: Vec<Operand> },
/// `ACCEPT into`
Accept { into: String },
/// `COMPUTE targets... [ROUNDED] = expr`
Compute {
targets: Vec<String>,
rounded: bool,
expr: Expr,
},
/// `ADD addends... TO to... [GIVING giving...]`
Add {
addends: Vec<Operand>,
to: Vec<String>,
giving: Vec<String>,
rounded: bool,
},
/// `SUBTRACT amounts... FROM from... [GIVING giving...]`
Subtract {
amounts: Vec<Operand>,
from: Vec<String>,
giving: Vec<String>,
rounded: bool,
},
/// `MULTIPLY left BY by [GIVING giving...]`
Multiply {
left: Operand,
by: Operand,
giving: Vec<String>,
rounded: bool,
},
/// `DIVIDE left {BY|INTO} right [GIVING giving...]`. `by_form` es
/// true para `BY` (`left/right`), false para `INTO` (`right/left`).
Divide {
left: Operand,
right: Operand,
by_form: bool,
giving: Vec<String>,
rounded: bool,
},
/// `IF cond [THEN] then_branch [ELSE else_branch] [END-IF]`
If {
cond: Cond,
then_branch: Vec<Stmt>,
else_branch: Vec<Stmt>,
},
/// `PERFORM ...` — ver [`Perform`].
Perform(Perform),
/// `GO TO target`
GoTo { target: String },
/// `STOP RUN`
StopRun,
/// `GOBACK`
Goback,
/// `EXIT` (y sus variantes `EXIT PROGRAM`/`PARAGRAPH`...).
Exit,
/// `CONTINUE` — el no-op de COBOL.
Continue,
/// Un verbo que la v1 no parsea: se conserva crudo para que las
/// etapas siguientes (o un humano) lo revisen.
Unknown { verb: String, tokens: Vec<Token> },
}
/// Un statement `PERFORM`: a quién ejecuta y cuántas veces.
#[derive(Debug, Clone, PartialEq)]
pub struct Perform {
pub target: PerformTarget,
pub control: PerformControl,
}
/// El cuerpo que un `PERFORM` ejecuta.
#[derive(Debug, Clone, PartialEq)]
pub enum PerformTarget {
/// `PERFORM PARA [THRU PARA2]` — ejecuta uno o un rango de párrafos.
Paragraph { name: String, thru: Option<String> },
/// `PERFORM ... statements ... END-PERFORM` — cuerpo en línea.
Inline(Vec<Stmt>),
}
/// Cuántas veces se ejecuta el cuerpo de un `PERFORM`.
#[derive(Debug, Clone, PartialEq)]
pub enum PerformControl {
/// Una sola vez.
Once,
/// `n TIMES`.
Times(Operand),
/// `UNTIL cond`.
Until(Cond),
}
@@ -0,0 +1,139 @@
//! `Cursor` — un cursor de avance sobre la lista de tokens de una
//! sentencia, más las primitivas para leer un operando.
use charka_parser::{Token, TokenKind};
use crate::ast::{Figurative, Operand};
/// Cursor sobre los tokens de una sentencia. `pos` es público dentro
/// del crate para que el parser de condiciones pueda rebobinar.
pub(crate) struct Cursor<'a> {
pub(crate) toks: &'a [Token],
pub(crate) pos: usize,
}
impl<'a> Cursor<'a> {
pub(crate) fn new(toks: &'a [Token]) -> Self {
Self { toks, pos: 0 }
}
/// ¿Se agotaron los tokens?
pub(crate) fn done(&self) -> bool {
self.pos >= self.toks.len()
}
/// El token actual, sin consumirlo.
pub(crate) fn peek(&self) -> Option<&Token> {
self.toks.get(self.pos)
}
/// El token `n` posiciones adelante, sin consumirlo.
pub(crate) fn peek_at(&self, n: usize) -> Option<&Token> {
self.toks.get(self.pos + n)
}
/// Consume y devuelve el token actual.
pub(crate) fn bump(&mut self) -> Option<Token> {
let t = self.toks.get(self.pos).cloned();
if t.is_some() {
self.pos += 1;
}
t
}
/// El token actual, si es una palabra, en mayúsculas.
pub(crate) fn peek_word(&self) -> Option<String> {
word_of(self.peek())
}
/// La palabra `n` posiciones adelante, en mayúsculas.
pub(crate) fn word_at(&self, n: usize) -> Option<String> {
word_of(self.peek_at(n))
}
/// ¿El token actual es la palabra `kw`?
pub(crate) fn at_word(&self, kw: &str) -> bool {
self.peek_word().as_deref() == Some(kw)
}
/// Consume el token actual si es la palabra `kw`.
pub(crate) fn eat_word(&mut self, kw: &str) -> bool {
if self.at_word(kw) {
self.pos += 1;
true
} else {
false
}
}
/// ¿El token actual es el símbolo `s`?
pub(crate) fn at_sym(&self, s: &str) -> bool {
matches!(self.peek(), Some(t) if t.kind == TokenKind::Symbol && t.text == s)
}
/// Consume el token actual si es el símbolo `s`.
pub(crate) fn eat_sym(&mut self, s: &str) -> bool {
if self.at_sym(s) {
self.pos += 1;
true
} else {
false
}
}
}
/// Si el token es una palabra, su texto en mayúsculas.
fn word_of(t: Option<&Token>) -> Option<String> {
match t {
Some(t) if t.kind == TokenKind::Word => Some(t.text.to_uppercase()),
_ => None,
}
}
/// Lee un operando: un literal con signo opcional, o un token suelto.
pub(crate) fn parse_operand(c: &mut Cursor) -> Operand {
// Signo delante de un literal numérico (`-5`, `+3`).
if (c.at_sym("-") || c.at_sym("+")) && c.peek_at(1).map(|t| t.kind) == Some(TokenKind::Number) {
let neg = c.at_sym("-");
c.bump();
let num = c.bump().expect("número tras el signo");
return Operand::Num(if neg {
format!("-{}", num.text)
} else {
num.text
});
}
match c.bump() {
Some(t) => token_to_operand(&t),
None => Operand::Num("0".into()),
}
}
/// Clasifica un token suelto como operando.
pub(crate) fn token_to_operand(t: &Token) -> Operand {
match t.kind {
TokenKind::Number => Operand::Num(t.text.clone()),
TokenKind::String => Operand::Str(t.text.clone()),
TokenKind::Word => {
let u = t.text.to_uppercase();
match figurative(&u) {
Some(f) => Operand::Figurative(f),
None => Operand::Data(u),
}
}
TokenKind::Period | TokenKind::Symbol => Operand::Data(t.text.clone()),
}
}
/// Reconoce una constante figurativa por su nombre en mayúsculas.
fn figurative(w: &str) -> Option<Figurative> {
Some(match w {
"ZERO" | "ZEROS" | "ZEROES" => Figurative::Zero,
"SPACE" | "SPACES" => Figurative::Space,
"HIGH-VALUE" | "HIGH-VALUES" => Figurative::HighValue,
"LOW-VALUE" | "LOW-VALUES" => Figurative::LowValue,
"QUOTE" | "QUOTES" => Figurative::Quote,
"NULL" | "NULLS" => Figurative::Null,
_ => return None,
})
}
+208
View File
@@ -0,0 +1,208 @@
//! Parseo de expresiones aritméticas (`COMPUTE`) y de condiciones
//! (`IF`, `PERFORM UNTIL`).
use charka_parser::TokenKind;
use crate::ast::{BinOp, CmpOp, Cond, Expr, Operand};
use crate::cursor::{parse_operand, Cursor};
use crate::kw::is_boundary;
// ── Expresiones ───────────────────────────────────────────────────
/// Parsea una expresión aritmética con precedencia y paréntesis.
pub(crate) fn parse_expr(c: &mut Cursor) -> Expr {
parse_bin(c, 0)
}
/// Trepa por precedencia: `min_prec` es la mínima precedencia que este
/// nivel acepta seguir consumiendo.
fn parse_bin(c: &mut Cursor, min_prec: u8) -> Expr {
let mut lhs = parse_unary(c);
while let Some((op, prec, right_assoc)) = peek_binop(c) {
if prec < min_prec {
break;
}
c.bump();
let next_min = if right_assoc { prec } else { prec + 1 };
let rhs = parse_bin(c, next_min);
lhs = Expr::Binary {
op,
lhs: Box::new(lhs),
rhs: Box::new(rhs),
};
}
lhs
}
/// Negación o signo unario delante de un primario.
fn parse_unary(c: &mut Cursor) -> Expr {
if c.eat_sym("-") {
return Expr::Neg(Box::new(parse_unary(c)));
}
if c.eat_sym("+") {
return parse_unary(c);
}
parse_primary(c)
}
/// Un primario: un paréntesis o un operando hoja.
fn parse_primary(c: &mut Cursor) -> Expr {
if c.eat_sym("(") {
let e = parse_bin(c, 0);
c.eat_sym(")");
return e;
}
// No consumir un verbo o conector: la expresión terminó.
if c.done() || c.peek_word().map(|w| is_boundary(&w)).unwrap_or(false) {
return Expr::Operand(Operand::Num("0".into()));
}
Expr::Operand(parse_operand(c))
}
/// El operador binario en el token actual, con su precedencia y si es
/// asociativo a derecha. `**` es la única potencia, asociativa a der.
fn peek_binop(c: &Cursor) -> Option<(BinOp, u8, bool)> {
let t = c.peek()?;
if t.kind != TokenKind::Symbol {
return None;
}
match t.text.as_str() {
"+" => Some((BinOp::Add, 1, false)),
"-" => Some((BinOp::Sub, 1, false)),
"*" => Some((BinOp::Mul, 2, false)),
"/" => Some((BinOp::Div, 2, false)),
"**" => Some((BinOp::Pow, 3, true)),
_ => None,
}
}
// ── Condiciones ───────────────────────────────────────────────────
/// Parsea una condición: comparaciones unidas por `AND`/`OR`/`NOT`.
pub(crate) fn parse_cond(c: &mut Cursor) -> Cond {
parse_or(c)
}
fn parse_or(c: &mut Cursor) -> Cond {
let mut lhs = parse_and(c);
while c.eat_word("OR") {
let rhs = parse_and(c);
lhs = Cond::Or(Box::new(lhs), Box::new(rhs));
}
lhs
}
fn parse_and(c: &mut Cursor) -> Cond {
let mut lhs = parse_not(c);
while c.eat_word("AND") {
let rhs = parse_not(c);
lhs = Cond::And(Box::new(lhs), Box::new(rhs));
}
lhs
}
fn parse_not(c: &mut Cursor) -> Cond {
if c.eat_word("NOT") {
return Cond::Not(Box::new(parse_not(c)));
}
parse_cond_primary(c)
}
/// Un primario de condición: un paréntesis, una comparación, o un dato
/// suelto (un nombre de condición de nivel 88).
fn parse_cond_primary(c: &mut Cursor) -> Cond {
if c.eat_sym("(") {
let inner = parse_or(c);
c.eat_sym(")");
return inner;
}
if c.done() || c.peek_word().map(|w| is_boundary(&w)).unwrap_or(false) {
return Cond::Named(String::new());
}
let lhs = parse_operand(c);
match parse_cmp_op(c) {
Some(op) => {
let rhs = parse_operand(c);
Cond::Compare { lhs, op, rhs }
}
None => match lhs {
Operand::Data(n) => Cond::Named(n),
// Un literal solo como condición es raro; se degrada a "≠ 0".
other => Cond::Compare {
lhs: other,
op: CmpOp::Ne,
rhs: Operand::Num("0".into()),
},
},
}
}
/// Lee un operador relacional (forma símbolo o forma palabra). Si no
/// hay ninguno, rebobina el cursor y devuelve `None`.
fn parse_cmp_op(c: &mut Cursor) -> Option<CmpOp> {
let save = c.pos;
c.eat_word("IS");
let negated = c.eat_word("NOT");
if let Some(op) = cmp_core(c) {
return Some(if negated { negate(op) } else { op });
}
c.pos = save;
None
}
/// El núcleo del comparador, sin el `IS`/`NOT` opcionales.
fn cmp_core(c: &mut Cursor) -> Option<CmpOp> {
if c.eat_sym("<>") {
return Some(CmpOp::Ne);
}
if c.eat_sym("<=") {
return Some(CmpOp::Le);
}
if c.eat_sym(">=") {
return Some(CmpOp::Ge);
}
if c.eat_sym("=") {
return Some(CmpOp::Eq);
}
if c.eat_sym("<") {
return Some(CmpOp::Lt);
}
if c.eat_sym(">") {
return Some(CmpOp::Gt);
}
if c.eat_word("EQUAL") || c.eat_word("EQUALS") {
c.eat_word("TO");
return Some(CmpOp::Eq);
}
if c.eat_word("GREATER") {
c.eat_word("THAN");
if c.eat_word("OR") {
c.eat_word("EQUAL");
c.eat_word("TO");
return Some(CmpOp::Ge);
}
return Some(CmpOp::Gt);
}
if c.eat_word("LESS") {
c.eat_word("THAN");
if c.eat_word("OR") {
c.eat_word("EQUAL");
c.eat_word("TO");
return Some(CmpOp::Le);
}
return Some(CmpOp::Lt);
}
None
}
/// El comparador opuesto — para resolver `NOT` delante de un relacional.
fn negate(op: CmpOp) -> CmpOp {
match op {
CmpOp::Eq => CmpOp::Ne,
CmpOp::Ne => CmpOp::Eq,
CmpOp::Lt => CmpOp::Ge,
CmpOp::Ge => CmpOp::Lt,
CmpOp::Gt => CmpOp::Le,
CmpOp::Le => CmpOp::Gt,
}
}
+101
View File
@@ -0,0 +1,101 @@
//! Predicados sobre las palabras clave de COBOL. COBOL no termina los
//! statements con un símbolo: una sentencia es una secuencia de
//! statements que se delimitan por la aparición del siguiente verbo.
//! Estos predicados le dicen al parser dónde corta un statement.
/// ¿Es `w` un verbo que inicia un statement? (En mayúsculas.)
pub(crate) fn is_verb(w: &str) -> bool {
matches!(
w,
"ACCEPT"
| "ADD"
| "ALTER"
| "CALL"
| "CANCEL"
| "CLOSE"
| "COMPUTE"
| "CONTINUE"
| "DELETE"
| "DISPLAY"
| "DIVIDE"
| "EVALUATE"
| "EXIT"
| "GO"
| "GOBACK"
| "IF"
| "INITIALIZE"
| "INSPECT"
| "MERGE"
| "MOVE"
| "MULTIPLY"
| "OPEN"
| "PERFORM"
| "READ"
| "RELEASE"
| "RETURN"
| "REWRITE"
| "SEARCH"
| "SET"
| "SORT"
| "START"
| "STOP"
| "STRING"
| "SUBTRACT"
| "UNSTRING"
| "WRITE"
)
}
/// ¿Es `w` un terminador de ámbito (`END-IF`, `ELSE`, `WHEN`...)?
pub(crate) fn is_terminator(w: &str) -> bool {
matches!(
w,
"ELSE"
| "WHEN"
| "END-IF"
| "END-PERFORM"
| "END-EVALUATE"
| "END-ADD"
| "END-SUBTRACT"
| "END-MULTIPLY"
| "END-DIVIDE"
| "END-COMPUTE"
| "END-READ"
| "END-WRITE"
| "END-CALL"
| "END-STRING"
| "END-UNSTRING"
| "END-SEARCH"
| "END-START"
| "END-DELETE"
| "END-REWRITE"
| "END-RETURN"
)
}
/// ¿Es `w` una palabra de conexión de una cláusula (`TO`, `GIVING`...)?
fn is_connector(w: &str) -> bool {
matches!(
w,
"TO" | "FROM"
| "GIVING"
| "BY"
| "INTO"
| "THRU"
| "THROUGH"
| "UNTIL"
| "TIMES"
| "ROUNDED"
| "THEN"
| "WITH"
| "UPON"
| "REMAINDER"
| "VARYING"
)
}
/// ¿Marca `w` el final de una lista de operandos o de nombres? Es así
/// para todo verbo, terminador o conector — ninguno puede ser un dato.
pub(crate) fn is_boundary(w: &str) -> bool {
is_verb(w) || is_terminator(w) || is_connector(w)
}
+329
View File
@@ -0,0 +1,329 @@
//! `charka-ir` — la representación intermedia del transpilador.
//!
//! Tercera etapa del pipeline COBOL→Rust: toma el [`Program`] de
//! `charka-parser` (cuyo PROCEDURE division es una lista de sentencias
//! con tokens crudos) y produce un [`Ir`] donde cada sentencia ya es un
//! árbol de [`Stmt`] tipados: `MOVE`, `IF`, `PERFORM`, `COMPUTE`, los
//! verbos aritméticos, etc.
//!
//! El modelo de datos (`DATA division`) pasa tal cual — el árbol de
//! [`DataItem`] que ya armó el parser sirve de tabla de símbolos.
//!
//! El lowering es **tolerante y total**: nunca falla. Un verbo que la
//! v1 no parsea se conserva como [`Stmt::Unknown`] con sus tokens
//! crudos, listo para que el codegen (o un humano) lo revise.
//!
//! Alcance v1 — los verbos parseados a fondo: `MOVE`, `DISPLAY`,
//! `ACCEPT`, `COMPUTE` (con expresiones con precedencia), `ADD`,
//! `SUBTRACT`, `MULTIPLY`, `DIVIDE`, `IF`/`ELSE`/`END-IF` (con
//! condiciones `AND`/`OR`/`NOT`), `PERFORM` (fuera de línea, en línea,
//! `TIMES`, `UNTIL`), `GO TO`, `STOP RUN`, `GOBACK`, `EXIT`,
//! `CONTINUE`. Fuera de alcance: `EVALUATE`, `STRING`/`UNSTRING`, E/S
//! de ficheros, `PERFORM VARYING`, CICS y SQL embebido.
#![forbid(unsafe_code)]
mod ast;
mod cursor;
mod expr;
mod kw;
mod stmt;
pub use ast::*;
pub use charka_parser::Program;
use cursor::Cursor;
/// Baja un [`Program`] parseado a la representación intermedia.
pub fn lower(program: &Program) -> Ir {
let procedures = program
.paragraphs
.iter()
.map(|p| {
let mut body = Vec::new();
for sentence in &p.sentences {
let mut cur = Cursor::new(&sentence.tokens);
body.extend(stmt::parse_statements(&mut cur, &[]));
}
Procedure {
name: p.name.clone(),
body,
}
})
.collect();
Ir {
program_id: program.program_id.clone().unwrap_or_default(),
data: program.data.clone(),
procedures,
}
}
#[cfg(test)]
mod tests {
use super::*;
use charka_lexer::{lex, SourceFormat};
/// Helper: lexa, parsea y baja a IR un fuente en formato libre.
fn ir(src: &str) -> Ir {
let toks = lex(src, SourceFormat::Free).expect("lex OK");
let program = charka_parser::parse(&toks).expect("parse OK");
lower(&program)
}
/// Helper: el cuerpo del primer (y normalmente único) párrafo.
fn body(src: &str) -> Vec<Stmt> {
let prog = format!("PROCEDURE DIVISION.\nMAIN.\n{src}\n");
ir(&prog).procedures.into_iter().next().unwrap().body
}
#[test]
fn empty_program_lowers_to_default() {
let got = lower(&charka_parser::parse(&[]).unwrap());
assert_eq!(got, Ir::default());
}
#[test]
fn move_simple() {
let b = body("MOVE 5 TO WS-X.");
assert_eq!(
b,
vec![Stmt::Move {
from: Operand::Num("5".into()),
to: vec!["WS-X".into()],
}]
);
}
#[test]
fn move_to_several_targets() {
let b = body("MOVE WS-A TO WS-B WS-C.");
assert_eq!(
b,
vec![Stmt::Move {
from: Operand::Data("WS-A".into()),
to: vec!["WS-B".into(), "WS-C".into()],
}]
);
}
#[test]
fn display_items_and_figurative() {
let b = body("DISPLAY 'TOTAL: ' WS-TOTAL SPACES.");
assert_eq!(
b,
vec![Stmt::Display {
items: vec![
Operand::Str("TOTAL: ".into()),
Operand::Data("WS-TOTAL".into()),
Operand::Figurative(Figurative::Space),
],
}]
);
}
#[test]
fn compute_respects_precedence() {
let b = body("COMPUTE WS-T = WS-A + WS-B * 2.");
let expr = match &b[0] {
Stmt::Compute { targets, expr, .. } => {
assert_eq!(targets, &vec!["WS-T".to_string()]);
expr.clone()
}
other => panic!("se esperaba COMPUTE, vino {other:?}"),
};
// WS-A + (WS-B * 2)
assert_eq!(
expr,
Expr::Binary {
op: BinOp::Add,
lhs: Box::new(Expr::Operand(Operand::Data("WS-A".into()))),
rhs: Box::new(Expr::Binary {
op: BinOp::Mul,
lhs: Box::new(Expr::Operand(Operand::Data("WS-B".into()))),
rhs: Box::new(Expr::Operand(Operand::Num("2".into()))),
}),
}
);
}
#[test]
fn compute_rounded_flag() {
let b = body("COMPUTE WS-T ROUNDED = WS-A / 3.");
assert!(matches!(&b[0], Stmt::Compute { rounded: true, .. }));
}
#[test]
fn add_in_place_and_giving() {
assert_eq!(
body("ADD 1 TO WS-CT."),
vec![Stmt::Add {
addends: vec![Operand::Num("1".into())],
to: vec!["WS-CT".into()],
giving: vec![],
rounded: false,
}]
);
assert_eq!(
body("ADD WS-A WS-B GIVING WS-C."),
vec![Stmt::Add {
addends: vec![Operand::Data("WS-A".into()), Operand::Data("WS-B".into()),],
to: vec![],
giving: vec!["WS-C".into()],
rounded: false,
}]
);
}
#[test]
fn subtract_from_giving() {
assert_eq!(
body("SUBTRACT WS-TAX FROM WS-GROSS GIVING WS-NET."),
vec![Stmt::Subtract {
amounts: vec![Operand::Data("WS-TAX".into())],
from: vec!["WS-GROSS".into()],
giving: vec!["WS-NET".into()],
rounded: false,
}]
);
}
#[test]
fn divide_by_and_into() {
assert!(matches!(
&body("DIVIDE WS-A BY WS-B GIVING WS-C.")[0],
Stmt::Divide { by_form: true, .. }
));
assert!(matches!(
&body("DIVIDE WS-A INTO WS-B.")[0],
Stmt::Divide { by_form: false, .. }
));
}
#[test]
fn if_else_end_if() {
let b = body("IF WS-X > 0 DISPLAY 'POS' ELSE DISPLAY 'NEG' END-IF.");
match &b[0] {
Stmt::If {
cond,
then_branch,
else_branch,
} => {
assert_eq!(
cond,
&Cond::Compare {
lhs: Operand::Data("WS-X".into()),
op: CmpOp::Gt,
rhs: Operand::Num("0".into()),
}
);
assert_eq!(then_branch.len(), 1);
assert_eq!(else_branch.len(), 1);
}
other => panic!("se esperaba IF, vino {other:?}"),
}
}
#[test]
fn if_condition_with_and() {
let b = body("IF A = 1 AND B = 2 CONTINUE END-IF.");
match &b[0] {
Stmt::If { cond, .. } => {
assert!(matches!(cond, Cond::And(_, _)));
}
other => panic!("se esperaba IF, vino {other:?}"),
}
}
#[test]
fn if_named_condition() {
// Un dato suelto en la condición es un nombre de condición (88).
let b = body("IF FLAG-IS-OK MOVE 1 TO X END-IF.");
match &b[0] {
Stmt::If { cond, .. } => {
assert_eq!(cond, &Cond::Named("FLAG-IS-OK".into()));
}
other => panic!("se esperaba IF, vino {other:?}"),
}
}
#[test]
fn perform_paragraph_and_times() {
assert_eq!(
body("PERFORM SUB-PARA."),
vec![Stmt::Perform(Perform {
target: PerformTarget::Paragraph {
name: "SUB-PARA".into(),
thru: None,
},
control: PerformControl::Once,
})]
);
assert_eq!(
body("PERFORM SUB-PARA 3 TIMES."),
vec![Stmt::Perform(Perform {
target: PerformTarget::Paragraph {
name: "SUB-PARA".into(),
thru: None,
},
control: PerformControl::Times(Operand::Num("3".into())),
})]
);
}
#[test]
fn perform_inline_until() {
let b = body("PERFORM UNTIL WS-DONE = 1 ADD 1 TO WS-CT END-PERFORM.");
match &b[0] {
Stmt::Perform(p) => {
assert!(matches!(p.control, PerformControl::Until(_)));
match &p.target {
PerformTarget::Inline(body) => assert_eq!(body.len(), 1),
other => panic!("se esperaba cuerpo en línea, vino {other:?}"),
}
}
other => panic!("se esperaba PERFORM, vino {other:?}"),
}
}
#[test]
fn several_statements_in_one_sentence() {
let b = body("MOVE 1 TO X DISPLAY X STOP RUN.");
assert_eq!(b.len(), 3);
assert!(matches!(b[0], Stmt::Move { .. }));
assert!(matches!(b[1], Stmt::Display { .. }));
assert_eq!(b[2], Stmt::StopRun);
}
#[test]
fn unrecognized_verb_becomes_unknown() {
let b = body("INSPECT WS-X TALLYING WS-N FOR ALL ' '.");
match &b[0] {
Stmt::Unknown { verb, tokens } => {
assert_eq!(verb, "INSPECT");
assert!(!tokens.is_empty());
}
other => panic!("se esperaba Unknown, vino {other:?}"),
}
}
#[test]
fn full_program_lowers() {
let program = ir("IDENTIFICATION DIVISION.\n\
PROGRAM-ID. ADDER.\n\
DATA DIVISION.\n\
WORKING-STORAGE SECTION.\n\
01 WS-A PIC 9(3) VALUE 10.\n\
01 WS-T PIC 9(4).\n\
PROCEDURE DIVISION.\n\
MAIN-PARA.\n\
COMPUTE WS-T = WS-A + 5.\n\
DISPLAY WS-T.\n\
STOP RUN.\n");
assert_eq!(program.program_id, "ADDER");
assert_eq!(program.data.len(), 2);
assert_eq!(program.procedures.len(), 1);
assert_eq!(program.procedures[0].name, "MAIN-PARA");
assert_eq!(program.procedures[0].body.len(), 3);
}
}
+402
View File
@@ -0,0 +1,402 @@
//! Parseo de los statements del PROCEDURE division. COBOL no separa
//! statements con un símbolo: cada uno termina donde empieza el verbo
//! del siguiente, por eso las listas de operandos se cortan al ver una
//! palabra "frontera" (ver [`crate::kw`]).
use charka_parser::TokenKind;
use crate::ast::{Operand, Perform, PerformControl, PerformTarget, Stmt};
use crate::cursor::{parse_operand, Cursor};
use crate::expr::{parse_cond, parse_expr};
use crate::kw::{is_boundary, is_terminator, is_verb};
/// Parsea statements hasta agotar los tokens o toparse con una palabra
/// de `stops` (los terminadores del bloque que llama).
pub(crate) fn parse_statements(c: &mut Cursor, stops: &[&str]) -> Vec<Stmt> {
let mut out = Vec::new();
while !c.done() {
if let Some(w) = c.peek_word() {
if stops.contains(&w.as_str()) {
break;
}
}
out.push(parse_one_stmt(c, stops));
}
out
}
/// Parsea un statement: despacha por el verbo. Todo parser consume al
/// menos un token, así que el bucle de [`parse_statements`] progresa.
fn parse_one_stmt(c: &mut Cursor, stops: &[&str]) -> Stmt {
match c.peek_word().unwrap_or_default().as_str() {
"MOVE" => parse_move(c),
"DISPLAY" => parse_display(c),
"ACCEPT" => parse_accept(c),
"COMPUTE" => parse_compute(c),
"ADD" => parse_add(c),
"SUBTRACT" => parse_subtract(c),
"MULTIPLY" => parse_multiply(c),
"DIVIDE" => parse_divide(c),
"IF" => parse_if(c),
"PERFORM" => parse_perform(c),
"GO" => parse_goto(c),
"STOP" => parse_stop(c),
"GOBACK" => {
c.bump();
Stmt::Goback
}
"EXIT" => parse_exit(c),
"CONTINUE" => {
c.bump();
Stmt::Continue
}
_ => parse_unknown(c, stops),
}
}
// ── Listas ────────────────────────────────────────────────────────
/// Lee una lista de nombres de dato (separados por comas opcionales),
/// hasta una palabra frontera. Consume las apariciones de `ROUNDED`.
fn parse_name_list(c: &mut Cursor, rounded: &mut bool) -> Vec<String> {
let mut names = Vec::new();
loop {
c.eat_sym(",");
if c.eat_word("ROUNDED") {
*rounded = true;
continue;
}
match c.peek_word() {
Some(w) if !is_boundary(&w) => {
c.bump();
names.push(w);
}
_ => break,
}
}
names
}
/// Lee una lista de operandos hasta una palabra frontera.
fn parse_operand_list(c: &mut Cursor) -> Vec<Operand> {
let mut ops = Vec::new();
loop {
c.eat_sym(",");
if c.done() {
break;
}
if let Some(w) = c.peek_word() {
if is_boundary(&w) {
break;
}
}
let is_start = matches!(
c.peek().map(|t| t.kind),
Some(TokenKind::Number | TokenKind::String | TokenKind::Word)
) || c.at_sym("-")
|| c.at_sym("+");
if !is_start {
break;
}
ops.push(parse_operand(c));
}
ops
}
/// Salta los tokens de una cláusula que la v1 no modela, hasta el
/// siguiente verbo o terminador de ámbito.
fn skip_to_stmt_boundary(c: &mut Cursor) {
while !c.done() {
if let Some(w) = c.peek_word() {
if is_verb(&w) || is_terminator(&w) {
break;
}
}
c.bump();
}
}
/// Lee un único nombre de dato, si lo hay y no es una palabra frontera.
fn parse_one_name(c: &mut Cursor) -> Option<String> {
match c.peek_word() {
Some(w) if !is_boundary(&w) => {
c.bump();
Some(w)
}
_ => None,
}
}
// ── Statements ────────────────────────────────────────────────────
fn parse_move(c: &mut Cursor) -> Stmt {
c.bump(); // MOVE
c.eat_word("CORRESPONDING");
c.eat_word("CORR");
let from = parse_operand(c);
c.eat_word("TO");
let mut rounded = false;
let to = parse_name_list(c, &mut rounded);
Stmt::Move { from, to }
}
fn parse_display(c: &mut Cursor) -> Stmt {
c.bump(); // DISPLAY
let items = parse_operand_list(c);
skip_to_stmt_boundary(c); // p. ej. `WITH NO ADVANCING`, `UPON ...`
Stmt::Display { items }
}
fn parse_accept(c: &mut Cursor) -> Stmt {
c.bump(); // ACCEPT
let into = parse_one_name(c).unwrap_or_default();
skip_to_stmt_boundary(c); // p. ej. `FROM DATE`
Stmt::Accept { into }
}
fn parse_compute(c: &mut Cursor) -> Stmt {
c.bump(); // COMPUTE
let mut rounded = false;
let targets = parse_name_list(c, &mut rounded);
if !c.eat_sym("=") {
c.eat_word("EQUAL");
}
let expr = parse_expr(c);
c.eat_word("END-COMPUTE");
Stmt::Compute {
targets,
rounded,
expr,
}
}
fn parse_add(c: &mut Cursor) -> Stmt {
c.bump(); // ADD
c.eat_word("CORRESPONDING");
c.eat_word("CORR");
let addends = parse_operand_list(c);
let mut rounded = false;
let mut to = Vec::new();
let mut giving = Vec::new();
if c.eat_word("TO") {
to = parse_name_list(c, &mut rounded);
}
if c.eat_word("GIVING") {
giving = parse_name_list(c, &mut rounded);
}
c.eat_word("END-ADD");
Stmt::Add {
addends,
to,
giving,
rounded,
}
}
fn parse_subtract(c: &mut Cursor) -> Stmt {
c.bump(); // SUBTRACT
c.eat_word("CORRESPONDING");
c.eat_word("CORR");
let amounts = parse_operand_list(c);
let mut rounded = false;
let mut from = Vec::new();
let mut giving = Vec::new();
if c.eat_word("FROM") {
from = parse_name_list(c, &mut rounded);
}
if c.eat_word("GIVING") {
giving = parse_name_list(c, &mut rounded);
}
c.eat_word("END-SUBTRACT");
Stmt::Subtract {
amounts,
from,
giving,
rounded,
}
}
fn parse_multiply(c: &mut Cursor) -> Stmt {
c.bump(); // MULTIPLY
let left = parse_operand(c);
c.eat_word("BY");
let by = parse_operand(c);
let mut rounded = false;
let mut giving = Vec::new();
if c.eat_word("GIVING") {
giving = parse_name_list(c, &mut rounded);
} else if c.eat_word("ROUNDED") {
rounded = true;
}
c.eat_word("END-MULTIPLY");
Stmt::Multiply {
left,
by,
giving,
rounded,
}
}
fn parse_divide(c: &mut Cursor) -> Stmt {
c.bump(); // DIVIDE
let left = parse_operand(c);
let by_form = if c.eat_word("BY") {
true
} else {
c.eat_word("INTO");
false
};
let right = parse_operand(c);
let mut rounded = false;
let mut giving = Vec::new();
if c.eat_word("GIVING") {
giving = parse_name_list(c, &mut rounded);
} else if c.eat_word("ROUNDED") {
rounded = true;
}
if c.eat_word("REMAINDER") {
let _ = parse_name_list(c, &mut rounded);
}
c.eat_word("END-DIVIDE");
Stmt::Divide {
left,
right,
by_form,
giving,
rounded,
}
}
fn parse_if(c: &mut Cursor) -> Stmt {
c.bump(); // IF
let cond = parse_cond(c);
c.eat_word("THEN");
let then_branch = parse_statements(c, &["ELSE", "END-IF"]);
let else_branch = if c.eat_word("ELSE") {
parse_statements(c, &["END-IF"])
} else {
Vec::new()
};
c.eat_word("END-IF");
Stmt::If {
cond,
then_branch,
else_branch,
}
}
fn parse_perform(c: &mut Cursor) -> Stmt {
c.bump(); // PERFORM
// `PERFORM UNTIL cond ... END-PERFORM` — cuerpo en línea.
if c.eat_word("UNTIL") {
let cond = parse_cond(c);
let body = parse_statements(c, &["END-PERFORM"]);
c.eat_word("END-PERFORM");
return inline_perform(body, PerformControl::Until(cond));
}
// `PERFORM n TIMES ... END-PERFORM` — cuerpo en línea.
if matches!(c.peek().map(|t| t.kind), Some(TokenKind::Number)) {
let n = parse_operand(c);
c.eat_word("TIMES");
let body = parse_statements(c, &["END-PERFORM"]);
c.eat_word("END-PERFORM");
return inline_perform(body, PerformControl::Times(n));
}
// `PERFORM ... END-PERFORM` — cuerpo en línea, una vez.
if c.peek_word().map(|w| is_verb(&w)).unwrap_or(false) {
let body = parse_statements(c, &["END-PERFORM"]);
c.eat_word("END-PERFORM");
return inline_perform(body, PerformControl::Once);
}
// `PERFORM PARA [THRU PARA2] [n TIMES | UNTIL cond]` — fuera de línea.
let Some(name) = parse_one_name(c) else {
// Forma no soportada (p. ej. `PERFORM VARYING`): perform vacío.
return inline_perform(Vec::new(), PerformControl::Once);
};
let thru = if c.eat_word("THRU") || c.eat_word("THROUGH") {
parse_one_name(c)
} else {
None
};
let control = if c.eat_word("UNTIL") {
PerformControl::Until(parse_cond(c))
} else if at_count(c) {
let n = parse_operand(c);
c.eat_word("TIMES");
PerformControl::Times(n)
} else {
PerformControl::Once
};
Stmt::Perform(Perform {
target: PerformTarget::Paragraph { name, thru },
control,
})
}
/// Arma un `PERFORM` con cuerpo en línea.
fn inline_perform(body: Vec<Stmt>, control: PerformControl) -> Stmt {
Stmt::Perform(Perform {
target: PerformTarget::Inline(body),
control,
})
}
/// ¿El cursor está sobre `<operando> TIMES`?
fn at_count(c: &Cursor) -> bool {
match c.peek().map(|t| t.kind) {
Some(TokenKind::Number) => true,
Some(TokenKind::Word) => {
let w = c.peek_word().unwrap_or_default();
!is_boundary(&w) && c.word_at(1).as_deref() == Some("TIMES")
}
_ => false,
}
}
fn parse_goto(c: &mut Cursor) -> Stmt {
c.bump(); // GO
c.eat_word("TO");
Stmt::GoTo {
target: parse_one_name(c).unwrap_or_default(),
}
}
fn parse_stop(c: &mut Cursor) -> Stmt {
c.bump(); // STOP
c.eat_word("RUN");
Stmt::StopRun
}
fn parse_exit(c: &mut Cursor) -> Stmt {
c.bump(); // EXIT
c.eat_word("PROGRAM");
c.eat_word("PARAGRAPH");
c.eat_word("PERFORM");
c.eat_word("SECTION");
Stmt::Exit
}
/// Verbo no soportado: conserva el verbo y sus tokens hasta el próximo
/// statement (otro verbo), terminador de ámbito o tope del bloque.
fn parse_unknown(c: &mut Cursor, stops: &[&str]) -> Stmt {
let verb = c.peek_word().unwrap_or_default();
let mut tokens = Vec::new();
if let Some(t) = c.bump() {
tokens.push(t);
}
while !c.done() {
if let Some(w) = c.peek_word() {
if stops.contains(&w.as_str()) || is_verb(&w) || is_terminator(&w) {
break;
}
}
if let Some(t) = c.bump() {
tokens.push(t);
}
}
Stmt::Unknown { verb, tokens }
}