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