1use crate::lexer::{Tok, Token};
7use crate::{Error, Pos};
8use std::collections::HashMap;
9
10pub const CONSTANT: &str = "IWX0002-W constant entry (Micro Focus and GnuCOBOL; Enterprise COBOL has no level 78 and no CONSTANT clause)";
11pub const CONCATENATION: &str = "IWX0004-W literal concatenation with & (Micro Focus and GnuCOBOL; Enterprise COBOL has none)";
12pub const BINARY_USAGE: &str = "IWX0005-W the COBOL 2002 binary usage (Micro Focus and GnuCOBOL; not Enterprise COBOL's)";
13pub const NO_IDENTIFICATION_HEADER: &str = "IWX0006-W PROGRAM-ID with no IDENTIFICATION DIVISION header before it (COBOL 2002, Micro Focus and GnuCOBOL; Enterprise COBOL requires the header)";
14pub const ASSIGN_ITEM: &str = "IWX0007-W ASSIGN to a data item (Micro Focus and GnuCOBOL; Enterprise COBOL's assignment-name is never a data item)";
15
16const BINARY_USAGES: &[(&str, &str)] = &[("BINARY-SHORT", "9(4)"), ("BINARY-LONG", "9(9)"), ("BINARY-DOUBLE", "9(18)")];
19
20const FIGURATIVES: &[&str] = &["ZERO", "ZEROS", "ZEROES", "SPACE", "SPACES", "HIGH-VALUE", "HIGH-VALUES", "LOW-VALUE", "LOW-VALUES", "QUOTE", "QUOTES", "NULL", "NULLS"];
21
22pub fn rewrite(tokens: Vec<Token>, cards: &[String]) -> Result<Vec<Token>, Error> {
26 let mut options = numeric::Options::default();
27 for card in cards {
28 options.apply(card).ok();
29 }
30 let mut r = Rewrite { tokens, at: 0, out: Vec::new(), constants: HashMap::new(), pending: Vec::new(), options };
31 let mut data = false;
32 while r.at < r.tokens.len() {
33 let division = r.tokens.get(r.at + 1).is_some_and(|t| matches!(&t.tok, Tok::Word(w) if w == "DIVISION"));
34 match &r.tokens[r.at].tok {
35 Tok::Word(w) if division => data = w == "DATA",
36 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")) => {
37 r.constant()?;
38 continue;
39 }
40 Tok::Word(w) if data && (w == "BINARY-CHAR" || BINARY_USAGES.iter().any(|(u, _)| u == w)) => {
41 r.binary_usage()?;
42 continue;
43 }
44 Tok::Word(w) if w == "PROGRAM-ID" && !ends_with_header(&r.out) => r.identification_header(),
45 _ => {}
46 }
47 let value = r.value()?;
48 r.push(value);
49 }
50 if let Some(last) = r.out.last_mut() {
51 last.messages.append(&mut r.pending);
52 }
53 Ok(r.out)
54}
55
56struct Rewrite {
57 tokens: Vec<Token>,
58 at: usize,
59 out: Vec<Token>,
60 constants: HashMap<String, Tok>,
61 pending: Vec<Error>,
63 options: numeric::Options,
65}
66
67impl Rewrite {
68 fn push(&mut self, mut token: Token) {
69 token.messages.splice(0..0, self.pending.drain(..));
70 self.out.push(token);
71 }
72
73 fn substituted(&self, k: usize) -> Token {
75 let mut token = self.tokens[k].clone();
76 match &token.tok {
77 Tok::Word(w) => {
78 if let Some(value) = self.constants.get(w) {
79 token.tok = value.clone();
80 token.spelled = None;
81 }
82 }
83 Tok::Pic(p) => {
84 if let Some(p) = picture(p, &self.constants) {
85 token.tok = Tok::Pic(p);
86 }
87 }
88 _ => {}
89 }
90 token
91 }
92
93 fn value(&mut self) -> Result<Token, Error> {
95 let mut left = self.substituted(self.at);
96 self.at += 1;
97 while self.tokens.get(self.at).is_some_and(|t| t.tok == Tok::Ampersand) {
98 let amp = self.tokens[self.at].clone();
99 if self.at + 1 >= self.tokens.len() {
100 return Err(Error::at(amp.pos, "& with no literal after it"));
101 }
102 let right = self.substituted(self.at + 1);
103 self.at += 2;
104 left = join(left, amp, right, |bytes| self.options.code_page().decode(bytes))?;
105 }
106 Ok(left)
107 }
108
109 fn word_at(&self, ahead: usize) -> Option<&str> {
110 match self.tokens.get(self.at + ahead).map(|t| &t.tok) {
111 Some(Tok::Word(w)) => Some(w),
112 _ => None,
113 }
114 }
115
116 fn constant(&mut self) -> Result<(), Error> {
121 let level = self.tokens[self.at].clone();
122 let named = self.tokens[self.at + 1].clone();
123 let Tok::Word(name) = named.tok else { unreachable!("the caller saw a word") };
124 let seventy_eight = level.tok == Tok::Number("78".into());
125 self.at += if seventy_eight { 2 } else { 3 };
126 self.pending.extend(level.messages);
127 self.pending.extend(named.messages);
128 let refused = |at: Pos, why: &str| Error::at(at, format!("constant {name}: {why}"));
129 match (self.word_at(0), self.word_at(1)) {
130 (Some("IS"), Some("GLOBAL")) => self.at += 2,
131 (Some("GLOBAL"), _) => self.at += 1,
132 _ => {}
133 }
134 let here = self.tokens.get(self.at).map_or(named.pos, |t| t.pos);
135 if seventy_eight {
136 if self.word_at(0) != Some("VALUE") {
137 return Err(refused(here, "a level-78 entry is VALUE and its value, then a period"));
138 }
139 self.at += 1;
140 if self.word_at(0) == Some("IS") {
141 self.at += 1;
142 }
143 } else if self.word_at(0) == Some("AS") {
144 self.at += 1;
145 }
146 if self.at >= self.tokens.len() {
147 return Err(refused(here, "VALUE with no value"));
148 }
149 let value = self.value()?;
150 let literal = match &value.tok {
151 Tok::Alnum(_) | Tok::Hex(_) | Tok::National(_) | Tok::Number(_) => true,
152 Tok::Word(w) => FIGURATIVES.contains(&w.as_str()),
153 _ => false,
154 };
155 let ended = self.tokens.get(self.at).is_some_and(|t| t.tok == Tok::Period);
156 if !literal || !ended {
157 let at = if literal { self.tokens.get(self.at).map_or(value.pos, |t| t.pos) } else { value.pos };
158 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"));
159 }
160 self.pending.extend(self.tokens[self.at].messages.iter().cloned());
161 self.at += 1;
162 self.pending.push(Error::warning(level.pos, format!("{CONSTANT}: {name} stands for its value wherever it is used after this entry")));
163 self.pending.extend(value.messages);
164 self.constants.insert(name, value.tok);
165 Ok(())
166 }
167
168 fn identification_header(&mut self) {
170 let at = self.tokens[self.at].clone();
171 let made = |tok: Tok, messages: Vec<Error>| Token { tok, pos: at.pos, area_a: at.area_a, spelled: None, after_comma: false, messages };
172 let warning = Error::warning(at.pos, format!("{NO_IDENTIFICATION_HEADER}: the program reads as though IDENTIFICATION DIVISION. came before it"));
173 self.push(made(Tok::Word("IDENTIFICATION".into()), vec![warning]));
174 self.push(made(Tok::Word("DIVISION".into()), Vec::new()));
175 self.push(made(Tok::Period, Vec::new()));
176 }
177
178 fn binary_usage(&mut self) -> Result<(), Error> {
181 let token = self.tokens[self.at].clone();
182 let Tok::Word(usage) = &token.tok else { unreachable!("the caller saw a word") };
183 let Some(&(_, digits)) = BINARY_USAGES.iter().find(|(u, _)| u == usage) else {
184 return Err(Error::at(token.pos, "BINARY-CHAR is a one-byte binary item, and ironwork's binary items are two, four or eight bytes, as Enterprise COBOL's are"));
185 };
186 let mut messages = Vec::new();
187 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()) {
188 messages.extend(self.out.pop().map(|t| t.messages).unwrap_or_default());
189 }
190 if self.out.last().is_some_and(|t| t.tok == Tok::Word("USAGE".into())) {
191 messages.extend(self.out.pop().map(|t| t.messages).unwrap_or_default());
192 }
193 self.at += 1;
194 let signed = match self.tokens.get(self.at).map(|t| &t.tok) {
195 Some(Tok::Word(w)) if w == "SIGNED" || w == "UNSIGNED" => {
196 self.at += 1;
197 w == "SIGNED"
198 }
199 _ => true,
200 };
201 let picture = format!("{}{digits}", if signed { "S" } else { "" });
202 let shown = format!("{BINARY_USAGE}: {usage}{} is read as PIC {picture} COMP-5", if signed { "" } else { " UNSIGNED" });
203 messages.push(Error::warning(token.pos, shown));
204 messages.extend(token.messages.iter().cloned());
205 let made = |tok: Tok, messages: Vec<Error>| Token { tok, pos: token.pos, area_a: false, spelled: None, after_comma: false, messages };
206 self.push(made(Tok::Word("PIC".into()), messages));
207 self.push(made(Tok::Pic(picture), Vec::new()));
208 self.push(made(Tok::Word("COMP-5".into()), Vec::new()));
209 Ok(())
210 }
211}
212
213fn ends_with_header(tokens: &[Token]) -> bool {
215 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)
216}
217
218fn picture(text: &str, constants: &HashMap<String, Tok>) -> Option<String> {
221 let (mut out, mut rest, mut changed) = (String::new(), text, false);
222 while let Some(open) = rest.find('(') {
223 let Some(close) = rest[open..].find(')').map(|c| open + c) else { break };
224 match constants.get(&rest[open + 1..close]) {
225 Some(Tok::Number(n)) if n.bytes().all(|b| b.is_ascii_digit()) => {
226 out.push_str(&rest[..=open]);
227 out.push_str(n);
228 out.push(')');
229 changed = true;
230 }
231 _ => out.push_str(&rest[..=close]),
232 }
233 rest = &rest[close + 1..];
234 }
235 out.push_str(rest);
236 changed.then_some(out)
237}
238
239fn join(left: Token, amp: Token, right: Token, decode: impl Fn(&[u8]) -> String) -> Result<Token, Error> {
243 let tok = match (&left.tok, &right.tok) {
244 (Tok::Alnum(a), Tok::Alnum(b)) => Tok::Alnum(format!("{a}{b}")),
245 (Tok::Hex(a), Tok::Hex(b)) => Tok::Hex([a.as_slice(), b].concat()),
246 (Tok::Alnum(a), Tok::Hex(b)) => Tok::Alnum(format!("{a}{}", decode(b))),
247 (Tok::Hex(a), Tok::Alnum(b)) => Tok::Alnum(format!("{}{b}", decode(a))),
248 (Tok::National(a), Tok::National(b)) => Tok::National(format!("{a}{b}")),
249 _ => return Err(Error::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")),
250 };
251 let mut messages = left.messages;
252 messages.push(Error::warning(amp.pos, format!("{CONCATENATION}: the literals on either side are one literal")));
253 messages.extend(amp.messages);
254 messages.extend(right.messages);
255 Ok(Token { tok, pos: left.pos, area_a: left.area_a, spelled: None, after_comma: left.after_comma, messages })
256}
257
258#[cfg(test)]
259mod tests {
260 use crate::ast::{Literal, Usage};
261 use crate::copy::Libraries;
262 use numeric::Compliance;
263
264 fn source(data: &str, procedure: &str) -> String {
265 format!(" IDENTIFICATION DIVISION.\n PROGRAM-ID. T.\n DATA DIVISION.\n WORKING-STORAGE SECTION.\n{data} PROCEDURE DIVISION.\n{procedure} GOBACK.\n")
266 }
267
268 fn extended(text: &str) -> Result<crate::ast::Program, crate::Error> {
269 crate::parse_with(text, &Libraries::default().with_compliance(Compliance::Extended))
270 }
271
272 #[test]
273 fn a_constant_stands_for_its_value_in_pictures_values_occurs_and_statements() {
274 let data = concat!(
275 " 78 MAX-LEN VALUE 3.\n",
276 " 78 GREETING IS GLOBAL VALUE IS 'AB' & X'C1'.\n",
277 " 01 QUOTED CONSTANT AS 'Q'.\n",
278 " 01 NOTHING CONSTANT GLOBAL SPACE.\n",
279 " 01 X PIC X(MAX-LEN) VALUE GREETING.\n",
280 " 01 G.\n",
281 " 05 T OCCURS MAX-LEN TIMES PIC X VALUE NOTHING.\n",
282 );
283 let p = extended(&source(data, " MOVE QUOTED TO X.\n")).unwrap();
284 let names: Vec<&str> = p.working_storage.iter().filter_map(|e| e.name.as_deref()).collect();
285 assert_eq!(names, ["X", "G", "T"]);
286 let x = &p.working_storage[0];
287 assert_eq!((x.picture.as_deref(), x.value.clone()), (Some("X(3)"), Some(Literal::Alnum("ABA".into()))));
288 assert_eq!(p.working_storage[2].occurs, Some(3));
289 let warnings: Vec<(u32, String)> = p.messages.iter().map(|m| (m.pos.line, m.message.clone())).collect();
290 assert_eq!(warnings.len(), 5, "{warnings:?}");
291 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"));
292 assert!(warnings[2].1.starts_with(super::CONCATENATION) && warnings[2].0 == 6);
293 assert!(warnings[3..].iter().all(|(_, m)| m.starts_with(super::CONSTANT)));
294 assert!(format!("{:?}", p.paragraphs[0].statements[0]).contains("Alnum(\"Q\")"));
295 }
296
297 #[test]
298 fn a_constant_whose_value_is_an_expression_or_missing_is_refused_by_name() {
299 let refused = |data: &str| extended(&source(data, "")).unwrap_err().message;
300 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");
301 assert_eq!(refused(" 78 N PIC 9 VALUE 1.\n"), "constant N: a level-78 entry is VALUE and its value, then a period");
302 assert!(refused(" 01 N PIC X VALUE 'A' & B.\n").starts_with("& joins two alphanumeric or hexadecimal literals"));
303 assert!(refused(" 01 N PIC X VALUE 'A' & N'B'.\n").starts_with("& joins two alphanumeric or hexadecimal literals"));
304 }
305
306 #[test]
307 fn the_binary_usages_are_comp_5_pictures_and_binary_char_is_refused() {
308 let data = " 01 A USAGE IS BINARY-LONG.\n 01 B BINARY-SHORT UNSIGNED VALUE 7.\n 01 C BINARY-DOUBLE SIGNED.\n";
309 let p = extended(&source(data, "")).unwrap();
310 let read: Vec<(Option<&str>, Option<Usage>)> = p.working_storage.iter().map(|e| (e.picture.as_deref(), e.usage)).collect();
311 assert_eq!(read, [(Some("S9(9)"), Some(Usage::NativeBinary)), (Some("9(4)"), Some(Usage::NativeBinary)), (Some("S9(18)"), Some(Usage::NativeBinary))]);
312 assert_eq!(p.working_storage[1].value, Some(Literal::Number("7".into())));
313 let shown: Vec<String> = p.messages.iter().map(|m| m.message.clone()).collect();
314 assert_eq!(shown[1], format!("{}: BINARY-SHORT UNSIGNED is read as PIC 9(4) COMP-5", super::BINARY_USAGE));
315 let refused = extended(&source(" 01 D BINARY-CHAR.\n", "")).unwrap_err();
316 assert!(refused.message.starts_with("BINARY-CHAR is a one-byte binary item") && refused.pos.line == 5);
317 }
318
319 #[test]
320 fn strict_reads_none_of_it() {
321 let strict = |data: &str| crate::parse(&source(data, ""));
322 assert_eq!(strict(" 01 N PIC X(2) VALUE 'A' & 'B'.\n").unwrap_err().message, "literal concatenation with & is not Enterprise COBOL's");
323 assert_eq!(strict(" 78 N VALUE 1.\n").unwrap().working_storage[0].level, 78);
324 assert!(strict(" 01 A BINARY-LONG.\n").unwrap_err().message.contains("BINARY-LONG is not a data description clause"));
325 }
326}