From 71a4068d12e81d0be7b08d0717114dd5b10868fe Mon Sep 17 00:00:00 2001 From: sergio Date: Thu, 21 May 2026 20:23:19 +0000 Subject: [PATCH] =?UTF-8?q?feat(charka):=20charka-ir=20=E2=80=94=20represe?= =?UTF-8?q?ntaci=C3=B3n=20intermedia=20con=20statements=20tipados?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- Cargo.lock | 8 + Cargo.toml | 1 + crates/modules/charka/SDD.md | 32 +- crates/modules/charka/charka-ir/Cargo.toml | 15 + crates/modules/charka/charka-ir/src/ast.rs | 200 +++++++++ crates/modules/charka/charka-ir/src/cursor.rs | 139 ++++++ crates/modules/charka/charka-ir/src/expr.rs | 208 +++++++++ crates/modules/charka/charka-ir/src/kw.rs | 101 +++++ crates/modules/charka/charka-ir/src/lib.rs | 329 ++++++++++++++ crates/modules/charka/charka-ir/src/stmt.rs | 402 ++++++++++++++++++ docs/changelog/charka.md | 31 ++ 11 files changed, 1462 insertions(+), 4 deletions(-) create mode 100644 crates/modules/charka/charka-ir/Cargo.toml create mode 100644 crates/modules/charka/charka-ir/src/ast.rs create mode 100644 crates/modules/charka/charka-ir/src/cursor.rs create mode 100644 crates/modules/charka/charka-ir/src/expr.rs create mode 100644 crates/modules/charka/charka-ir/src/kw.rs create mode 100644 crates/modules/charka/charka-ir/src/lib.rs create mode 100644 crates/modules/charka/charka-ir/src/stmt.rs diff --git a/Cargo.lock b/Cargo.lock index 7a9fdaa..543eb95 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2315,6 +2315,14 @@ dependencies = [ "thiserror 2.0.18", ] +[[package]] +name = "charka-ir" +version = "0.1.0" +dependencies = [ + "charka-lexer", + "charka-parser", +] + [[package]] name = "charka-lexer" version = "0.1.0" diff --git a/Cargo.toml b/Cargo.toml index 9006621..4756036 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -159,6 +159,7 @@ members = [ "crates/modules/charka/charka-bcd", "crates/modules/charka/charka-lexer", "crates/modules/charka/charka-parser", + "crates/modules/charka/charka-ir", # ============================================================ # modules/mirada/ — Compositor Wayland diff --git a/crates/modules/charka/SDD.md b/crates/modules/charka/SDD.md index 722d541..8e8c52c 100644 --- a/crates/modules/charka/SDD.md +++ b/crates/modules/charka/SDD.md @@ -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, procedures: Vec }`. + El modelo de datos pasa tal cual (sirve de tabla de símbolos). +- `Procedure { name, body: Vec }`. `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`) | diff --git a/crates/modules/charka/charka-ir/Cargo.toml b/crates/modules/charka/charka-ir/Cargo.toml new file mode 100644 index 0000000..fabdade --- /dev/null +++ b/crates/modules/charka/charka-ir/Cargo.toml @@ -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" } diff --git a/crates/modules/charka/charka-ir/src/ast.rs b/crates/modules/charka/charka-ir/src/ast.rs new file mode 100644 index 0000000..5ea48b0 --- /dev/null +++ b/crates/modules/charka/charka-ir/src/ast.rs @@ -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, + /// Los párrafos del PROCEDURE, con sus statements ya tipados. + pub procedures: Vec, +} + +/// 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, +} + +/// 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), + /// Operación binaria. + Binary { + op: BinOp, + lhs: Box, + rhs: Box, + }, +} + +/// 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), + /// Conjunción lógica. + And(Box, Box), + /// Disyunción lógica. + Or(Box, Box), +} + +/// 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 }, + /// `DISPLAY items...` + Display { items: Vec }, + /// `ACCEPT into` + Accept { into: String }, + /// `COMPUTE targets... [ROUNDED] = expr` + Compute { + targets: Vec, + rounded: bool, + expr: Expr, + }, + /// `ADD addends... TO to... [GIVING giving...]` + Add { + addends: Vec, + to: Vec, + giving: Vec, + rounded: bool, + }, + /// `SUBTRACT amounts... FROM from... [GIVING giving...]` + Subtract { + amounts: Vec, + from: Vec, + giving: Vec, + rounded: bool, + }, + /// `MULTIPLY left BY by [GIVING giving...]` + Multiply { + left: Operand, + by: Operand, + giving: Vec, + 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, + rounded: bool, + }, + /// `IF cond [THEN] then_branch [ELSE else_branch] [END-IF]` + If { + cond: Cond, + then_branch: Vec, + else_branch: Vec, + }, + /// `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 }, +} + +/// 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 }, + /// `PERFORM ... statements ... END-PERFORM` — cuerpo en línea. + Inline(Vec), +} + +/// 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), +} diff --git a/crates/modules/charka/charka-ir/src/cursor.rs b/crates/modules/charka/charka-ir/src/cursor.rs new file mode 100644 index 0000000..51ac5a2 --- /dev/null +++ b/crates/modules/charka/charka-ir/src/cursor.rs @@ -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 { + 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 { + word_of(self.peek()) + } + + /// La palabra `n` posiciones adelante, en mayúsculas. + pub(crate) fn word_at(&self, n: usize) -> Option { + 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 { + 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 { + 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, + }) +} diff --git a/crates/modules/charka/charka-ir/src/expr.rs b/crates/modules/charka/charka-ir/src/expr.rs new file mode 100644 index 0000000..477290a --- /dev/null +++ b/crates/modules/charka/charka-ir/src/expr.rs @@ -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 { + 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 { + 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, + } +} diff --git a/crates/modules/charka/charka-ir/src/kw.rs b/crates/modules/charka/charka-ir/src/kw.rs new file mode 100644 index 0000000..b0542ca --- /dev/null +++ b/crates/modules/charka/charka-ir/src/kw.rs @@ -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) +} diff --git a/crates/modules/charka/charka-ir/src/lib.rs b/crates/modules/charka/charka-ir/src/lib.rs new file mode 100644 index 0000000..9bccad4 --- /dev/null +++ b/crates/modules/charka/charka-ir/src/lib.rs @@ -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 { + 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); + } +} diff --git a/crates/modules/charka/charka-ir/src/stmt.rs b/crates/modules/charka/charka-ir/src/stmt.rs new file mode 100644 index 0000000..4dff640 --- /dev/null +++ b/crates/modules/charka/charka-ir/src/stmt.rs @@ -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 { + 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 { + 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 { + 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 { + 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, control: PerformControl) -> Stmt { + Stmt::Perform(Perform { + target: PerformTarget::Inline(body), + control, + }) +} + +/// ¿El cursor está sobre ` 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 } +} diff --git a/docs/changelog/charka.md b/docs/changelog/charka.md index 7de7539..4c6d898 100644 --- a/docs/changelog/charka.md +++ b/docs/changelog/charka.md @@ -3,6 +3,37 @@ 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. +### 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 Crate nuevo `crates/modules/charka/charka-parser` — la segunda etapa del