Skip to main content

ironwork_syntax/
extended.rs

1//! What `--compliance extended` does to the tokens before the parser reads them: each constant
2//! entry comes out and every later use of its name stands for its value, literals joined by `&`
3//! become one literal, BINARY-SHORT, BINARY-LONG and BINARY-DOUBLE become COMP-5 PICTUREs,
4//! FLOAT-SHORT and FLOAT-LONG become COMP-1 and COMP-2, PROGRAM-POINTER becomes PROCEDURE-POINTER,
5//! and RETURNING OMITTED leaves a program's
6//! PROCEDURE DIVISION header (docs/compliance.md).
7
8use crate::lexer::{Tok, Token};
9use crate::messages::{IWX0002, IWX0004, IWX0005, IWX0006, IWX0009, IWX0027, IWX0044};
10use crate::{Error, Pos};
11use std::collections::HashMap;
12
13pub const CONSTANT: &str = "constant entry (Micro Focus and GnuCOBOL; Enterprise COBOL has no level 78 and no CONSTANT clause)";
14pub const CONCATENATION: &str = "literal concatenation with & (Micro Focus and GnuCOBOL; Enterprise COBOL has none)";
15pub const BINARY_USAGE: &str = "the COBOL 2002 binary usage (Micro Focus and GnuCOBOL; not Enterprise COBOL's)";
16pub const GNUCOBOL_BINARY_USAGE: &str = "GnuCOBOL's binary usage (not Enterprise COBOL's)";
17pub const NO_IDENTIFICATION_HEADER: &str = "PROGRAM-ID with no IDENTIFICATION DIVISION header before it (COBOL 2002, Micro Focus and GnuCOBOL; Enterprise COBOL requires the header)";
18pub const ASSIGN_ITEM: &str = "ASSIGN to a data item (Micro Focus and GnuCOBOL; Enterprise COBOL's assignment-name is never a data item)";
19pub const RETURNING_OMITTED: &str = "PROCEDURE DIVISION RETURNING OMITTED (GnuCOBOL; Enterprise COBOL's RETURNING names an 01 or 77 item of the LINKAGE SECTION)";
20
21/// A binary usage word read as a COMP-5 PICTURE of two, four or eight bytes.
22struct BinaryUsage {
23    word: &'static str,
24    digits: &'static str,
25    /// Whether the word fixes its sign; the others take SIGNED (the default) or UNSIGNED after them.
26    signed: Option<bool>,
27    origin: &'static str,
28}
29
30/// The binary usages and the COMP-5 PICTURE each is, as GnuCOBOL 3.2 sizes them.
31const BINARY_USAGES: &[BinaryUsage] = &[
32    BinaryUsage { word: "BINARY-SHORT", digits: "9(4)", signed: None, origin: BINARY_USAGE },
33    BinaryUsage { word: "BINARY-LONG", digits: "9(9)", signed: None, origin: BINARY_USAGE },
34    BinaryUsage { word: "BINARY-DOUBLE", digits: "9(18)", signed: None, origin: BINARY_USAGE },
35    BinaryUsage { word: "BINARY-LONG-LONG", digits: "9(18)", signed: None, origin: GNUCOBOL_BINARY_USAGE },
36    BinaryUsage { word: "SIGNED-SHORT", digits: "9(4)", signed: Some(true), origin: GNUCOBOL_BINARY_USAGE },
37    BinaryUsage { word: "UNSIGNED-SHORT", digits: "9(4)", signed: Some(false), origin: GNUCOBOL_BINARY_USAGE },
38    BinaryUsage { word: "SIGNED-INT", digits: "9(9)", signed: Some(true), origin: GNUCOBOL_BINARY_USAGE },
39    BinaryUsage { word: "UNSIGNED-INT", digits: "9(9)", signed: Some(false), origin: GNUCOBOL_BINARY_USAGE },
40    BinaryUsage { word: "SIGNED-LONG", digits: "9(18)", signed: Some(true), origin: GNUCOBOL_BINARY_USAGE },
41    BinaryUsage { word: "UNSIGNED-LONG", digits: "9(18)", signed: Some(false), origin: GNUCOBOL_BINARY_USAGE },
42];
43
44const FIGURATIVES: &[&str] = &["ZERO", "ZEROS", "ZEROES", "SPACE", "SPACES", "HIGH-VALUE", "HIGH-VALUES", "LOW-VALUE", "LOW-VALUES", "QUOTE", "QUOTES", "NULL", "NULLS"];
45
46/// The tokens with constant entries taken out, their names replaced by their values, `&`
47/// concatenations joined, the binary usages rewritten and RETURNING OMITTED taken out of a
48/// program's PROCEDURE DIVISION header. `cards` are the CBL and PROCESS options, whose code page
49/// reads a hexadecimal literal joined to an alphanumeric one.
50pub fn rewrite(tokens: Vec<Token>, cards: &[String]) -> Result<Vec<Token>, Error> {
51    let mut options = numeric::Options::default();
52    for card in cards {
53        options.apply(card).ok();
54    }
55    let mut r = Rewrite { tokens, at: 0, out: Vec::new(), constants: HashMap::new(), pending: Vec::new(), options };
56    // `program`: the last ID paragraph was a PROGRAM-ID, not a function's, class's or method's.
57    let (mut data, mut program, mut header) = (false, false, false);
58    while r.at < r.tokens.len() {
59        let division = r.tokens.get(r.at + 1).is_some_and(|t| matches!(&t.tok, Tok::Word(w) if w == "DIVISION"));
60        if let Tok::Word(w) = &r.tokens[r.at].tok
61            && matches!(w.as_str(), "PROGRAM-ID" | "FUNCTION-ID" | "METHOD-ID" | "CLASS-ID" | "INTERFACE-ID")
62        {
63            program = w == "PROGRAM-ID";
64        }
65        match &r.tokens[r.at].tok {
66            Tok::Word(w) if division => {
67                data = w == "DATA";
68                header = program && w == "PROCEDURE";
69            }
70            Tok::Period => header = false,
71            Tok::Word(w) if header && w == "RETURNING" && r.word_at(1) == Some("OMITTED") && r.tokens.get(r.at + 2).is_some_and(|t| t.tok == Tok::Period) => {
72                r.returning_omitted();
73                continue;
74            }
75            Tok::Number(n) if data && r.out.last().is_none_or(|t| t.tok == Tok::Period) && matches!(r.tokens.get(r.at + 1).map(|t| &t.tok), Some(Tok::Word(_))) && (n == "78" || r.word_at(2) == Some("CONSTANT") && matches!(n.as_str(), "01" | "1")) => {
76                r.constant()?;
77                continue;
78            }
79            Tok::Word(w) if data && BINARY_USAGES.iter().any(|u| u.word == w) => {
80                r.binary_usage()?;
81                continue;
82            }
83            Tok::Word(w) if data && (w == "FLOAT-SHORT" || w == "FLOAT-LONG") => {
84                r.float_usage();
85                continue;
86            }
87            Tok::Word(w) if data && w == "PROGRAM-POINTER" => {
88                r.program_pointer();
89                continue;
90            }
91            Tok::Word(w) if w == "PROGRAM-ID" && !ends_with_header(&r.out) => r.identification_header(),
92            _ => {}
93        }
94        let value = r.value()?;
95        r.push(value);
96    }
97    if let Some(last) = r.out.last_mut() {
98        last.messages.append(&mut r.pending);
99    }
100    Ok(r.out)
101}
102
103struct Rewrite {
104    tokens: Vec<Token>,
105    at: usize,
106    out: Vec<Token>,
107    constants: HashMap<String, Tok>,
108    /// Messages of tokens taken out, for the next token kept.
109    pending: Vec<Error>,
110    /// The cards' options, whose code page reads a hexadecimal literal joined to an alphanumeric one.
111    options: numeric::Options,
112}
113
114impl Rewrite {
115    fn push(&mut self, mut token: Token) {
116        token.messages.splice(0..0, self.pending.drain(..));
117        self.out.push(token);
118    }
119
120    /// Token `k`, a constant's name replaced by its value and a PICTURE's `(name)` by the number.
121    fn substituted(&self, k: usize) -> Token {
122        let mut token = self.tokens[k].clone();
123        match &token.tok {
124            Tok::Word(w) => {
125                if let Some(value) = self.constants.get(w) {
126                    token.tok = value.clone();
127                    token.spelled = None;
128                }
129            }
130            Tok::Pic(p) => {
131                if let Some(p) = picture(p, &self.constants) {
132                    token.tok = Tok::Pic(p);
133                }
134            }
135            _ => {}
136        }
137        token
138    }
139
140    /// The token at `at`, substituted, and the literals any `&` joins to it.
141    fn value(&mut self) -> Result<Token, Error> {
142        let mut left = self.substituted(self.at);
143        self.at += 1;
144        while self.tokens.get(self.at).is_some_and(|t| t.tok == Tok::Ampersand) {
145            let amp = self.tokens[self.at].clone();
146            if self.at + 1 >= self.tokens.len() {
147                return Err(crate::messages::IWS0013.at(amp.pos, "& with no literal after it"));
148            }
149            let right = self.substituted(self.at + 1);
150            self.at += 2;
151            left = join(left, amp, right, |bytes| self.options.code_page().decode(bytes))?;
152        }
153        Ok(left)
154    }
155
156    fn word_at(&self, ahead: usize) -> Option<&str> {
157        match self.tokens.get(self.at + ahead).map(|t| &t.tok) {
158            Some(Tok::Word(w)) => Some(w),
159            _ => None,
160        }
161    }
162
163    /// A constant entry from its level number, `78 name [IS] [GLOBAL] VALUE [IS] value.` or
164    /// `01 name CONSTANT [IS] [GLOBAL] [AS] value.`, where the value is a literal, a figurative
165    /// constant, a constant already defined, or literals joined by `&`. The entry is taken out of
166    /// the tokens.
167    fn constant(&mut self) -> Result<(), Error> {
168        let level = self.tokens[self.at].clone();
169        let named = self.tokens[self.at + 1].clone();
170        let Tok::Word(name) = named.tok else { unreachable!("the caller saw a word") };
171        let seventy_eight = level.tok == Tok::Number("78".into());
172        self.at += if seventy_eight { 2 } else { 3 };
173        self.pending.extend(level.messages);
174        self.pending.extend(named.messages);
175        let refused = |at: Pos, why: &str| crate::messages::IWS0014.at(at, format!("constant {name}: {why}"));
176        match (self.word_at(0), self.word_at(1)) {
177            (Some("IS"), Some("GLOBAL")) => self.at += 2,
178            (Some("GLOBAL"), _) => self.at += 1,
179            _ => {}
180        }
181        let here = self.tokens.get(self.at).map_or(named.pos, |t| t.pos);
182        if seventy_eight {
183            if self.word_at(0) != Some("VALUE") {
184                return Err(refused(here, "a level-78 entry is VALUE and its value, then a period"));
185            }
186            self.at += 1;
187            if self.word_at(0) == Some("IS") {
188                self.at += 1;
189            }
190        } else if self.word_at(0) == Some("AS") {
191            self.at += 1;
192        }
193        if self.at >= self.tokens.len() {
194            return Err(refused(here, "VALUE with no value"));
195        }
196        let value = self.value()?;
197        let literal = match &value.tok {
198            Tok::Alnum(_) | Tok::Hex(_) | Tok::National(_) | Tok::Number(_) => true,
199            Tok::Word(w) => FIGURATIVES.contains(&w.as_str()),
200            _ => false,
201        };
202        let ended = self.tokens.get(self.at).is_some_and(|t| t.tok == Tok::Period);
203        if !literal || !ended {
204            let at = if literal { self.tokens.get(self.at).map_or(value.pos, |t| t.pos) } else { value.pos };
205            return Err(refused(at, "the value is a literal, a figurative constant, a constant defined before, or literals joined by &; ironwork computes no expression there"));
206        }
207        self.pending.extend(self.tokens[self.at].messages.iter().cloned());
208        self.at += 1;
209        self.pending.push(IWX0002.at(level.pos, format!("{CONSTANT}: {name} stands for its value wherever it is used after this entry")));
210        self.pending.extend(value.messages);
211        self.constants.insert(name, value.tok);
212        Ok(())
213    }
214
215    /// `IDENTIFICATION DIVISION.` before a PROGRAM-ID that has none, which it means.
216    fn identification_header(&mut self) {
217        let at = self.tokens[self.at].clone();
218        let made = |tok: Tok, messages: Vec<Error>| Token { tok, pos: at.pos, area_a: at.area_a, spelled: None, after_comma: false, messages };
219        let warning = IWX0006.at(at.pos, format!("{NO_IDENTIFICATION_HEADER}: the program reads as though IDENTIFICATION DIVISION. came before it"));
220        self.push(made(Tok::Word("IDENTIFICATION".into()), vec![warning]));
221        self.push(made(Tok::Word("DIVISION".into()), Vec::new()));
222        self.push(made(Tok::Period, Vec::new()));
223    }
224
225    /// `RETURNING OMITTED` ending a program's PROCEDURE DIVISION header, taken out: GnuCOBOL's
226    /// program that returns no item is one with no RETURNING phrase.
227    fn returning_omitted(&mut self) {
228        let returning = self.tokens[self.at].clone();
229        self.pending.push(IWX0009.at(returning.pos, format!("{RETURNING_OMITTED}: the program is read with no RETURNING phrase, and returns its RETURN-CODE to its caller as any program does")));
230        self.pending.extend(returning.messages);
231        self.pending.extend(self.tokens[self.at + 1].messages.iter().cloned());
232        self.at += 2;
233    }
234
235    /// `[USAGE [IS]] BINARY-SHORT [SIGNED|UNSIGNED]` and the other binary usages in a data entry, as
236    /// `PIC S9(n) COMP-5`, or `PIC 9(n) COMP-5` when unsigned: SIGNED is the default where the word
237    /// does not fix the sign.
238    /// FLOAT-SHORT or FLOAT-LONG read as COMP-1 or COMP-2, IBM's hexadecimal floating point.
239    fn float_usage(&mut self) {
240        let mut token = self.tokens[self.at].clone();
241        let Tok::Word(usage) = &token.tok else { unreachable!("the caller saw a word") };
242        let ibm = if usage == "FLOAT-SHORT" { "COMP-1" } else { "COMP-2" };
243        let shown = format!("{usage} (GnuCOBOL and Micro Focus; Enterprise COBOL writes COMP-1 and COMP-2): it is read as {ibm}, IBM's hexadecimal floating point");
244        token.messages.insert(0, IWX0027.at(token.pos, shown));
245        token.tok = Tok::Word(ibm.into());
246        token.spelled = None;
247        self.at += 1;
248        self.push(token);
249    }
250
251    /// PROGRAM-POINTER read as PROCEDURE-POINTER, which IBM sets and CALLs the same way.
252    fn program_pointer(&mut self) {
253        let mut token = self.tokens[self.at].clone();
254        token.messages.insert(0, IWX0044.at(token.pos, "PROGRAM-POINTER (GnuCOBOL and Micro Focus; Enterprise COBOL writes PROCEDURE-POINTER): it is read as PROCEDURE-POINTER, set by SET ... TO ENTRY and called by CALL"));
255        token.tok = Tok::Word("PROCEDURE-POINTER".into());
256        token.spelled = None;
257        self.at += 1;
258        self.push(token);
259    }
260
261    fn binary_usage(&mut self) -> Result<(), Error> {
262        let token = self.tokens[self.at].clone();
263        let Tok::Word(usage) = &token.tok else { unreachable!("the caller saw a word") };
264        let Some(found) = BINARY_USAGES.iter().find(|u| u.word == usage) else { unreachable!("the caller saw a binary usage") };
265        let mut messages = Vec::new();
266        if self.out.last().is_some_and(|t| t.tok == Tok::Word("IS".into())) && self.out.len() >= 2 && self.out[self.out.len() - 2].tok == Tok::Word("USAGE".into()) {
267            messages.extend(self.out.pop().map(|t| t.messages).unwrap_or_default());
268        }
269        if self.out.last().is_some_and(|t| t.tok == Tok::Word("USAGE".into())) {
270            messages.extend(self.out.pop().map(|t| t.messages).unwrap_or_default());
271        }
272        self.at += 1;
273        let signed = match (found.signed, self.tokens.get(self.at).map(|t| &t.tok)) {
274            (Some(fixed), _) => fixed,
275            (None, Some(Tok::Word(w))) if w == "SIGNED" || w == "UNSIGNED" => {
276                self.at += 1;
277                w == "SIGNED"
278            }
279            (None, _) => true,
280        };
281        let picture = format!("{}{}", if signed { "S" } else { "" }, found.digits);
282        let suffix = if found.signed.is_none() && !signed { " UNSIGNED" } else { "" };
283        let shown = format!("{}: {usage}{suffix} is read as PIC {picture} COMP-5", found.origin);
284        messages.push(IWX0005.at(token.pos, shown));
285        messages.extend(token.messages.iter().cloned());
286        let made = |tok: Tok, messages: Vec<Error>| Token { tok, pos: token.pos, area_a: false, spelled: None, after_comma: false, messages };
287        self.push(made(Tok::Word("PIC".into()), messages));
288        self.push(made(Tok::Pic(picture), Vec::new()));
289        self.push(made(Tok::Word("COMP-5".into()), Vec::new()));
290        Ok(())
291    }
292}
293
294/// Whether the tokens end with `IDENTIFICATION DIVISION.` or `ID DIVISION.`.
295fn ends_with_header(tokens: &[Token]) -> bool {
296    matches!(tokens, [.., a, b, c] if matches!(&a.tok, Tok::Word(w) if w == "IDENTIFICATION" || w == "ID") && b.tok == Tok::Word("DIVISION".into()) && c.tok == Tok::Period)
297}
298
299/// `(name)` in a PICTURE, where `name` is a constant whose value is an unsigned integer, with the
300/// integer in its place.
301fn picture(text: &str, constants: &HashMap<String, Tok>) -> Option<String> {
302    let (mut out, mut rest, mut changed) = (String::new(), text, false);
303    while let Some(open) = rest.find('(') {
304        let Some(close) = rest[open..].find(')').map(|c| open + c) else { break };
305        match constants.get(&rest[open + 1..close]) {
306            Some(Tok::Number(n)) if n.bytes().all(|b| b.is_ascii_digit()) => {
307                out.push_str(&rest[..=open]);
308                out.push_str(n);
309                out.push(')');
310                changed = true;
311            }
312            _ => out.push_str(&rest[..=close]),
313        }
314        rest = &rest[close + 1..];
315    }
316    out.push_str(rest);
317    changed.then_some(out)
318}
319
320/// `left & right` as one literal, at `left`'s place. Alphanumeric and hexadecimal literals join
321/// into an alphanumeric one, a hexadecimal one's bytes read in the program's code page, two
322/// hexadecimal literals into a hexadecimal one, and national literals into a national one.
323fn join(left: Token, amp: Token, right: Token, decode: impl Fn(&[u8]) -> String) -> Result<Token, Error> {
324    let tok = match (&left.tok, &right.tok) {
325        (Tok::Alnum(a), Tok::Alnum(b)) => Tok::Alnum(format!("{a}{b}")),
326        (Tok::Hex(a), Tok::Hex(b)) => Tok::Hex([a.as_slice(), b].concat()),
327        (Tok::Alnum(a), Tok::Hex(b)) => Tok::Alnum(format!("{a}{}", decode(b))),
328        (Tok::Hex(a), Tok::Alnum(b)) => Tok::Alnum(format!("{}{b}", decode(a))),
329        (Tok::National(a), Tok::National(b)) => Tok::National(format!("{a}{b}")),
330        _ => return Err(crate::messages::IWS0016.at(amp.pos, "& joins two alphanumeric or hexadecimal literals, or two national literals, either of which may be a level-78 constant standing for one")),
331    };
332    let mut messages = left.messages;
333    messages.push(IWX0004.at(amp.pos, format!("{CONCATENATION}: the literals on either side are one literal")));
334    messages.extend(amp.messages);
335    messages.extend(right.messages);
336    Ok(Token { tok, pos: left.pos, area_a: left.area_a, spelled: None, after_comma: left.after_comma, messages })
337}
338
339#[cfg(test)]
340mod tests {
341    use crate::ast::{Literal, Usage};
342    use crate::copy::Libraries;
343    use numeric::Compliance;
344
345    fn source(data: &str, procedure: &str) -> String {
346        format!("       IDENTIFICATION DIVISION.\n       PROGRAM-ID. T.\n       DATA DIVISION.\n       WORKING-STORAGE SECTION.\n{data}       PROCEDURE DIVISION.\n{procedure}           GOBACK.\n")
347    }
348
349    fn extended(text: &str) -> Result<crate::ast::Program, crate::Error> {
350        crate::parse_with(text, &Libraries::default().with_compliance(Compliance::Extended))
351    }
352
353    #[test]
354    fn a_constant_stands_for_its_value_in_pictures_values_occurs_and_statements() {
355        let data = concat!(
356            "       78  MAX-LEN VALUE 3.\n",
357            "       78  GREETING IS GLOBAL VALUE IS 'AB' & X'C1'.\n",
358            "       01  QUOTED CONSTANT AS 'Q'.\n",
359            "       01  NOTHING CONSTANT GLOBAL SPACE.\n",
360            "       01  X PIC X(MAX-LEN) VALUE GREETING.\n",
361            "       01  G.\n",
362            "           05 T OCCURS MAX-LEN TIMES PIC X VALUE NOTHING.\n",
363        );
364        let p = extended(&source(data, "           MOVE QUOTED TO X.\n")).unwrap();
365        let names: Vec<&str> = p.working_storage.iter().filter_map(|e| e.name.as_deref()).collect();
366        assert_eq!(names, ["X", "G", "T"]);
367        let x = &p.working_storage[0];
368        assert_eq!((x.picture.as_deref(), x.value.clone()), (Some("X(3)"), Some(Literal::Alnum("ABA".into()))));
369        assert_eq!(p.working_storage[2].occurs, Some(3));
370        let warnings: Vec<(u32, String)> = p.messages.iter().map(|m| (m.pos.line, m.message.clone())).collect();
371        assert_eq!(warnings.len(), 5, "{warnings:?}");
372        assert!(warnings[..2].iter().all(|(_, m)| m.starts_with(super::CONSTANT)) && warnings[0].0 == 5 && warnings[0].1.ends_with("MAX-LEN stands for its value wherever it is used after this entry"));
373        assert!(warnings[2].1.starts_with(super::CONCATENATION) && warnings[2].0 == 6);
374        assert!(warnings[3..].iter().all(|(_, m)| m.starts_with(super::CONSTANT)));
375        assert!(format!("{:?}", p.paragraphs[0].statements[0]).contains("Alnum(\"Q\")"));
376    }
377
378    #[test]
379    fn a_constant_whose_value_is_an_expression_or_missing_is_refused_by_name() {
380        let refused = |data: &str| extended(&source(data, "")).unwrap_err().message;
381        assert_eq!(refused("       78  N VALUE 1 + 2.\n"), "constant N: the value is a literal, a figurative constant, a constant defined before, or literals joined by &; ironwork computes no expression there");
382        assert_eq!(refused("       78  N PIC 9 VALUE 1.\n"), "constant N: a level-78 entry is VALUE and its value, then a period");
383        assert!(refused("       01  N PIC X VALUE 'A' & B.\n").starts_with("& joins two alphanumeric or hexadecimal literals"));
384        assert!(refused("       01  N PIC X VALUE 'A' & N'B'.\n").starts_with("& joins two alphanumeric or hexadecimal literals"));
385    }
386
387    #[test]
388    fn the_binary_usages_are_comp_5_pictures_and_binary_char_is_left_to_the_parser() {
389        let data = "       01  A USAGE IS BINARY-LONG.\n       01  B BINARY-SHORT UNSIGNED VALUE 7.\n       01  C BINARY-DOUBLE SIGNED.\n";
390        let p = extended(&source(data, "")).unwrap();
391        let read: Vec<(Option<&str>, Option<Usage>)> = p.working_storage.iter().map(|e| (e.picture.as_deref(), e.usage)).collect();
392        assert_eq!(read, [(Some("S9(9)"), Some(Usage::NativeBinary)), (Some("9(4)"), Some(Usage::NativeBinary)), (Some("S9(18)"), Some(Usage::NativeBinary))]);
393        assert_eq!(p.working_storage[1].value, Some(Literal::Number("7".into())));
394        let shown: Vec<String> = p.messages.iter().map(|m| m.message.clone()).collect();
395        assert_eq!(shown[1], format!("{}: BINARY-SHORT UNSIGNED is read as PIC 9(4) COMP-5", super::BINARY_USAGE));
396        let data = "       01  F UNSIGNED-INT.\n       01  G SIGNED-SHORT.\n       01  H BINARY-LONG-LONG UNSIGNED.\n       01  I UNSIGNED-LONG.\n";
397        let p = extended(&source(data, "")).unwrap();
398        let read: Vec<(Option<&str>, Option<Usage>)> = p.working_storage.iter().map(|e| (e.picture.as_deref(), e.usage)).collect();
399        assert_eq!(read, [(Some("9(9)"), Some(Usage::NativeBinary)), (Some("S9(4)"), Some(Usage::NativeBinary)), (Some("9(18)"), Some(Usage::NativeBinary)), (Some("9(18)"), Some(Usage::NativeBinary))]);
400        assert_eq!(p.messages[0].message, format!("{}: UNSIGNED-INT is read as PIC 9(9) COMP-5", super::GNUCOBOL_BINARY_USAGE));
401        let p = extended(&source("       01  D BINARY-CHAR UNSIGNED.\n       01  E USAGE BINARY-CHAR SIGNED.\n", "")).unwrap();
402        let read: Vec<(Option<&str>, Option<Usage>)> = p.working_storage.iter().map(|e| (e.picture.as_deref(), e.usage)).collect();
403        assert_eq!(read, [(None, Some(Usage::BinaryChar { signed: false })), (None, Some(Usage::BinaryChar { signed: true }))]);
404    }
405
406    #[test]
407    fn returning_omitted_leaves_a_programs_header_and_nothing_else() {
408        let header = "       PROCEDURE DIVISION USING A RETURNING OMITTED.\n";
409        let text = format!("       IDENTIFICATION DIVISION.\n       PROGRAM-ID. T.\n       DATA DIVISION.\n       LINKAGE SECTION.\n       01 A PIC X.\n{header}           CALL 'S' RETURNING OMITTED.\n           GOBACK.\n");
410        let p = extended(&text).unwrap();
411        assert_eq!((p.using.len(), p.returning.as_deref()), (1, None));
412        let shown: Vec<(u32, u32, &str)> = p.messages.iter().map(|m| (m.pos.line, m.pos.col, m.message.as_str())).collect();
413        let warning = format!("{}: the program is read with no RETURNING phrase, and returns its RETURN-CODE to its caller as any program does", super::RETURNING_OMITTED);
414        assert_eq!(shown, [(6, header.find("RETURNING").unwrap() as u32 + 1, warning.as_str())]);
415        assert!(format!("{:?}", p.paragraphs[0].statements[0]).contains("\"OMITTED\""), "a CALL's RETURNING OMITTED is not the header's");
416        assert_eq!(crate::parse(&text).unwrap().returning.as_deref(), Some("OMITTED"));
417        let function = "       IDENTIFICATION DIVISION.\n       FUNCTION-ID. F.\n       PROCEDURE DIVISION RETURNING OMITTED.\n           GOBACK.\n       END FUNCTION F.\n";
418        let f = extended(function).unwrap();
419        assert_eq!((f.returning.as_deref(), f.messages.len()), (Some("OMITTED"), 0), "a function returns an item");
420    }
421
422    #[test]
423    fn strict_reads_none_of_it() {
424        let strict = |data: &str| crate::parse(&source(data, ""));
425        assert_eq!(strict("       01  N PIC X(2) VALUE 'A' & 'B'.\n").unwrap_err().message, "literal concatenation with & is not Enterprise COBOL's");
426        assert_eq!(strict("       78  N VALUE 1.\n").unwrap().working_storage[0].level, 78);
427        assert!(strict("       01  A BINARY-LONG.\n").unwrap_err().message.contains("BINARY-LONG is not a data description clause"));
428    }
429}