gioser

tawasuyu/brahman

Rust 570TOML 215Markdown 68JSON 198261 Definitions

⎇ d3cdbb2d2d5bb34442417328aabb9235fcff5c05 crates/modules/charka/charka-parser/src/lib.rs Rust · 694 lines · 22.9 KB History Who touched what raw

feat(charka): charka-parser — COBOL'85 (subconjunto) a AST d3cdbb2d2 · sergio ·

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}