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:
Generated
+8
@@ -2315,6 +2315,14 @@ dependencies = [
|
|||||||
"thiserror 2.0.18",
|
"thiserror 2.0.18",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "charka-ir"
|
||||||
|
version = "0.1.0"
|
||||||
|
dependencies = [
|
||||||
|
"charka-lexer",
|
||||||
|
"charka-parser",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "charka-lexer"
|
name = "charka-lexer"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
|
|||||||
@@ -159,6 +159,7 @@ members = [
|
|||||||
"crates/modules/charka/charka-bcd",
|
"crates/modules/charka/charka-bcd",
|
||||||
"crates/modules/charka/charka-lexer",
|
"crates/modules/charka/charka-lexer",
|
||||||
"crates/modules/charka/charka-parser",
|
"crates/modules/charka/charka-parser",
|
||||||
|
"crates/modules/charka/charka-ir",
|
||||||
|
|
||||||
# ============================================================
|
# ============================================================
|
||||||
# modules/mirada/ — Compositor Wayland
|
# modules/mirada/ — Compositor Wayland
|
||||||
|
|||||||
@@ -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-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-lexer` | lib | Tokenizador COBOL: formato fijo (tarjeta de 80 columnas) y libre |
|
||||||
| `charka-parser` | lib | Parser COBOL'85 (subconjunto): tokens → AST (`Program`) |
|
| `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
|
## 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
|
- Limitación v1: no parsea statements, ni la ENVIRONMENT division, ni
|
||||||
CICS / SQL embebido / dialectos IBM.
|
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
|
## Estado
|
||||||
|
|
||||||
`charka-bcd` (22 tests), `charka-lexer` (17 tests) y `charka-parser`
|
`charka-bcd` (22 tests), `charka-lexer` (17 tests), `charka-parser`
|
||||||
(15 tests) implementados y verdes. **Pendiente** — el resto del
|
(15 tests) y `charka-ir` (17 tests) implementados y verdes.
|
||||||
transpilador (Fase D del plan macro):
|
**Pendiente** — el resto del transpilador (Fase D del plan macro):
|
||||||
|
|
||||||
| crate pendiente | rol |
|
| crate pendiente | rol |
|
||||||
| ----------------- | ---------------------------------------------------- |
|
| ----------------- | ---------------------------------------------------- |
|
||||||
| `charka-ir` | representación intermedia (parseo de statements) |
|
|
||||||
| `charka-codegen` | emisión de Rust |
|
| `charka-codegen` | emisión de Rust |
|
||||||
| `charka-shadow` | validador en sombra (original vs transpilado) |
|
| `charka-shadow` | validador en sombra (original vs transpilado) |
|
||||||
| `charka-runtime` | runtime determinista (sobre `charka-bcd`) |
|
| `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 }
|
||||||
|
}
|
||||||
@@ -3,6 +3,37 @@
|
|||||||
Transpilador COBOL → Rust. El módulo más grande del ecosistema (Fase D
|
Transpilador COBOL → Rust. El módulo más grande del ecosistema (Fase D
|
||||||
del plan macro) — el parser COBOL completo es un esfuerzo multi-mes.
|
del plan macro) — el parser COBOL completo es un esfuerzo multi-mes.
|
||||||
|
|
||||||
|
### feat(charka-ir): representación intermedia — statements tipados
|
||||||
|
|
||||||
|
Crate nuevo `crates/modules/charka/charka-ir` — la tercera etapa del
|
||||||
|
pipeline: `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.
|
||||||
|
- `Ir { program_id, data, procedures }`. El modelo de datos (la DATA
|
||||||
|
division) pasa tal cual — 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 (`+ -` <
|
||||||
|
`* /` < `**` asociativo a derecha). Condiciones de `IF`/`UNTIL` con
|
||||||
|
comparadores en forma símbolo (`= < > <= >= <>`) o palabra
|
||||||
|
(`EQUAL TO`, `GREATER THAN`...), `AND`/`OR`/`NOT` y nombres de
|
||||||
|
condición (datos de nivel 88).
|
||||||
|
- COBOL no termina los statements con un símbolo: el parser delimita
|
||||||
|
las listas de operandos con palabras "frontera" (verbos,
|
||||||
|
terminadores `END-*`/`ELSE`, conectores `TO`/`GIVING`/`BY`...).
|
||||||
|
- Un verbo no soportado se conserva como `Stmt::Unknown { verb,
|
||||||
|
tokens }` — el lowering nunca aborta.
|
||||||
|
- Fuera de alcance v1: `EVALUATE`, `STRING`/`UNSTRING`, E/S de
|
||||||
|
ficheros, `PERFORM VARYING`, CICS, SQL embebido.
|
||||||
|
- 17 tests: MOVE simple y multi-destino, DISPLAY con figurativas,
|
||||||
|
precedencia de COMPUTE, flag ROUNDED, ADD in-place vs GIVING,
|
||||||
|
SUBTRACT, DIVIDE BY/INTO, IF/ELSE, condiciones con AND, nombre de
|
||||||
|
condición, PERFORM párrafo/TIMES/UNTIL en línea, varios statements
|
||||||
|
en una sentencia, verbo desconocido, programa completo.
|
||||||
|
|
||||||
### feat(charka-parser): parser COBOL'85 → AST
|
### feat(charka-parser): parser COBOL'85 → AST
|
||||||
|
|
||||||
Crate nuevo `crates/modules/charka/charka-parser` — la segunda etapa del
|
Crate nuevo `crates/modules/charka/charka-parser` — la segunda etapa del
|
||||||
|
|||||||
Reference in New Issue
Block a user