1use 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
21struct BinaryUsage {
23 word: &'static str,
24 digits: &'static str,
25 signed: Option<bool>,
27 origin: &'static str,
28}
29
30const 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
46pub 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 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 pending: Vec<Error>,
110 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 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 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 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 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 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 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 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
294fn 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
299fn 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
320fn 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}