feat(charka): charka-parser — COBOL'85 (subconjunto) a AST
Segunda etapa del transpilador: Vec<Token> -> Program. Alcance v1 = el esqueleto del programa. - parse(&[Token]) -> Result<Program, ParseError>. AST: Program (program_id, data, paragraphs), DataItem, Paragraph, Sentence. - Particiona el flujo en las 4 divisions por sus encabezados; extrae el PROGRAM-ID de la IDENTIFICATION. - DATA division -> árbol de DataItem: nivel, nombre, PICTURE reensamblado (S9 ( 5 ) V99 -> S9(5)V99) y VALUE. Anida por número de nivel (01/77 raíces, 88 cuelga del precedente). - PROCEDURE division -> Vec<Paragraph> con Sentence de tokens crudos (sin parseo de statement). Sentencias previas al primer encabezado van a un párrafo implícito "". - Tolerante: salta SECTION, FD/SD y cláusulas que no sean PIC/VALUE. - 15 tests verdes; fmt + clippy limpios. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
Generated
+8
@@ -2322,6 +2322,14 @@ dependencies = [
|
|||||||
"thiserror 2.0.18",
|
"thiserror 2.0.18",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "charka-parser"
|
||||||
|
version = "0.1.0"
|
||||||
|
dependencies = [
|
||||||
|
"charka-lexer",
|
||||||
|
"thiserror 2.0.18",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "chasqui-card"
|
name = "chasqui-card"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
|
|||||||
@@ -158,6 +158,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",
|
||||||
|
|
||||||
# ============================================================
|
# ============================================================
|
||||||
# modules/mirada/ — Compositor Wayland
|
# modules/mirada/ — Compositor Wayland
|
||||||
|
|||||||
@@ -12,6 +12,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-bcd
|
## charka-bcd
|
||||||
|
|
||||||
@@ -43,16 +44,35 @@ Primera etapa del pipeline: texto COBOL → secuencia de `Token`.
|
|||||||
- Limitación v1: sin continuación de literales entre líneas
|
- Limitación v1: sin continuación de literales entre líneas
|
||||||
(indicador `-`).
|
(indicador `-`).
|
||||||
|
|
||||||
|
## charka-parser
|
||||||
|
|
||||||
|
Segunda etapa: tokens → AST. Alcance v1 — el **esqueleto del programa**.
|
||||||
|
|
||||||
|
- `parse(&[Token]) -> Result<Program, ParseError>`.
|
||||||
|
- `Program { program_id, data: Vec<DataItem>, paragraphs: Vec<Paragraph> }`.
|
||||||
|
- **DATA division** → árbol de `DataItem` (`level`, `name`, `picture`,
|
||||||
|
`value`, `children`). Reensambla la cláusula `PICTURE` desde sus
|
||||||
|
tokens (`S9` `(` `5` `)` `V99` → `S9(5)V99`); anida por número de
|
||||||
|
nivel (01 y 77 son raíces; 88 cuelga del ítem precedente).
|
||||||
|
- **PROCEDURE division** → `Vec<Paragraph>`, cada `Paragraph` con sus
|
||||||
|
`Sentence` (tokens crudos — sin parseo a nivel de statement). Las
|
||||||
|
sentencias previas al primer encabezado van a un párrafo implícito
|
||||||
|
de nombre "".
|
||||||
|
- Tolerante: salta encabezados de `SECTION`, entradas `FD`/`SD` y
|
||||||
|
cláusulas de datos que no sean `PICTURE`/`VALUE`. `ParseError` sólo
|
||||||
|
ante nivel inválido o dato sin nombre.
|
||||||
|
- Limitación v1: no parsea statements, ni la ENVIRONMENT division, ni
|
||||||
|
CICS / SQL embebido / dialectos IBM.
|
||||||
|
|
||||||
## Estado
|
## Estado
|
||||||
|
|
||||||
`charka-bcd` (22 tests) y `charka-lexer` (17 tests) implementados y
|
`charka-bcd` (22 tests), `charka-lexer` (17 tests) y `charka-parser`
|
||||||
verdes. **Pendiente** — el resto del transpilador (Fase D del plan
|
(15 tests) implementados y verdes. **Pendiente** — el resto del
|
||||||
macro):
|
transpilador (Fase D del plan macro):
|
||||||
|
|
||||||
| crate pendiente | rol |
|
| crate pendiente | rol |
|
||||||
| ----------------- | ---------------------------------------------------- |
|
| ----------------- | ---------------------------------------------------- |
|
||||||
| `charka-parser` | parser COBOL'85 → AST (luego CICS + SQL embebido) |
|
| `charka-ir` | representación intermedia (parseo de statements) |
|
||||||
| `charka-ir` | representación intermedia |
|
|
||||||
| `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,13 @@
|
|||||||
|
[package]
|
||||||
|
name = "charka-parser"
|
||||||
|
version.workspace = true
|
||||||
|
edition.workspace = true
|
||||||
|
rust-version.workspace = true
|
||||||
|
license.workspace = true
|
||||||
|
authors.workspace = true
|
||||||
|
publish.workspace = true
|
||||||
|
description = "charka-parser — parser de COBOL'85 (subconjunto) a AST: divisiones, modelo de datos (DATA division) y párrafos del PROCEDURE."
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
charka-lexer = { path = "../charka-lexer" }
|
||||||
|
thiserror = { workspace = true }
|
||||||
@@ -0,0 +1,694 @@
|
|||||||
|
//! `charka-parser` — parser de COBOL'85 (subconjunto) a AST.
|
||||||
|
//!
|
||||||
|
//! Segunda etapa del transpilador COBOL→Rust: consume los [`Token`] del
|
||||||
|
//! `charka-lexer` y produce un [`Program`]. El alcance de la v1 es el
|
||||||
|
//! **esqueleto del programa**:
|
||||||
|
//!
|
||||||
|
//! - Las cuatro divisiones (`IDENTIFICATION`, `ENVIRONMENT`, `DATA`,
|
||||||
|
//! `PROCEDURE`) y el `PROGRAM-ID`.
|
||||||
|
//! - La **DATA division** como un árbol de [`DataItem`]: número de
|
||||||
|
//! nivel, nombre, cláusula `PICTURE` reensamblada y `VALUE`.
|
||||||
|
//! - La **PROCEDURE division** como una lista de [`Paragraph`], cada
|
||||||
|
//! uno con sus [`Sentence`] (los tokens crudos de cada sentencia).
|
||||||
|
//!
|
||||||
|
//! Lo que **no** hace todavía: el parseo a nivel de statement (cada
|
||||||
|
//! sentencia queda como `Vec<Token>` — es trabajo de `charka-ir`), la
|
||||||
|
//! ENVIRONMENT division, CICS, SQL embebido y los dialectos IBM. Son
|
||||||
|
//! las etapas siguientes del plan.
|
||||||
|
//!
|
||||||
|
//! El parser es tolerante: salta lo que no entiende (encabezados de
|
||||||
|
//! `SECTION`, entradas `FD`/`SD`, cláusulas de datos que no sean
|
||||||
|
//! `PICTURE`/`VALUE`) en vez de fallar. Sólo emite [`ParseError`] ante
|
||||||
|
//! un número de nivel inválido o un dato sin nombre.
|
||||||
|
|
||||||
|
#![forbid(unsafe_code)]
|
||||||
|
|
||||||
|
use thiserror::Error;
|
||||||
|
|
||||||
|
pub use charka_lexer::{Token, TokenKind};
|
||||||
|
|
||||||
|
/// Un programa COBOL parseado: el esqueleto de sus cuatro divisiones.
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq, Default)]
|
||||||
|
pub struct Program {
|
||||||
|
/// El `PROGRAM-ID` de la IDENTIFICATION division, si está presente.
|
||||||
|
pub program_id: Option<String>,
|
||||||
|
/// Los ítems raíz de la DATA division (cada `01`/`77` con su árbol).
|
||||||
|
pub data: Vec<DataItem>,
|
||||||
|
/// Los párrafos de la PROCEDURE division, en orden de aparición.
|
||||||
|
pub paragraphs: Vec<Paragraph>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Un ítem de datos de la DATA division: un número de nivel, un nombre
|
||||||
|
/// y, opcionalmente, las cláusulas `PICTURE` y `VALUE`. Los ítems de
|
||||||
|
/// mayor nivel numérico cuelgan como `children` del que los contiene.
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
|
pub struct DataItem {
|
||||||
|
/// Número de nivel: 01-49 (jerárquico), 66 (`RENAMES`), 77
|
||||||
|
/// (elemental independiente) u 88 (nombre de condición).
|
||||||
|
pub level: u8,
|
||||||
|
/// Nombre del dato, normalizado a mayúsculas (`FILLER` incluido).
|
||||||
|
pub name: String,
|
||||||
|
/// Cláusula `PICTURE` reensamblada (`S9(5)V99`), si la hay.
|
||||||
|
pub picture: Option<String>,
|
||||||
|
/// Cláusula `VALUE`: literal numérico (con signo), constante
|
||||||
|
/// figurativa en mayúsculas, o literal de texto entre comillas.
|
||||||
|
pub value: Option<String>,
|
||||||
|
/// Ítems subordinados (de nivel numérico mayor).
|
||||||
|
pub children: Vec<DataItem>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Un párrafo de la PROCEDURE division. El párrafo implícito que
|
||||||
|
/// agrupa las sentencias previas al primer encabezado tiene `name` "".
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
|
pub struct Paragraph {
|
||||||
|
/// Nombre del párrafo en mayúsculas; "" para el párrafo implícito.
|
||||||
|
pub name: String,
|
||||||
|
/// Sentencias del párrafo, en orden.
|
||||||
|
pub sentences: Vec<Sentence>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Una sentencia: los tokens entre dos puntos terminadores. La v1 no
|
||||||
|
/// parsea a nivel de statement — eso es trabajo de `charka-ir`.
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
|
pub struct Sentence {
|
||||||
|
pub tokens: Vec<Token>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Error de parseo, con la línea del token donde se detectó.
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq, Error)]
|
||||||
|
pub enum ParseError {
|
||||||
|
#[error("línea {line}: número de nivel inválido {found:?} (esperado 01-49, 66, 77 u 88)")]
|
||||||
|
BadLevel { line: u32, found: String },
|
||||||
|
#[error("línea {line}: se esperaba el nombre de un dato tras el número de nivel")]
|
||||||
|
ExpectedDataName { line: u32 },
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Las cuatro divisiones de un programa COBOL.
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
enum DivKind {
|
||||||
|
Identification,
|
||||||
|
Environment,
|
||||||
|
Data,
|
||||||
|
Procedure,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Parsea una secuencia de tokens COBOL en un [`Program`].
|
||||||
|
pub fn parse(tokens: &[Token]) -> Result<Program, ParseError> {
|
||||||
|
let divs = find_divisions(tokens);
|
||||||
|
let mut program = Program::default();
|
||||||
|
for (idx, &(kind, start)) in divs.iter().enumerate() {
|
||||||
|
// El cuerpo de la división va desde el punto que cierra su
|
||||||
|
// encabezado hasta el comienzo de la división siguiente.
|
||||||
|
let lo = match period_after(tokens, start) {
|
||||||
|
Some(p) => p + 1,
|
||||||
|
None => tokens.len(),
|
||||||
|
};
|
||||||
|
let next = divs.get(idx + 1).map(|&(_, s)| s).unwrap_or(tokens.len());
|
||||||
|
let body = &tokens[lo..next.max(lo)];
|
||||||
|
match kind {
|
||||||
|
DivKind::Identification => parse_identification(body, &mut program),
|
||||||
|
DivKind::Environment => {} // la v1 ignora la ENVIRONMENT division
|
||||||
|
DivKind::Data => program.data = parse_data(body)?,
|
||||||
|
DivKind::Procedure => program.paragraphs = parse_procedure(body),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(program)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Localiza los encabezados de división (`X DIVISION`) y devuelve, en
|
||||||
|
/// orden, su clase y el índice del token de la palabra de división.
|
||||||
|
fn find_divisions(tokens: &[Token]) -> Vec<(DivKind, usize)> {
|
||||||
|
let mut divs = Vec::new();
|
||||||
|
let mut i = 0;
|
||||||
|
while i + 1 < tokens.len() {
|
||||||
|
if kw(tokens.get(i + 1)).as_deref() == Some("DIVISION") {
|
||||||
|
let kind = match kw(tokens.get(i)).as_deref() {
|
||||||
|
Some("IDENTIFICATION") | Some("ID") => Some(DivKind::Identification),
|
||||||
|
Some("ENVIRONMENT") => Some(DivKind::Environment),
|
||||||
|
Some("DATA") => Some(DivKind::Data),
|
||||||
|
Some("PROCEDURE") => Some(DivKind::Procedure),
|
||||||
|
_ => None,
|
||||||
|
};
|
||||||
|
if let Some(k) = kind {
|
||||||
|
divs.push((k, i));
|
||||||
|
i += 2;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
i += 1;
|
||||||
|
}
|
||||||
|
divs
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Extrae el `PROGRAM-ID` del cuerpo de la IDENTIFICATION division.
|
||||||
|
fn parse_identification(body: &[Token], program: &mut Program) {
|
||||||
|
for (i, t) in body.iter().enumerate() {
|
||||||
|
if kw(Some(t)).as_deref() == Some("PROGRAM-ID") {
|
||||||
|
// `PROGRAM-ID. NAME.` — el punto tras `PROGRAM-ID` es opcional.
|
||||||
|
let mut j = i + 1;
|
||||||
|
if body.get(j).map(|t| t.kind) == Some(TokenKind::Period) {
|
||||||
|
j += 1;
|
||||||
|
}
|
||||||
|
if let Some(name) = body.get(j) {
|
||||||
|
program.program_id = Some(match name.kind {
|
||||||
|
TokenKind::Word => name.text.to_uppercase(),
|
||||||
|
TokenKind::String => name.text.clone(),
|
||||||
|
_ => continue,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Parsea el cuerpo de la DATA division en un árbol de [`DataItem`].
|
||||||
|
fn parse_data(body: &[Token]) -> Result<Vec<DataItem>, ParseError> {
|
||||||
|
let mut flat = Vec::new();
|
||||||
|
for sent in split_sentences(body) {
|
||||||
|
let Some(first) = sent.first() else { continue };
|
||||||
|
// Sólo las sentencias que arrancan con un número de nivel son
|
||||||
|
// entradas de datos; los encabezados de SECTION y las entradas
|
||||||
|
// FD/SD empiezan con palabra y se ignoran.
|
||||||
|
if first.kind != TokenKind::Number {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let level = parse_level(first)?;
|
||||||
|
flat.push(parse_data_entry(level, &sent)?);
|
||||||
|
}
|
||||||
|
Ok(build_tree(flat))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Valida que el token sea un número de nivel COBOL (01-49, 66, 77, 88).
|
||||||
|
fn parse_level(t: &Token) -> Result<u8, ParseError> {
|
||||||
|
let bad = || ParseError::BadLevel {
|
||||||
|
line: t.line,
|
||||||
|
found: t.text.clone(),
|
||||||
|
};
|
||||||
|
let n: u8 = t.text.parse().map_err(|_| bad())?;
|
||||||
|
if (1..=49).contains(&n) || n == 66 || n == 77 || n == 88 {
|
||||||
|
Ok(n)
|
||||||
|
} else {
|
||||||
|
Err(bad())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Parsea una sola entrada de datos (una sentencia ya sin el punto).
|
||||||
|
fn parse_data_entry(level: u8, sent: &[Token]) -> Result<DataItem, ParseError> {
|
||||||
|
let line = sent[0].line;
|
||||||
|
let name_tok = sent.get(1).ok_or(ParseError::ExpectedDataName { line })?;
|
||||||
|
if name_tok.kind != TokenKind::Word {
|
||||||
|
return Err(ParseError::ExpectedDataName {
|
||||||
|
line: name_tok.line,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
let name = name_tok.text.to_uppercase();
|
||||||
|
|
||||||
|
let mut picture = None;
|
||||||
|
let mut value = None;
|
||||||
|
let mut i = 2;
|
||||||
|
while i < sent.len() {
|
||||||
|
match kw(sent.get(i)).as_deref() {
|
||||||
|
Some("PIC") | Some("PICTURE") => {
|
||||||
|
i += 1;
|
||||||
|
if kw(sent.get(i)).as_deref() == Some("IS") {
|
||||||
|
i += 1;
|
||||||
|
}
|
||||||
|
let (pic, next) = collect_picture(sent, i);
|
||||||
|
if picture.is_none() && !pic.is_empty() {
|
||||||
|
picture = Some(pic);
|
||||||
|
}
|
||||||
|
i = next;
|
||||||
|
}
|
||||||
|
Some("VALUE") => {
|
||||||
|
i += 1;
|
||||||
|
if matches!(kw(sent.get(i)).as_deref(), Some("IS") | Some("ARE")) {
|
||||||
|
i += 1;
|
||||||
|
}
|
||||||
|
let (val, next) = collect_value(sent, i);
|
||||||
|
if value.is_none() {
|
||||||
|
value = val;
|
||||||
|
}
|
||||||
|
i = next;
|
||||||
|
}
|
||||||
|
_ => i += 1,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(DataItem {
|
||||||
|
level,
|
||||||
|
name,
|
||||||
|
picture,
|
||||||
|
value,
|
||||||
|
children: Vec::new(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Reensambla la cláusula PICTURE: concatena el texto de los tokens
|
||||||
|
/// (`S9` `(` `5` `)` `V99` → `S9(5)V99`) hasta toparse con otra
|
||||||
|
/// cláusula de datos. El resultado va en mayúsculas.
|
||||||
|
fn collect_picture(sent: &[Token], start: usize) -> (String, usize) {
|
||||||
|
let mut s = String::new();
|
||||||
|
let mut i = start;
|
||||||
|
while let Some(t) = sent.get(i) {
|
||||||
|
if let Some(w) = kw(Some(t)) {
|
||||||
|
if is_data_clause_kw(&w) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
s.push_str(&t.text);
|
||||||
|
i += 1;
|
||||||
|
}
|
||||||
|
(s.to_uppercase(), i)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Captura el literal de una cláusula VALUE: un signo opcional seguido
|
||||||
|
/// de un número, una constante figurativa o un literal de texto (que se
|
||||||
|
/// devuelve entre comillas simples para distinguirlo de una palabra).
|
||||||
|
fn collect_value(sent: &[Token], start: usize) -> (Option<String>, usize) {
|
||||||
|
let mut i = start;
|
||||||
|
let mut s = String::new();
|
||||||
|
if let Some(t) = sent.get(i) {
|
||||||
|
if t.kind == TokenKind::Symbol && (t.text == "+" || t.text == "-") {
|
||||||
|
s.push_str(&t.text);
|
||||||
|
i += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if let Some(t) = sent.get(i) {
|
||||||
|
match t.kind {
|
||||||
|
TokenKind::Number => {
|
||||||
|
s.push_str(&t.text);
|
||||||
|
i += 1;
|
||||||
|
}
|
||||||
|
TokenKind::String => {
|
||||||
|
s.push('\'');
|
||||||
|
s.push_str(&t.text);
|
||||||
|
s.push('\'');
|
||||||
|
i += 1;
|
||||||
|
}
|
||||||
|
TokenKind::Word => {
|
||||||
|
// Constante figurativa: ZERO, ZEROS, SPACE, SPACES,
|
||||||
|
// HIGH-VALUE, LOW-VALUE, QUOTES, NULL...
|
||||||
|
s.push_str(&t.text.to_uppercase());
|
||||||
|
i += 1;
|
||||||
|
}
|
||||||
|
TokenKind::Period | TokenKind::Symbol => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if s.is_empty() {
|
||||||
|
(None, i)
|
||||||
|
} else {
|
||||||
|
(Some(s), i)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// ¿Es `w` una palabra que abre una cláusula de datos? Marca el final
|
||||||
|
/// de la cláusula PICTURE.
|
||||||
|
fn is_data_clause_kw(w: &str) -> bool {
|
||||||
|
matches!(
|
||||||
|
w,
|
||||||
|
"VALUE"
|
||||||
|
| "OCCURS"
|
||||||
|
| "REDEFINES"
|
||||||
|
| "RENAMES"
|
||||||
|
| "USAGE"
|
||||||
|
| "COMP"
|
||||||
|
| "COMP-1"
|
||||||
|
| "COMP-2"
|
||||||
|
| "COMP-3"
|
||||||
|
| "COMP-4"
|
||||||
|
| "COMP-5"
|
||||||
|
| "COMPUTATIONAL"
|
||||||
|
| "COMPUTATIONAL-1"
|
||||||
|
| "COMPUTATIONAL-2"
|
||||||
|
| "COMPUTATIONAL-3"
|
||||||
|
| "COMPUTATIONAL-4"
|
||||||
|
| "COMPUTATIONAL-5"
|
||||||
|
| "BINARY"
|
||||||
|
| "PACKED-DECIMAL"
|
||||||
|
| "SIGN"
|
||||||
|
| "JUSTIFIED"
|
||||||
|
| "JUST"
|
||||||
|
| "BLANK"
|
||||||
|
| "SYNCHRONIZED"
|
||||||
|
| "SYNC"
|
||||||
|
| "DISPLAY"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Convierte la lista plana de ítems en un árbol según los niveles: un
|
||||||
|
/// ítem cuelga del anterior cuyo nivel sea estrictamente menor. Los
|
||||||
|
/// niveles 01 y 77 son siempre raíces.
|
||||||
|
fn build_tree(flat: Vec<DataItem>) -> Vec<DataItem> {
|
||||||
|
let mut roots: Vec<DataItem> = Vec::new();
|
||||||
|
let mut stack: Vec<DataItem> = Vec::new();
|
||||||
|
for item in flat {
|
||||||
|
if item.level == 1 || item.level == 77 {
|
||||||
|
while let Some(done) = stack.pop() {
|
||||||
|
attach(&mut stack, &mut roots, done);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Cerrar los hermanos y descendientes ya completos.
|
||||||
|
while stack.last().is_some_and(|top| top.level >= item.level) {
|
||||||
|
let done = stack.pop().unwrap();
|
||||||
|
attach(&mut stack, &mut roots, done);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
stack.push(item);
|
||||||
|
}
|
||||||
|
while let Some(done) = stack.pop() {
|
||||||
|
attach(&mut stack, &mut roots, done);
|
||||||
|
}
|
||||||
|
roots
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Engancha un ítem completo: como hijo del que esté en el tope de la
|
||||||
|
/// pila, o como raíz si la pila está vacía.
|
||||||
|
fn attach(stack: &mut [DataItem], roots: &mut Vec<DataItem>, item: DataItem) {
|
||||||
|
if let Some(parent) = stack.last_mut() {
|
||||||
|
parent.children.push(item);
|
||||||
|
} else {
|
||||||
|
roots.push(item);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Parsea el cuerpo de la PROCEDURE division en párrafos.
|
||||||
|
fn parse_procedure(body: &[Token]) -> Vec<Paragraph> {
|
||||||
|
let mut paras: Vec<Paragraph> = Vec::new();
|
||||||
|
let mut current = Paragraph {
|
||||||
|
name: String::new(),
|
||||||
|
sentences: Vec::new(),
|
||||||
|
};
|
||||||
|
let mut current_named = false;
|
||||||
|
for sent in split_sentences(body) {
|
||||||
|
if let Some(name) = paragraph_header_name(&sent) {
|
||||||
|
// Cerrar el párrafo en curso si tiene identidad o contenido.
|
||||||
|
if current_named || !current.sentences.is_empty() {
|
||||||
|
paras.push(std::mem::replace(
|
||||||
|
&mut current,
|
||||||
|
Paragraph {
|
||||||
|
name: String::new(),
|
||||||
|
sentences: Vec::new(),
|
||||||
|
},
|
||||||
|
));
|
||||||
|
}
|
||||||
|
current.name = name;
|
||||||
|
current_named = true;
|
||||||
|
} else {
|
||||||
|
current.sentences.push(Sentence { tokens: sent });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if current_named || !current.sentences.is_empty() {
|
||||||
|
paras.push(current);
|
||||||
|
}
|
||||||
|
paras
|
||||||
|
}
|
||||||
|
|
||||||
|
/// ¿Es esta sentencia un encabezado de párrafo o de sección? Lo es si
|
||||||
|
/// es una sola palabra (un nombre de párrafo) o `NOMBRE SECTION`. Se
|
||||||
|
/// excluyen `EXIT`/`GOBACK`/`CONTINUE`, que de una sola palabra son
|
||||||
|
/// statements, no encabezados.
|
||||||
|
fn paragraph_header_name(sent: &[Token]) -> Option<String> {
|
||||||
|
match sent {
|
||||||
|
[w] if w.kind == TokenKind::Word => {
|
||||||
|
let name = w.text.to_uppercase();
|
||||||
|
if matches!(name.as_str(), "EXIT" | "GOBACK" | "CONTINUE") {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
Some(name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
[w, s] if w.kind == TokenKind::Word && kw(Some(s)).as_deref() == Some("SECTION") => {
|
||||||
|
Some(w.text.to_uppercase())
|
||||||
|
}
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Parte un tramo de tokens en sentencias por el punto terminador. El
|
||||||
|
/// punto se descarta; las sentencias vacías no se emiten.
|
||||||
|
fn split_sentences(body: &[Token]) -> Vec<Vec<Token>> {
|
||||||
|
let mut out = Vec::new();
|
||||||
|
let mut cur = Vec::new();
|
||||||
|
for t in body {
|
||||||
|
if t.kind == TokenKind::Period {
|
||||||
|
if !cur.is_empty() {
|
||||||
|
out.push(std::mem::take(&mut cur));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
cur.push(t.clone());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !cur.is_empty() {
|
||||||
|
out.push(cur);
|
||||||
|
}
|
||||||
|
out
|
||||||
|
}
|
||||||
|
|
||||||
|
/// El índice del primer `Period` en `tokens` a partir de `start`.
|
||||||
|
fn period_after(tokens: &[Token], start: usize) -> Option<usize> {
|
||||||
|
tokens
|
||||||
|
.iter()
|
||||||
|
.enumerate()
|
||||||
|
.skip(start)
|
||||||
|
.find(|(_, t)| t.kind == TokenKind::Period)
|
||||||
|
.map(|(i, _)| i)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Si el token es una palabra, su texto en mayúsculas (COBOL es
|
||||||
|
/// case-insensitive). `None` para cualquier otra clase de token o
|
||||||
|
/// posición ausente.
|
||||||
|
fn kw(t: Option<&Token>) -> Option<String> {
|
||||||
|
match t {
|
||||||
|
Some(t) if t.kind == TokenKind::Word => Some(t.text.to_uppercase()),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use charka_lexer::{lex, SourceFormat};
|
||||||
|
|
||||||
|
/// Helper: lexa el fuente en formato libre y lo parsea.
|
||||||
|
fn parse_src(src: &str) -> Program {
|
||||||
|
let toks = lex(src, SourceFormat::Free).expect("lex OK");
|
||||||
|
parse(&toks).expect("parse OK")
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn empty_input_yields_empty_program() {
|
||||||
|
let p = parse(&[]).unwrap();
|
||||||
|
assert_eq!(p, Program::default());
|
||||||
|
assert!(p.program_id.is_none());
|
||||||
|
assert!(p.data.is_empty());
|
||||||
|
assert!(p.paragraphs.is_empty());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn captures_program_id() {
|
||||||
|
let p = parse_src(
|
||||||
|
"IDENTIFICATION DIVISION.\n\
|
||||||
|
PROGRAM-ID. PAYROLL.\n",
|
||||||
|
);
|
||||||
|
assert_eq!(p.program_id.as_deref(), Some("PAYROLL"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn id_division_short_form_and_case_normalized() {
|
||||||
|
let p = parse_src("ID DIVISION.\nPROGRAM-ID. hello.\n");
|
||||||
|
assert_eq!(p.program_id.as_deref(), Some("HELLO"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn flat_data_items() {
|
||||||
|
let p = parse_src(
|
||||||
|
"DATA DIVISION.\n\
|
||||||
|
WORKING-STORAGE SECTION.\n\
|
||||||
|
77 WS-COUNT PIC 9(3) VALUE 0.\n\
|
||||||
|
77 WS-NAME PIC X(20).\n",
|
||||||
|
);
|
||||||
|
assert_eq!(p.data.len(), 2);
|
||||||
|
assert_eq!(p.data[0].name, "WS-COUNT");
|
||||||
|
assert_eq!(p.data[0].level, 77);
|
||||||
|
assert_eq!(p.data[0].picture.as_deref(), Some("9(3)"));
|
||||||
|
assert_eq!(p.data[0].value.as_deref(), Some("0"));
|
||||||
|
assert_eq!(p.data[1].name, "WS-NAME");
|
||||||
|
assert_eq!(p.data[1].picture.as_deref(), Some("X(20)"));
|
||||||
|
assert!(p.data[1].value.is_none());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn data_item_nesting() {
|
||||||
|
let p = parse_src(
|
||||||
|
"DATA DIVISION.\n\
|
||||||
|
WORKING-STORAGE SECTION.\n\
|
||||||
|
01 CUSTOMER-RECORD.\n\
|
||||||
|
05 CUST-NAME PIC X(20).\n\
|
||||||
|
05 CUST-ADDR.\n\
|
||||||
|
10 STREET PIC X(30).\n\
|
||||||
|
10 CITY PIC X(20).\n",
|
||||||
|
);
|
||||||
|
assert_eq!(p.data.len(), 1);
|
||||||
|
let rec = &p.data[0];
|
||||||
|
assert_eq!(rec.name, "CUSTOMER-RECORD");
|
||||||
|
assert_eq!(rec.children.len(), 2);
|
||||||
|
assert_eq!(rec.children[0].name, "CUST-NAME");
|
||||||
|
let addr = &rec.children[1];
|
||||||
|
assert_eq!(addr.name, "CUST-ADDR");
|
||||||
|
assert_eq!(addr.children.len(), 2);
|
||||||
|
assert_eq!(addr.children[0].name, "STREET");
|
||||||
|
assert_eq!(addr.children[1].name, "CITY");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn picture_clause_reassembled() {
|
||||||
|
let p = parse_src(
|
||||||
|
"DATA DIVISION.\n\
|
||||||
|
WORKING-STORAGE SECTION.\n\
|
||||||
|
01 WS-AMOUNT PIC S9(5)V99.\n",
|
||||||
|
);
|
||||||
|
assert_eq!(p.data[0].picture.as_deref(), Some("S9(5)V99"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn picture_and_name_lowercase_normalized() {
|
||||||
|
let p = parse_src(
|
||||||
|
"DATA DIVISION.\n\
|
||||||
|
01 ws-x pic x(4).\n",
|
||||||
|
);
|
||||||
|
assert_eq!(p.data[0].name, "WS-X");
|
||||||
|
assert_eq!(p.data[0].picture.as_deref(), Some("X(4)"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn value_variants() {
|
||||||
|
let p = parse_src(
|
||||||
|
"DATA DIVISION.\n\
|
||||||
|
WORKING-STORAGE SECTION.\n\
|
||||||
|
01 WS-TEXT PIC X(5) VALUE 'BOB'.\n\
|
||||||
|
01 WS-ZERO PIC 9(3) VALUE ZERO.\n\
|
||||||
|
01 WS-NEG PIC S9(3) VALUE -5.\n",
|
||||||
|
);
|
||||||
|
assert_eq!(p.data[0].value.as_deref(), Some("'BOB'"));
|
||||||
|
assert_eq!(p.data[1].value.as_deref(), Some("ZERO"));
|
||||||
|
assert_eq!(p.data[2].value.as_deref(), Some("-5"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn level_88_condition_names_nest() {
|
||||||
|
let p = parse_src(
|
||||||
|
"DATA DIVISION.\n\
|
||||||
|
WORKING-STORAGE SECTION.\n\
|
||||||
|
01 WS-STATUS PIC X.\n\
|
||||||
|
88 STATUS-OK VALUE 'O'.\n\
|
||||||
|
88 STATUS-BAD VALUE 'B'.\n",
|
||||||
|
);
|
||||||
|
let st = &p.data[0];
|
||||||
|
assert_eq!(st.name, "WS-STATUS");
|
||||||
|
assert_eq!(st.children.len(), 2);
|
||||||
|
assert_eq!(st.children[0].level, 88);
|
||||||
|
assert_eq!(st.children[0].name, "STATUS-OK");
|
||||||
|
assert_eq!(st.children[0].value.as_deref(), Some("'O'"));
|
||||||
|
assert_eq!(st.children[1].name, "STATUS-BAD");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn filler_data_items() {
|
||||||
|
let p = parse_src(
|
||||||
|
"DATA DIVISION.\n\
|
||||||
|
01 HEADER-LINE.\n\
|
||||||
|
05 FILLER PIC X(10) VALUE 'NAME'.\n\
|
||||||
|
05 FILLER PIC X(10) VALUE 'AGE'.\n",
|
||||||
|
);
|
||||||
|
assert_eq!(p.data[0].children.len(), 2);
|
||||||
|
assert_eq!(p.data[0].children[0].name, "FILLER");
|
||||||
|
assert_eq!(p.data[0].children[1].name, "FILLER");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn bad_level_number_is_error() {
|
||||||
|
let toks = lex(
|
||||||
|
"DATA DIVISION.\n\
|
||||||
|
WORKING-STORAGE SECTION.\n\
|
||||||
|
99 WS-X PIC X.\n",
|
||||||
|
SourceFormat::Free,
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
let err = parse(&toks).unwrap_err();
|
||||||
|
assert!(matches!(err, ParseError::BadLevel { found, .. } if found == "99"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn procedure_paragraphs() {
|
||||||
|
let p = parse_src(
|
||||||
|
"PROCEDURE DIVISION.\n\
|
||||||
|
MAIN-PARA.\n\
|
||||||
|
DISPLAY 'HELLO'.\n\
|
||||||
|
PERFORM SUB-PARA.\n\
|
||||||
|
STOP RUN.\n\
|
||||||
|
SUB-PARA.\n\
|
||||||
|
DISPLAY 'SUB'.\n",
|
||||||
|
);
|
||||||
|
assert_eq!(p.paragraphs.len(), 2);
|
||||||
|
assert_eq!(p.paragraphs[0].name, "MAIN-PARA");
|
||||||
|
assert_eq!(p.paragraphs[0].sentences.len(), 3);
|
||||||
|
assert_eq!(p.paragraphs[1].name, "SUB-PARA");
|
||||||
|
assert_eq!(p.paragraphs[1].sentences.len(), 1);
|
||||||
|
// La sentencia conserva sus tokens crudos.
|
||||||
|
assert_eq!(p.paragraphs[0].sentences[0].tokens[0].text, "DISPLAY");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn implicit_paragraph_for_leading_statements() {
|
||||||
|
let p = parse_src(
|
||||||
|
"PROCEDURE DIVISION.\n\
|
||||||
|
DISPLAY 'FIRST'.\n\
|
||||||
|
MAIN-PARA.\n\
|
||||||
|
STOP RUN.\n",
|
||||||
|
);
|
||||||
|
assert_eq!(p.paragraphs.len(), 2);
|
||||||
|
assert_eq!(p.paragraphs[0].name, "");
|
||||||
|
assert_eq!(p.paragraphs[0].sentences.len(), 1);
|
||||||
|
assert_eq!(p.paragraphs[1].name, "MAIN-PARA");
|
||||||
|
assert_eq!(p.paragraphs[1].sentences.len(), 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn procedure_section_header_recognized() {
|
||||||
|
let p = parse_src(
|
||||||
|
"PROCEDURE DIVISION.\n\
|
||||||
|
INIT-SECTION SECTION.\n\
|
||||||
|
DISPLAY 'X'.\n",
|
||||||
|
);
|
||||||
|
assert_eq!(p.paragraphs.len(), 1);
|
||||||
|
assert_eq!(p.paragraphs[0].name, "INIT-SECTION");
|
||||||
|
assert_eq!(p.paragraphs[0].sentences.len(), 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn full_program_end_to_end() {
|
||||||
|
let p = parse_src(
|
||||||
|
"IDENTIFICATION DIVISION.\n\
|
||||||
|
PROGRAM-ID. ADDER.\n\
|
||||||
|
ENVIRONMENT DIVISION.\n\
|
||||||
|
DATA DIVISION.\n\
|
||||||
|
WORKING-STORAGE SECTION.\n\
|
||||||
|
01 WS-A PIC 9(3) VALUE 10.\n\
|
||||||
|
01 WS-B PIC 9(3) VALUE 32.\n\
|
||||||
|
01 WS-TOTAL PIC 9(4).\n\
|
||||||
|
PROCEDURE DIVISION.\n\
|
||||||
|
MAIN-PARA.\n\
|
||||||
|
ADD WS-A TO WS-B GIVING WS-TOTAL.\n\
|
||||||
|
DISPLAY WS-TOTAL.\n\
|
||||||
|
STOP RUN.\n",
|
||||||
|
);
|
||||||
|
assert_eq!(p.program_id.as_deref(), Some("ADDER"));
|
||||||
|
assert_eq!(p.data.len(), 3);
|
||||||
|
assert_eq!(p.data[0].value.as_deref(), Some("10"));
|
||||||
|
assert_eq!(p.data[2].name, "WS-TOTAL");
|
||||||
|
assert!(p.data[2].value.is_none());
|
||||||
|
assert_eq!(p.paragraphs.len(), 1);
|
||||||
|
assert_eq!(p.paragraphs[0].name, "MAIN-PARA");
|
||||||
|
assert_eq!(p.paragraphs[0].sentences.len(), 3);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,6 +3,34 @@
|
|||||||
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-parser): parser COBOL'85 → AST
|
||||||
|
|
||||||
|
Crate nuevo `crates/modules/charka/charka-parser` — la segunda etapa del
|
||||||
|
pipeline: `Vec<Token>` → `Program` (AST). Alcance v1: el esqueleto del
|
||||||
|
programa.
|
||||||
|
|
||||||
|
- `parse(&[Token]) -> Result<Program, ParseError>`. El AST: `Program`
|
||||||
|
(`program_id`, `data`, `paragraphs`), `DataItem`, `Paragraph`,
|
||||||
|
`Sentence`.
|
||||||
|
- Particiona el flujo de tokens en las cuatro divisiones por sus
|
||||||
|
encabezados (`X DIVISION`); de la IDENTIFICATION extrae el
|
||||||
|
`PROGRAM-ID`.
|
||||||
|
- **DATA division** → árbol de `DataItem`: número de nivel, nombre,
|
||||||
|
cláusula `PICTURE` reensamblada (`S9` `(` `5` `)` `V99` →
|
||||||
|
`S9(5)V99`) y `VALUE`. Anida por número de nivel — 01 y 77 son
|
||||||
|
raíces, 88 cuelga del ítem precedente.
|
||||||
|
- **PROCEDURE division** → `Vec<Paragraph>`, cada párrafo con sus
|
||||||
|
`Sentence` (tokens crudos; sin parseo a nivel de statement, eso es
|
||||||
|
`charka-ir`). Las sentencias previas al primer encabezado van a un
|
||||||
|
párrafo implícito de nombre "".
|
||||||
|
- Parser tolerante: salta encabezados de `SECTION`, entradas `FD`/`SD`
|
||||||
|
y cláusulas de datos que no sean `PICTURE`/`VALUE`. `ParseError` sólo
|
||||||
|
ante un número de nivel inválido o un dato sin nombre.
|
||||||
|
- 15 tests: PROGRAM-ID (forma larga y `ID` corta), ítems planos y
|
||||||
|
anidados, reensamblado de PICTURE, variantes de VALUE, niveles 88,
|
||||||
|
`FILLER`, párrafos y párrafo implícito, encabezado de sección,
|
||||||
|
programa completo de punta a punta, nivel inválido.
|
||||||
|
|
||||||
### feat(charka-lexer): tokenizador de COBOL
|
### feat(charka-lexer): tokenizador de COBOL
|
||||||
|
|
||||||
Crate nuevo `crates/modules/charka/charka-lexer` — la primera etapa del
|
Crate nuevo `crates/modules/charka/charka-lexer` — la primera etapa del
|
||||||
|
|||||||
@@ -0,0 +1,2 @@
|
|||||||
|
{"kind":"seed","seq":0,"entity":"Cliente","id":"b300a04f-36e6-4d22-99d9-b036c1b7b0db","data":{"nombre":"\\xc","email":"sad","empresa":"ww","id":"b300a04f-36e6-4d22-99d9-b036c1b7b0db"}}
|
||||||
|
{"kind":"seed","seq":1,"entity":"Cliente","id":"50129365-f780-43a5-9704-e33dbe8da27e","data":{"nombre":"sds","email":"sas","empresa":"asd","id":"50129365-f780-43a5-9704-e33dbe8da27e"}}
|
||||||
Reference in New Issue
Block a user