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:
@@ -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" }
|
||||
@@ -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,
|
||||
})
|
||||
}
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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 }
|
||||
}
|
||||
Reference in New Issue
Block a user