⎇ 3902763daa94397756ff2394116daef97814090a
crates/modules/charka/charka-parser/src/lib.rs
Historia
Quién tocó qué
crudo
feat(charka): OCCURS — tablas y referencias con subíndice
1//! `charka-parser` — parser de COBOL'85 (subconjunto) a AST.
2//!
3//! Segunda etapa del transpilador COBOL→Rust: consume los [`Token`] del
4//! `charka-lexer` y produce un [`Program`]. El alcance de la v1 es el
5//! **esqueleto del programa**:
6//!
7//! - Las cuatro divisiones (`IDENTIFICATION`, `ENVIRONMENT`, `DATA`,
8//! `PROCEDURE`) y el `PROGRAM-ID`.
9//! - La **DATA division** como un árbol de [`DataItem`]: número de
10//! nivel, nombre, cláusula `PICTURE` reensamblada y `VALUE`.
11//! - La **PROCEDURE division** como una lista de [`Paragraph`], cada
12//! uno con sus [`Sentence`] (los tokens crudos de cada sentencia).
13//!
14//! Lo que **no** hace todavía: el parseo a nivel de statement (cada
15//! sentencia queda como `Vec<Token>` — es trabajo de `charka-ir`), la
16//! ENVIRONMENT division, CICS, SQL embebido y los dialectos IBM. Son
17//! las etapas siguientes del plan.
18//!
19//! El parser es tolerante: salta lo que no entiende (encabezados de
20//! `SECTION`, entradas `FD`/`SD`, cláusulas de datos que no sean
21//! `PICTURE`/`VALUE`) en vez de fallar. Sólo emite [`ParseError`] ante
22//! un número de nivel inválido o un dato sin nombre.
23
24#![forbid(unsafe_code)]
25
26use thiserror::Error;
27
28pub use charka_lexer::{Token, TokenKind};
29
30/// Un programa COBOL parseado: el esqueleto de sus cuatro divisiones.
31#[derive(Debug, Clone, PartialEq, Eq, Default)]
32pub struct Program {
33 /// El `PROGRAM-ID` de la IDENTIFICATION division, si está presente.
34 pub program_id: Option<String>,
35 /// Los ítems raíz de la DATA division (cada `01`/`77` con su árbol).
36 pub data: Vec<DataItem>,
37 /// Los párrafos de la PROCEDURE division, en orden de aparición.
38 pub paragraphs: Vec<Paragraph>,
39}
40
41/// Un ítem de datos de la DATA division: un número de nivel, un nombre
42/// y, opcionalmente, las cláusulas `PICTURE` y `VALUE`. Los ítems de
43/// mayor nivel numérico cuelgan como `children` del que los contiene.
44#[derive(Debug, Clone, PartialEq, Eq)]
45pub struct DataItem {
46 /// Número de nivel: 01-49 (jerárquico), 66 (`RENAMES`), 77
47 /// (elemental independiente) u 88 (nombre de condición).
48 pub level: u8,
49 /// Nombre del dato, normalizado a mayúsculas (`FILLER` incluido).
50 pub name: String,
51 /// Cláusula `PICTURE` reensamblada (`S9(5)V99`), si la hay.
52 pub picture: Option<String>,
53 /// Cláusula `VALUE`: literal numérico (con signo), constante
54 /// figurativa en mayúsculas, o literal de texto entre comillas.
55 pub value: Option<String>,
56 /// Cláusula `OCCURS n [TIMES]`: el dato es una tabla de `n`
57 /// elementos. `None` si es un dato escalar.
58 pub occurs: Option<u32>,
59 /// Ítems subordinados (de nivel numérico mayor).
60 pub children: Vec<DataItem>,
61}
62
63/// Un párrafo de la PROCEDURE division. El párrafo implícito que
64/// agrupa las sentencias previas al primer encabezado tiene `name` "".
65#[derive(Debug, Clone, PartialEq, Eq)]
66pub struct Paragraph {
67 /// Nombre del párrafo en mayúsculas; "" para el párrafo implícito.
68 pub name: String,
69 /// Sentencias del párrafo, en orden.
70 pub sentences: Vec<Sentence>,
71}
72
73/// Una sentencia: los tokens entre dos puntos terminadores. La v1 no
74/// parsea a nivel de statement — eso es trabajo de `charka-ir`.
75#[derive(Debug, Clone, PartialEq, Eq)]
76pub struct Sentence {
77 pub tokens: Vec<Token>,
78}
79
80/// Error de parseo, con la línea del token donde se detectó.
81#[derive(Debug, Clone, PartialEq, Eq, Error)]
82pub enum ParseError {
83 #[error("línea {line}: número de nivel inválido {found:?} (esperado 01-49, 66, 77 u 88)")]
84 BadLevel { line: u32, found: String },
85 #[error("línea {line}: se esperaba el nombre de un dato tras el número de nivel")]
86 ExpectedDataName { line: u32 },
87}
88
89/// Las cuatro divisiones de un programa COBOL.
90#[derive(Debug, Clone, Copy, PartialEq, Eq)]
91enum DivKind {
92 Identification,
93 Environment,
94 Data,
95 Procedure,
96}
97
98/// Parsea una secuencia de tokens COBOL en un [`Program`].
99pub fn parse(tokens: &[Token]) -> Result<Program, ParseError> {
100 let divs = find_divisions(tokens);
101 let mut program = Program::default();
102 for (idx, &(kind, start)) in divs.iter().enumerate() {
103 // El cuerpo de la división va desde el punto que cierra su
104 // encabezado hasta el comienzo de la división siguiente.
105 let lo = match period_after(tokens, start) {
106 Some(p) => p + 1,
107 None => tokens.len(),
108 };
109 let next = divs.get(idx + 1).map(|&(_, s)| s).unwrap_or(tokens.len());
110 let body = &tokens[lo..next.max(lo)];
111 match kind {
112 DivKind::Identification => parse_identification(body, &mut program),
113 DivKind::Environment => {} // la v1 ignora la ENVIRONMENT division
114 DivKind::Data => program.data = parse_data(body)?,
115 DivKind::Procedure => program.paragraphs = parse_procedure(body),
116 }
117 }
118 Ok(program)
119}
120
121/// Localiza los encabezados de división (`X DIVISION`) y devuelve, en
122/// orden, su clase y el índice del token de la palabra de división.
123fn find_divisions(tokens: &[Token]) -> Vec<(DivKind, usize)> {
124 let mut divs = Vec::new();
125 let mut i = 0;
126 while i + 1 < tokens.len() {
127 if kw(tokens.get(i + 1)).as_deref() == Some("DIVISION") {
128 let kind = match kw(tokens.get(i)).as_deref() {
129 Some("IDENTIFICATION") | Some("ID") => Some(DivKind::Identification),
130 Some("ENVIRONMENT") => Some(DivKind::Environment),
131 Some("DATA") => Some(DivKind::Data),
132 Some("PROCEDURE") => Some(DivKind::Procedure),
133 _ => None,
134 };
135 if let Some(k) = kind {
136 divs.push((k, i));
137 i += 2;
138 continue;
139 }
140 }
141 i += 1;
142 }
143 divs
144}
145
146/// Extrae el `PROGRAM-ID` del cuerpo de la IDENTIFICATION division.
147fn parse_identification(body: &[Token], program: &mut Program) {
148 for (i, t) in body.iter().enumerate() {
149 if kw(Some(t)).as_deref() == Some("PROGRAM-ID") {
150 // `PROGRAM-ID. NAME.` — el punto tras `PROGRAM-ID` es opcional.
151 let mut j = i + 1;
152 if body.get(j).map(|t| t.kind) == Some(TokenKind::Period) {
153 j += 1;
154 }
155 if let Some(name) = body.get(j) {
156 program.program_id = Some(match name.kind {
157 TokenKind::Word => name.text.to_uppercase(),
158 TokenKind::String => name.text.clone(),
159 _ => continue,
160 });
161 }
162 return;
163 }
164 }
165}
166
167/// Parsea el cuerpo de la DATA division en un árbol de [`DataItem`].
168fn parse_data(body: &[Token]) -> Result<Vec<DataItem>, ParseError> {
169 let mut flat = Vec::new();
170 for sent in split_sentences(body) {
171 let Some(first) = sent.first() else { continue };
172 // Sólo las sentencias que arrancan con un número de nivel son
173 // entradas de datos; los encabezados de SECTION y las entradas
174 // FD/SD empiezan con palabra y se ignoran.
175 if first.kind != TokenKind::Number {
176 continue;
177 }
178 let level = parse_level(first)?;
179 flat.push(parse_data_entry(level, &sent)?);
180 }
181 Ok(build_tree(flat))
182}
183
184/// Valida que el token sea un número de nivel COBOL (01-49, 66, 77, 88).
185fn parse_level(t: &Token) -> Result<u8, ParseError> {
186 let bad = || ParseError::BadLevel {
187 line: t.line,
188 found: t.text.clone(),
189 };
190 let n: u8 = t.text.parse().map_err(|_| bad())?;
191 if (1..=49).contains(&n) || n == 66 || n == 77 || n == 88 {
192 Ok(n)
193 } else {
194 Err(bad())
195 }
196}
197
198/// Parsea una sola entrada de datos (una sentencia ya sin el punto).
199fn parse_data_entry(level: u8, sent: &[Token]) -> Result<DataItem, ParseError> {
200 let line = sent[0].line;
201 let name_tok = sent.get(1).ok_or(ParseError::ExpectedDataName { line })?;
202 if name_tok.kind != TokenKind::Word {
203 return Err(ParseError::ExpectedDataName {
204 line: name_tok.line,
205 });
206 }
207 let name = name_tok.text.to_uppercase();
208
209 let mut picture = None;
210 let mut value = None;
211 let mut occurs = None;
212 let mut i = 2;
213 while i < sent.len() {
214 match kw(sent.get(i)).as_deref() {
215 Some("OCCURS") => {
216 i += 1;
217 if let Some(t) = sent.get(i) {
218 if t.kind == TokenKind::Number {
219 if occurs.is_none() {
220 occurs = t.text.parse::<u32>().ok();
221 }
222 i += 1;
223 }
224 }
225 if kw(sent.get(i)).as_deref() == Some("TIMES") {
226 i += 1;
227 }
228 }
229 Some("PIC") | Some("PICTURE") => {
230 i += 1;
231 if kw(sent.get(i)).as_deref() == Some("IS") {
232 i += 1;
233 }
234 let (pic, next) = collect_picture(sent, i);
235 if picture.is_none() && !pic.is_empty() {
236 picture = Some(pic);
237 }
238 i = next;
239 }
240 Some("VALUE") => {
241 i += 1;
242 if matches!(kw(sent.get(i)).as_deref(), Some("IS") | Some("ARE")) {
243 i += 1;
244 }
245 let (val, next) = collect_value(sent, i);
246 if value.is_none() {
247 value = val;
248 }
249 i = next;
250 }
251 _ => i += 1,
252 }
253 }
254
255 Ok(DataItem {
256 level,
257 name,
258 picture,
259 value,
260 occurs,
261 children: Vec::new(),
262 })
263}
264
265/// Reensambla la cláusula PICTURE: concatena el texto de los tokens
266/// (`S9` `(` `5` `)` `V99` → `S9(5)V99`) hasta toparse con otra
267/// cláusula de datos. El resultado va en mayúsculas.
268fn collect_picture(sent: &[Token], start: usize) -> (String, usize) {
269 let mut s = String::new();
270 let mut i = start;
271 while let Some(t) = sent.get(i) {
272 if let Some(w) = kw(Some(t)) {
273 if is_data_clause_kw(&w) {
274 break;
275 }
276 }
277 s.push_str(&t.text);
278 i += 1;
279 }
280 (s.to_uppercase(), i)
281}
282
283/// Captura el literal de una cláusula VALUE: un signo opcional seguido
284/// de un número, una constante figurativa o un literal de texto (que se
285/// devuelve entre comillas simples para distinguirlo de una palabra).
286fn collect_value(sent: &[Token], start: usize) -> (Option<String>, usize) {
287 let mut i = start;
288 let mut s = String::new();
289 if let Some(t) = sent.get(i) {
290 if t.kind == TokenKind::Symbol && (t.text == "+" || t.text == "-") {
291 s.push_str(&t.text);
292 i += 1;
293 }
294 }
295 if let Some(t) = sent.get(i) {
296 match t.kind {
297 TokenKind::Number => {
298 s.push_str(&t.text);
299 i += 1;
300 }
301 TokenKind::String => {
302 s.push('\'');
303 s.push_str(&t.text);
304 s.push('\'');
305 i += 1;
306 }
307 TokenKind::Word => {
308 // Constante figurativa: ZERO, ZEROS, SPACE, SPACES,
309 // HIGH-VALUE, LOW-VALUE, QUOTES, NULL...
310 s.push_str(&t.text.to_uppercase());
311 i += 1;
312 }
313 TokenKind::Period | TokenKind::Symbol => {}
314 }
315 }
316 if s.is_empty() {
317 (None, i)
318 } else {
319 (Some(s), i)
320 }
321}
322
323/// ¿Es `w` una palabra que abre una cláusula de datos? Marca el final
324/// de la cláusula PICTURE.
325fn is_data_clause_kw(w: &str) -> bool {
326 matches!(
327 w,
328 "VALUE"
329 | "OCCURS"
330 | "REDEFINES"
331 | "RENAMES"
332 | "USAGE"
333 | "COMP"
334 | "COMP-1"
335 | "COMP-2"
336 | "COMP-3"
337 | "COMP-4"
338 | "COMP-5"
339 | "COMPUTATIONAL"
340 | "COMPUTATIONAL-1"
341 | "COMPUTATIONAL-2"
342 | "COMPUTATIONAL-3"
343 | "COMPUTATIONAL-4"
344 | "COMPUTATIONAL-5"
345 | "BINARY"
346 | "PACKED-DECIMAL"
347 | "SIGN"
348 | "JUSTIFIED"
349 | "JUST"
350 | "BLANK"
351 | "SYNCHRONIZED"
352 | "SYNC"
353 | "DISPLAY"
354 )
355}
356
357/// Convierte la lista plana de ítems en un árbol según los niveles: un
358/// ítem cuelga del anterior cuyo nivel sea estrictamente menor. Los
359/// niveles 01 y 77 son siempre raíces.
360fn build_tree(flat: Vec<DataItem>) -> Vec<DataItem> {
361 let mut roots: Vec<DataItem> = Vec::new();
362 let mut stack: Vec<DataItem> = Vec::new();
363 for item in flat {
364 if item.level == 1 || item.level == 77 {
365 while let Some(done) = stack.pop() {
366 attach(&mut stack, &mut roots, done);
367 }
368 } else {
369 // Cerrar los hermanos y descendientes ya completos.
370 while stack.last().is_some_and(|top| top.level >= item.level) {
371 let done = stack.pop().unwrap();
372 attach(&mut stack, &mut roots, done);
373 }
374 }
375 stack.push(item);
376 }
377 while let Some(done) = stack.pop() {
378 attach(&mut stack, &mut roots, done);
379 }
380 roots
381}
382
383/// Engancha un ítem completo: como hijo del que esté en el tope de la
384/// pila, o como raíz si la pila está vacía.
385fn attach(stack: &mut [DataItem], roots: &mut Vec<DataItem>, item: DataItem) {
386 if let Some(parent) = stack.last_mut() {
387 parent.children.push(item);
388 } else {
389 roots.push(item);
390 }
391}
392
393/// Parsea el cuerpo de la PROCEDURE division en párrafos.
394fn parse_procedure(body: &[Token]) -> Vec<Paragraph> {
395 let mut paras: Vec<Paragraph> = Vec::new();
396 let mut current = Paragraph {
397 name: String::new(),
398 sentences: Vec::new(),
399 };
400 let mut current_named = false;
401 for sent in split_sentences(body) {
402 if let Some(name) = paragraph_header_name(&sent) {
403 // Cerrar el párrafo en curso si tiene identidad o contenido.
404 if current_named || !current.sentences.is_empty() {
405 paras.push(std::mem::replace(
406 &mut current,
407 Paragraph {
408 name: String::new(),
409 sentences: Vec::new(),
410 },
411 ));
412 }
413 current.name = name;
414 current_named = true;
415 } else {
416 current.sentences.push(Sentence { tokens: sent });
417 }
418 }
419 if current_named || !current.sentences.is_empty() {
420 paras.push(current);
421 }
422 paras
423}
424
425/// ¿Es esta sentencia un encabezado de párrafo o de sección? Lo es si
426/// es una sola palabra (un nombre de párrafo) o `NOMBRE SECTION`. Se
427/// excluyen `EXIT`/`GOBACK`/`CONTINUE`, que de una sola palabra son
428/// statements, no encabezados.
429fn paragraph_header_name(sent: &[Token]) -> Option<String> {
430 match sent {
431 [w] if w.kind == TokenKind::Word => {
432 let name = w.text.to_uppercase();
433 if matches!(name.as_str(), "EXIT" | "GOBACK" | "CONTINUE") {
434 None
435 } else {
436 Some(name)
437 }
438 }
439 [w, s] if w.kind == TokenKind::Word && kw(Some(s)).as_deref() == Some("SECTION") => {
440 Some(w.text.to_uppercase())
441 }
442 _ => None,
443 }
444}
445
446/// Parte un tramo de tokens en sentencias por el punto terminador. El
447/// punto se descarta; las sentencias vacías no se emiten.
448fn split_sentences(body: &[Token]) -> Vec<Vec<Token>> {
449 let mut out = Vec::new();
450 let mut cur = Vec::new();
451 for t in body {
452 if t.kind == TokenKind::Period {
453 if !cur.is_empty() {
454 out.push(std::mem::take(&mut cur));
455 }
456 } else {
457 cur.push(t.clone());
458 }
459 }
460 if !cur.is_empty() {
461 out.push(cur);
462 }
463 out
464}
465
466/// El índice del primer `Period` en `tokens` a partir de `start`.
467fn period_after(tokens: &[Token], start: usize) -> Option<usize> {
468 tokens
469 .iter()
470 .enumerate()
471 .skip(start)
472 .find(|(_, t)| t.kind == TokenKind::Period)
473 .map(|(i, _)| i)
474}
475
476/// Si el token es una palabra, su texto en mayúsculas (COBOL es
477/// case-insensitive). `None` para cualquier otra clase de token o
478/// posición ausente.
479fn kw(t: Option<&Token>) -> Option<String> {
480 match t {
481 Some(t) if t.kind == TokenKind::Word => Some(t.text.to_uppercase()),
482 _ => None,
483 }
484}
485
486#[cfg(test)]
487mod tests {
488 use super::*;
489 use charka_lexer::{lex, SourceFormat};
490
491 /// Helper: lexa el fuente en formato libre y lo parsea.
492 fn parse_src(src: &str) -> Program {
493 let toks = lex(src, SourceFormat::Free).expect("lex OK");
494 parse(&toks).expect("parse OK")
495 }
496
497 #[test]
498 fn empty_input_yields_empty_program() {
499 let p = parse(&[]).unwrap();
500 assert_eq!(p, Program::default());
501 assert!(p.program_id.is_none());
502 assert!(p.data.is_empty());
503 assert!(p.paragraphs.is_empty());
504 }
505
506 #[test]
507 fn captures_program_id() {
508 let p = parse_src(
509 "IDENTIFICATION DIVISION.\n\
510 PROGRAM-ID. PAYROLL.\n",
511 );
512 assert_eq!(p.program_id.as_deref(), Some("PAYROLL"));
513 }
514
515 #[test]
516 fn id_division_short_form_and_case_normalized() {
517 let p = parse_src("ID DIVISION.\nPROGRAM-ID. hello.\n");
518 assert_eq!(p.program_id.as_deref(), Some("HELLO"));
519 }
520
521 #[test]
522 fn flat_data_items() {
523 let p = parse_src(
524 "DATA DIVISION.\n\
525 WORKING-STORAGE SECTION.\n\
526 77 WS-COUNT PIC 9(3) VALUE 0.\n\
527 77 WS-NAME PIC X(20).\n",
528 );
529 assert_eq!(p.data.len(), 2);
530 assert_eq!(p.data[0].name, "WS-COUNT");
531 assert_eq!(p.data[0].level, 77);
532 assert_eq!(p.data[0].picture.as_deref(), Some("9(3)"));
533 assert_eq!(p.data[0].value.as_deref(), Some("0"));
534 assert_eq!(p.data[1].name, "WS-NAME");
535 assert_eq!(p.data[1].picture.as_deref(), Some("X(20)"));
536 assert!(p.data[1].value.is_none());
537 }
538
539 #[test]
540 fn data_item_nesting() {
541 let p = parse_src(
542 "DATA DIVISION.\n\
543 WORKING-STORAGE SECTION.\n\
544 01 CUSTOMER-RECORD.\n\
545 05 CUST-NAME PIC X(20).\n\
546 05 CUST-ADDR.\n\
547 10 STREET PIC X(30).\n\
548 10 CITY PIC X(20).\n",
549 );
550 assert_eq!(p.data.len(), 1);
551 let rec = &p.data[0];
552 assert_eq!(rec.name, "CUSTOMER-RECORD");
553 assert_eq!(rec.children.len(), 2);
554 assert_eq!(rec.children[0].name, "CUST-NAME");
555 let addr = &rec.children[1];
556 assert_eq!(addr.name, "CUST-ADDR");
557 assert_eq!(addr.children.len(), 2);
558 assert_eq!(addr.children[0].name, "STREET");
559 assert_eq!(addr.children[1].name, "CITY");
560 }
561
562 #[test]
563 fn picture_clause_reassembled() {
564 let p = parse_src(
565 "DATA DIVISION.\n\
566 WORKING-STORAGE SECTION.\n\
567 01 WS-AMOUNT PIC S9(5)V99.\n",
568 );
569 assert_eq!(p.data[0].picture.as_deref(), Some("S9(5)V99"));
570 }
571
572 #[test]
573 fn picture_and_name_lowercase_normalized() {
574 let p = parse_src(
575 "DATA DIVISION.\n\
576 01 ws-x pic x(4).\n",
577 );
578 assert_eq!(p.data[0].name, "WS-X");
579 assert_eq!(p.data[0].picture.as_deref(), Some("X(4)"));
580 }
581
582 #[test]
583 fn value_variants() {
584 let p = parse_src(
585 "DATA DIVISION.\n\
586 WORKING-STORAGE SECTION.\n\
587 01 WS-TEXT PIC X(5) VALUE 'BOB'.\n\
588 01 WS-ZERO PIC 9(3) VALUE ZERO.\n\
589 01 WS-NEG PIC S9(3) VALUE -5.\n",
590 );
591 assert_eq!(p.data[0].value.as_deref(), Some("'BOB'"));
592 assert_eq!(p.data[1].value.as_deref(), Some("ZERO"));
593 assert_eq!(p.data[2].value.as_deref(), Some("-5"));
594 }
595
596 #[test]
597 fn level_88_condition_names_nest() {
598 let p = parse_src(
599 "DATA DIVISION.\n\
600 WORKING-STORAGE SECTION.\n\
601 01 WS-STATUS PIC X.\n\
602 88 STATUS-OK VALUE 'O'.\n\
603 88 STATUS-BAD VALUE 'B'.\n",
604 );
605 let st = &p.data[0];
606 assert_eq!(st.name, "WS-STATUS");
607 assert_eq!(st.children.len(), 2);
608 assert_eq!(st.children[0].level, 88);
609 assert_eq!(st.children[0].name, "STATUS-OK");
610 assert_eq!(st.children[0].value.as_deref(), Some("'O'"));
611 assert_eq!(st.children[1].name, "STATUS-BAD");
612 }
613
614 #[test]
615 fn filler_data_items() {
616 let p = parse_src(
617 "DATA DIVISION.\n\
618 01 HEADER-LINE.\n\
619 05 FILLER PIC X(10) VALUE 'NAME'.\n\
620 05 FILLER PIC X(10) VALUE 'AGE'.\n",
621 );
622 assert_eq!(p.data[0].children.len(), 2);
623 assert_eq!(p.data[0].children[0].name, "FILLER");
624 assert_eq!(p.data[0].children[1].name, "FILLER");
625 }
626
627 #[test]
628 fn occurs_clause_captured() {
629 let p = parse_src(
630 "DATA DIVISION.\n\
631 WORKING-STORAGE SECTION.\n\
632 01 WS-TABLA.\n\
633 05 WS-ELEM PIC 9(3) OCCURS 10 TIMES.\n",
634 );
635 let elem = &p.data[0].children[0];
636 assert_eq!(elem.name, "WS-ELEM");
637 assert_eq!(elem.occurs, Some(10));
638 assert_eq!(elem.picture.as_deref(), Some("9(3)"));
639 }
640
641 #[test]
642 fn bad_level_number_is_error() {
643 let toks = lex(
644 "DATA DIVISION.\n\
645 WORKING-STORAGE SECTION.\n\
646 99 WS-X PIC X.\n",
647 SourceFormat::Free,
648 )
649 .unwrap();
650 let err = parse(&toks).unwrap_err();
651 assert!(matches!(err, ParseError::BadLevel { found, .. } if found == "99"));
652 }
653
654 #[test]
655 fn procedure_paragraphs() {
656 let p = parse_src(
657 "PROCEDURE DIVISION.\n\
658 MAIN-PARA.\n\
659 DISPLAY 'HELLO'.\n\
660 PERFORM SUB-PARA.\n\
661 STOP RUN.\n\
662 SUB-PARA.\n\
663 DISPLAY 'SUB'.\n",
664 );
665 assert_eq!(p.paragraphs.len(), 2);
666 assert_eq!(p.paragraphs[0].name, "MAIN-PARA");
667 assert_eq!(p.paragraphs[0].sentences.len(), 3);
668 assert_eq!(p.paragraphs[1].name, "SUB-PARA");
669 assert_eq!(p.paragraphs[1].sentences.len(), 1);
670 // La sentencia conserva sus tokens crudos.
671 assert_eq!(p.paragraphs[0].sentences[0].tokens[0].text, "DISPLAY");
672 }
673
674 #[test]
675 fn implicit_paragraph_for_leading_statements() {
676 let p = parse_src(
677 "PROCEDURE DIVISION.\n\
678 DISPLAY 'FIRST'.\n\
679 MAIN-PARA.\n\
680 STOP RUN.\n",
681 );
682 assert_eq!(p.paragraphs.len(), 2);
683 assert_eq!(p.paragraphs[0].name, "");
684 assert_eq!(p.paragraphs[0].sentences.len(), 1);
685 assert_eq!(p.paragraphs[1].name, "MAIN-PARA");
686 assert_eq!(p.paragraphs[1].sentences.len(), 1);
687 }
688
689 #[test]
690 fn procedure_section_header_recognized() {
691 let p = parse_src(
692 "PROCEDURE DIVISION.\n\
693 INIT-SECTION SECTION.\n\
694 DISPLAY 'X'.\n",
695 );
696 assert_eq!(p.paragraphs.len(), 1);
697 assert_eq!(p.paragraphs[0].name, "INIT-SECTION");
698 assert_eq!(p.paragraphs[0].sentences.len(), 1);
699 }
700
701 #[test]
702 fn full_program_end_to_end() {
703 let p = parse_src(
704 "IDENTIFICATION DIVISION.\n\
705 PROGRAM-ID. ADDER.\n\
706 ENVIRONMENT DIVISION.\n\
707 DATA DIVISION.\n\
708 WORKING-STORAGE SECTION.\n\
709 01 WS-A PIC 9(3) VALUE 10.\n\
710 01 WS-B PIC 9(3) VALUE 32.\n\
711 01 WS-TOTAL PIC 9(4).\n\
712 PROCEDURE DIVISION.\n\
713 MAIN-PARA.\n\
714 ADD WS-A TO WS-B GIVING WS-TOTAL.\n\
715 DISPLAY WS-TOTAL.\n\
716 STOP RUN.\n",
717 );
718 assert_eq!(p.program_id.as_deref(), Some("ADDER"));
719 assert_eq!(p.data.len(), 3);
720 assert_eq!(p.data[0].value.as_deref(), Some("10"));
721 assert_eq!(p.data[2].name, "WS-TOTAL");
722 assert!(p.data[2].value.is_none());
723 assert_eq!(p.paragraphs.len(), 1);
724 assert_eq!(p.paragraphs[0].name, "MAIN-PARA");
725 assert_eq!(p.paragraphs[0].sentences.len(), 3);
726 }
727}