1use crate::source::{FreeSpan, Source};
2use crate::{Error, Pos};
3use numeric::Compliance;
4
5#[derive(Clone, Debug, PartialEq, Eq)]
6pub enum Tok {
7 Word(String),
9 Number(String),
11 Alnum(String),
12 Hex(Vec<u8>),
13 National(String),
14 Dbcs(String),
16 Pic(String),
17 Exec(String),
19 Period,
20 LParen,
21 RParen,
22 Colon,
23 Plus,
24 Minus,
25 Star,
26 Slash,
27 Power,
28 Eq,
29 Lt,
30 Gt,
31 Le,
32 Ge,
33 Ampersand,
36}
37
38pub const NOT_EQUAL: &str = "IWX0003-W <> (Micro Focus and GnuCOBOL; Enterprise COBOL writes NOT =) is read as NOT =";
40
41#[derive(Clone, Debug, PartialEq, Eq)]
42pub struct Token {
43 pub tok: Tok,
44 pub pos: Pos,
45 pub area_a: bool,
47 pub spelled: Option<String>,
49 pub after_comma: bool,
51 pub messages: Vec<Error>,
53}
54
55struct Lexer<'a> {
56 chars: Vec<char>,
57 positions: &'a [Pos],
58 at: usize,
59 tokens: Vec<Token>,
60 decimal_comma: bool,
62 currency: Vec<char>,
64 outer: Vec<bool>,
67 comma_pending: bool,
68 pending: Vec<Error>,
70 extended: bool,
71 free: &'a [FreeSpan],
73 warned: Vec<bool>,
74 n_is_dbcs: bool,
76}
77
78const DBCS_LITERAL_MAX: usize = 28;
80
81pub fn lex(source: &Source) -> Result<Vec<Token>, Error> {
82 lex_under(source, Compliance::Strict)
83}
84
85pub fn lex_under(source: &Source, compliance: Compliance) -> Result<Vec<Token>, Error> {
88 let mut options = numeric::Options::default();
89 for card in &source.options {
90 options.apply(card).ok();
91 }
92 let n_is_dbcs = options.nsymbol == numeric::Nsymbol::Dbcs;
93 let mut lx = Lexer {
94 chars: source.text.chars().collect(),
95 positions: &source.positions,
96 at: 0,
97 tokens: Vec::new(),
98 decimal_comma: false,
99 currency: Vec::new(),
100 outer: Vec::new(),
101 comma_pending: false,
102 pending: Vec::new(),
103 extended: compliance == Compliance::Extended,
104 free: &source.free,
105 warned: vec![false; source.free.len()],
106 n_is_dbcs,
107 };
108 while lx.at < lx.chars.len() {
109 lx.next_token()?;
110 }
111 Ok(lx.tokens)
112}
113
114fn is_word_char(c: char) -> bool {
115 c.is_ascii_alphanumeric() || c == '-' || c == '_'
116}
117
118fn ebcdic_lowercase(byte: u8) -> Option<char> {
123 let (start, first) = match byte {
124 0x81..=0x89 => (0x81, b'a'),
125 0x91..=0x99 => (0x91, b'j'),
126 0xA2..=0xA9 => (0xA2, b's'),
127 _ => return None,
128 };
129 Some(char::from(first + (byte - start)))
130}
131
132fn non_cobol(c: char) -> bool {
133 u32::from(c) <= 0xFF && !c.is_ascii_alphanumeric() && !" \n+-*/=$,;.\"'()><:_&".contains(c)
134}
135
136impl Lexer<'_> {
137 fn peek(&self, ahead: usize) -> Option<char> {
138 self.chars.get(self.at + ahead).copied()
139 }
140
141 fn pos(&self) -> Pos {
142 self.positions.get(self.at).copied().unwrap_or_default()
143 }
144
145 fn separator_follows(&self, ahead: usize) -> bool {
146 self.peek(ahead).is_none_or(|c| c == ' ' || c == '\n')
147 }
148
149 fn emit(&mut self, tok: Tok, pos: Pos) {
150 let before = |back: usize| match self.tokens.len().checked_sub(back).map(|i| &self.tokens[i].tok) {
151 Some(Tok::Word(p)) => p.as_str(),
152 _ => "",
153 };
154 match &tok {
155 Tok::Word(w) => match w.as_str() {
156 "PROGRAM-ID" => self.outer.push(self.decimal_comma),
157 "PROGRAM" if before(1) == "END" => self.decimal_comma = self.outer.pop().unwrap_or(false),
158 "COMMA" if before(1) == "DECIMAL-POINT" || before(1) == "IS" && before(2) == "DECIMAL-POINT" => self.decimal_comma = true,
159 _ => {}
160 },
161 Tok::Alnum(a) => {
162 let mut chars = a.chars();
163 let names_symbol = before(1) == "SYMBOL" || (1..=3).any(|back| before(back) == "CURRENCY" && (1..back).all(|b| matches!(before(b), "SIGN" | "IS")));
164 if let (Some(c), None, true) = (chars.next(), chars.next(), names_symbol) {
165 self.currency.push(c);
166 }
167 }
168 Tok::Hex(bytes) => {
169 let names_symbol = (1..=3).any(|back| before(back) == "CURRENCY" && (1..back).all(|b| matches!(before(b), "SIGN" | "IS")));
170 if let ([byte], true) = (bytes.as_slice(), names_symbol)
171 && let Some(c) = ebcdic_lowercase(*byte)
172 {
173 self.currency.push(c);
174 }
175 }
176 _ => {}
177 }
178 let spelled = match &tok {
179 Tok::Word(w) => self
180 .at
181 .checked_sub(w.chars().count())
182 .map(|start| &self.chars[start..self.at])
183 .filter(|raw| raw.iter().any(char::is_ascii_lowercase))
184 .map(|raw| raw.iter().collect::<String>())
185 .filter(|raw| raw.eq_ignore_ascii_case(w)),
186 _ => None,
187 };
188 let after_comma = std::mem::take(&mut self.comma_pending);
189 let span = self.free.iter().position(|s| s.holds(pos));
190 let area_a = match span {
191 Some(k) => {
192 if !std::mem::replace(&mut self.warned[k], true)
193 && let Some(warning) = &self.free[k].warning
194 {
195 self.pending.insert(0, warning.clone());
196 }
197 self.tokens.last().is_none_or(|t| t.tok == Tok::Period) && !matches!(&tok, Tok::Word(w) if rt::reserved_words::is_reserved(w))
198 }
199 None => (8..=11).contains(&pos.col),
200 };
201 self.tokens.push(Token { tok, pos, area_a, spelled, after_comma, messages: std::mem::take(&mut self.pending) });
202 }
203
204 fn point(&self) -> char {
206 if self.decimal_comma { ',' } else { '.' }
207 }
208
209 fn expecting_picture(&self) -> bool {
210 let words: Vec<&str> =
211 self.tokens.iter().rev().take(2).map(|t| if let Tok::Word(w) = &t.tok { w.as_str() } else { "" }).collect();
212 matches!(words.as_slice(), ["PIC" | "PICTURE", ..] | ["IS", "PIC" | "PICTURE"])
213 }
214
215 fn next_token(&mut self) -> Result<(), Error> {
216 let c = self.chars[self.at];
217 let pos = self.pos();
218 let point = self.point();
219 let leading_point = c == ',' && point == ',' && self.peek(1).is_some_and(|d| d.is_ascii_digit()) && !self.at.checked_sub(1).is_some_and(|i| is_word_char(self.chars[i]));
220 if leading_point && self.expecting_picture() {
221 return self.picture(pos);
222 }
223 if leading_point {
224 let tok = self.number_or_word(pos)?;
225 self.emit(tok, pos);
226 return Ok(());
227 }
228 if c == ' ' || c == '\n' || c == ',' || c == ';' {
229 self.comma_pending |= c == ',' || c == ';';
230 self.at += 1;
231 return Ok(());
232 }
233 if self.expecting_picture() {
234 return self.picture(pos);
235 }
236 let next = self.peek(1);
237 let quote_next = matches!(next, Some('\'' | '"'));
238 match c {
239 '\'' | '"' => {
240 let text = self.quoted(pos)?;
241 self.emit(Tok::Alnum(text), pos);
242 }
243 'X' | 'x' if quote_next => {
244 self.at += 1;
245 let text = self.quoted(pos)?;
246 let bytes = unhex(&text).ok_or_else(|| Error::at(pos, format!("X'{text}' is not an even number of hex digits")))?;
247 self.emit(Tok::Hex(bytes), pos);
248 }
249 'N' | 'n' if matches!(next, Some('X' | 'x')) && matches!(self.peek(2), Some('\'' | '"')) => {
250 self.at += 2;
251 let text = self.quoted(pos)?;
252 let units = unhex(&text).filter(|b| !b.is_empty() && b.len().is_multiple_of(2) && b.len() <= 160);
253 let units = units.map(|b| b.chunks(2).map(|u| u16::from_be_bytes([u[0], u[1]])).collect::<Vec<_>>());
254 let national = units.and_then(|u| String::from_utf16(&u).ok());
255 let national = national.ok_or_else(|| Error::at(pos, format!("NX'{text}': a national hexadecimal literal is 4 to 320 hex digits, four to each UTF-16 code unit")))?;
256 self.emit(Tok::National(national), pos);
257 }
258 'N' | 'n' if quote_next && self.n_is_dbcs => self.dbcs_literal(pos)?,
259 'G' | 'g' if quote_next => self.dbcs_literal(pos)?,
260 'N' | 'n' if quote_next => {
261 self.at += 1;
262 let text = self.quoted(pos)?;
263 self.emit(Tok::National(text), pos);
264 }
265 'Z' | 'z' if quote_next => {
266 self.at += 1;
267 let text = self.quoted(pos)?;
268 self.emit(Tok::Alnum(format!("{text}\0")), pos);
269 }
270 '.' if self.separator_follows(1) => {
271 self.at += 1;
272 self.emit(Tok::Period, pos);
273 }
274 '+' | '-' if next.is_some_and(|n| n.is_ascii_digit() || (n == point && self.peek(2).is_some_and(|d| d.is_ascii_digit()))) => {
275 self.at += 1;
276 let digits = self.number_or_word(pos)?;
277 match digits {
278 Tok::Number(n) => self.emit(Tok::Number(format!("{c}{n}")), pos),
279 _ => return Err(Error::at(pos, "a sign must be followed by a number")),
280 }
281 }
282 _ if c.is_ascii_alphanumeric() || c == '.' || non_cobol(c) => {
283 let tok = self.number_or_word(pos)?;
284 match tok {
285 Tok::Word(w) if w == "EXEC" || w == "EXECUTE" => {
286 let tok = self.exec_block(pos)?;
287 self.emit(tok, pos);
288 }
289 tok => self.emit(tok, pos),
290 }
291 }
292 '<' if self.extended && next == Some('>') => {
293 self.pending.push(Error::warning(pos, NOT_EQUAL));
294 self.at += 2;
295 self.emit(Tok::Word("NOT".into()), pos);
296 let at = self.positions.get(self.at - 1).copied().unwrap_or(pos);
297 self.emit(Tok::Eq, at);
298 }
299 '&' if self.extended && matches!(self.tokens.last().map(|t| &t.tok), Some(Tok::Alnum(_) | Tok::Hex(_) | Tok::National(_) | Tok::Word(_))) => {
300 self.at += 1;
301 self.emit(Tok::Ampersand, pos);
302 }
303 _ => {
304 let (tok, len) = match (c, next) {
305 ('*', Some('*')) => (Tok::Power, 2),
306 ('<', Some('=')) => (Tok::Le, 2),
307 ('>', Some('=')) => (Tok::Ge, 2),
308 ('*', _) => (Tok::Star, 1),
309 ('/', _) => (Tok::Slash, 1),
310 ('+', _) => (Tok::Plus, 1),
311 ('-', _) => (Tok::Minus, 1),
312 ('=', _) => (Tok::Eq, 1),
313 ('<', _) => (Tok::Lt, 1),
314 ('>', _) => (Tok::Gt, 1),
315 ('(', _) => (Tok::LParen, 1),
316 (')', _) => (Tok::RParen, 1),
317 (':', _) => (Tok::Colon, 1),
318 ('&', _) if matches!(self.tokens.last().map(|t| &t.tok), Some(Tok::Alnum(_) | Tok::Hex(_) | Tok::National(_))) => {
319 return Err(Error::at(pos, "literal concatenation with & is not Enterprise COBOL's"));
320 }
321 _ => return Err(Error::at(pos, format!("unexpected character {c:?}"))),
322 };
323 self.at += len;
324 self.emit(tok, pos);
325 }
326 }
327 Ok(())
328 }
329
330 fn exec_block(&mut self, pos: Pos) -> Result<Tok, Error> {
332 let start = self.at;
333 let mut quote: Option<char> = None;
334 while let Some(c) = self.peek(0) {
335 match quote {
336 Some(q) if c == q => quote = None,
337 Some(_) => {}
338 None if c == '\'' || c == '"' => quote = Some(c),
339 None if c.eq_ignore_ascii_case(&'E') => {
340 let ahead: String = self.chars[self.at..].iter().take(8).collect();
341 let boundary = self.chars.get(self.at + 8).is_none_or(|d| !is_word_char(*d)) && (self.at == 0 || !is_word_char(self.chars[self.at - 1]));
342 if ahead.eq_ignore_ascii_case("END-EXEC") && boundary {
343 let text: String = self.chars[start..self.at].iter().collect();
344 self.at += 8;
345 return Ok(Tok::Exec(text.split_whitespace().collect::<Vec<_>>().join(" ")));
346 }
347 }
348 None => {}
349 }
350 self.at += 1;
351 }
352 Err(Error::at(pos, "EXEC with no END-EXEC"))
353 }
354
355 fn dbcs_literal(&mut self, pos: Pos) -> Result<(), Error> {
360 self.at += 1;
361 let text = self.quoted(pos)?;
362 let text = text.strip_prefix('\u{E}').unwrap_or(&text);
363 let text = text.strip_suffix('\u{F}').unwrap_or(text).to_owned();
364 let count = text.chars().count();
365 if count == 0 || count > DBCS_LITERAL_MAX {
366 return Err(Error::at(pos, format!("a DBCS literal holds 1 to {DBCS_LITERAL_MAX} characters, not {count}")));
367 }
368 self.emit(Tok::Dbcs(text), pos);
369 Ok(())
370 }
371
372 fn quoted(&mut self, pos: Pos) -> Result<String, Error> {
373 let quote = self.chars[self.at];
374 self.at += 1;
375 let mut text = String::new();
376 loop {
377 match self.peek(0) {
378 None | Some('\n') => return Err(Error::at(pos, "an unterminated literal")),
379 Some(c) if c == quote && self.peek(1) == Some(quote) => {
380 text.push(quote);
381 self.at += 2;
382 }
383 Some(c) if c == quote => {
384 self.at += 1;
385 return Ok(text);
386 }
387 Some(c) => {
388 text.push(c);
389 self.at += 1;
390 }
391 }
392 }
393 }
394
395 fn number_or_word(&mut self, pos: Pos) -> Result<Tok, Error> {
396 let start = self.at;
397 while let Some(c) = self.peek(0).filter(|&c| is_word_char(c) || non_cobol(c)) {
398 if non_cobol(c) {
399 self.pending.push(Error::at(self.pos(), format!("non-COBOL character {c:?}: the character was accepted")).graded(crate::Severity::Error));
400 }
401 self.at += 1;
402 }
403 let run: String = self.chars[start..self.at].iter().collect();
404 let all_digits = run.chars().all(|c| c.is_ascii_digit());
405 if all_digits && self.peek(0) == Some(self.point()) && self.peek(1).is_some_and(|c| c.is_ascii_digit()) {
406 self.at += 1;
407 let frac_start = self.at;
408 while self.peek(0).is_some_and(|c| c.is_ascii_digit()) {
409 self.at += 1;
410 }
411 let frac: String = self.chars[frac_start..self.at].iter().collect();
412 return Ok(Tok::Number(format!("{run}.{frac}")));
413 }
414 if run.is_empty() {
415 return Err(Error::at(pos, "unexpected '.'"));
416 }
417 Ok(if all_digits { Tok::Number(run) } else { Tok::Word(run.to_ascii_uppercase()) })
418 }
419
420 fn picture(&mut self, pos: Pos) -> Result<(), Error> {
422 let start = self.at;
423 while self.peek(0).is_some_and(|c| c != ' ' && c != '\n') {
424 self.at += 1;
425 }
426 let mut end = self.at;
427 if end > start && matches!(self.chars[end - 1], '.' | ',' | ';') {
428 end -= 1;
429 }
430 let text: String = self.chars[start..end].iter().collect();
431 if text.is_empty() {
432 return Err(Error::at(pos, "PICTURE with no character-string"));
433 }
434 self.at = end;
435 let text: String = text.chars().map(|c| if self.currency.contains(&c) { c } else { c.to_ascii_uppercase() }).collect();
436 let tok = if matches!(text.as_str(), "IS" | "SYMBOL") && self.tokens.last().is_some_and(|t| matches!(&t.tok, Tok::Word(w) if w == "PIC" || w == "PICTURE")) {
437 Tok::Word(text)
438 } else {
439 Tok::Pic(text)
440 };
441 self.emit(tok, pos);
442 Ok(())
443 }
444}
445
446fn unhex(text: &str) -> Option<Vec<u8>> {
447 if !text.len().is_multiple_of(2) || !text.is_ascii() {
448 return None;
449 }
450 (0..text.len()).step_by(2).map(|i| u8::from_str_radix(&text[i..i + 2], 16).ok()).collect()
451}
452
453#[cfg(test)]
454mod tests {
455 use super::*;
456 use crate::source;
457
458 fn toks(text: &str) -> Vec<Tok> {
459 lex(&source::read(text).unwrap()).unwrap().into_iter().map(|t| t.tok).collect()
460 }
461
462 fn w(s: &str) -> Tok {
463 Tok::Word(s.into())
464 }
465
466 #[test]
467 fn numbers_words_and_the_separator_period() {
468 assert_eq!(toks(" 05 A-1 VALUE 0.1."), [Tok::Number("05".into()), w("A-1"), w("VALUE"), Tok::Number("0.1".into()), Tok::Period]);
469 assert_eq!(toks(" VALUE -12345."), [w("VALUE"), Tok::Number("-12345".into()), Tok::Period]);
470 assert_eq!(toks(" 100-MAIN."), [w("100-MAIN"), Tok::Period]);
471 }
472
473 #[test]
474 fn operators_need_spaces_and_signed_literals_do_not() {
475 assert_eq!(toks(" A - 1 ** 2"), [w("A"), Tok::Minus, Tok::Number("1".into()), Tok::Power, Tok::Number("2".into())]);
476 assert_eq!(toks(" >= <= ("), [Tok::Ge, Tok::Le, Tok::LParen]);
477 }
478
479 #[test]
480 fn literals() {
481 assert_eq!(toks(" 'IT''S' X'F1C1' N'AB'"), [Tok::Alnum("IT'S".into()), Tok::Hex(vec![0xF1, 0xC1]), Tok::National("AB".into())]);
482 assert_eq!(toks(" NX'00410042' nx\"265ED83DDE00\""), [Tok::National("AB".into()), Tok::National("\u{265E}\u{1F600}".into())]);
483 assert_eq!(toks(" G'\u{E}AB\u{F}' g\"日本\""), [Tok::Dbcs("AB".into()), Tok::Dbcs("日本".into())]);
484 assert_eq!(toks(" CBL NSYMBOL(DBCS)\n N'AB' NX'0041'"), [Tok::Dbcs("AB".into()), Tok::National("A".into())]);
485 let long = format!(" G'{}'", "A".repeat(29));
486 assert!(lex(&source::read(&long).unwrap()).unwrap_err().message.contains("1 to 28 characters, not 29"));
487 for bad in ["NX'GH'", "NX'1'", "NX'004'", "NX'D83D'"] {
488 let e = lex(&source::read(&format!(" {bad}")).unwrap()).unwrap_err();
489 assert!(e.message.contains("a national hexadecimal literal is 4 to 320 hex digits"), "{bad}: {}", e.message);
490 }
491 }
492
493 #[test]
494 fn a_null_terminated_literal_ends_with_x00() {
495 assert_eq!(toks(" Z'ABC' z\"(I)V\""), [Tok::Alnum("ABC\0".into()), Tok::Alnum("(I)V\0".into())]);
496 }
497
498 #[test]
499 fn a_picture_is_one_token_and_keeps_its_own_periods() {
500 assert_eq!(toks(" PIC S9(3)V99 COMP-3."), [w("PIC"), Tok::Pic("S9(3)V99".into()), w("COMP-3"), Tok::Period]);
501 assert_eq!(toks(" PICTURE IS ZZ,ZZ9.99."), [w("PICTURE"), w("IS"), Tok::Pic("ZZ,ZZ9.99".into()), Tok::Period]);
502 }
503
504 #[test]
505 fn only_the_last_period_or_comma_before_the_space_is_a_separator() {
506 assert_eq!(toks(" PIC 9,9,9,."), [w("PIC"), Tok::Pic("9,9,9,".into()), Tok::Period]);
507 assert_eq!(toks(" PIC 999999999999.."), [w("PIC"), Tok::Pic("999999999999.".into()), Tok::Period]);
508 assert_eq!(toks(" PIC 99, VALUE 1."), [w("PIC"), Tok::Pic("99".into()), w("VALUE"), Tok::Number("1".into()), Tok::Period]);
509 assert_eq!(toks(" PIC 9.9,; VALUE 1."), [w("PIC"), Tok::Pic("9.9,".into()), w("VALUE"), Tok::Number("1".into()), Tok::Period]);
510 assert_eq!(toks(" PIC 999., VALUE 1."), [w("PIC"), Tok::Pic("999.".into()), w("VALUE"), Tok::Number("1".into()), Tok::Period]);
511 let comma = " DECIMAL-POINT IS COMMA.\n PIC 9.9.9,. PIC 999,,\n";
512 let pics: Vec<Tok> = toks(comma).into_iter().filter(|t| matches!(t, Tok::Pic(_))).collect();
513 assert_eq!(pics, [Tok::Pic("9.9.9,".into()), Tok::Pic("999,".into())]);
514 }
515
516 #[test]
517 fn commas_between_operands_are_separators() {
518 assert_eq!(toks(" F(A, 1)"), [w("F"), Tok::LParen, w("A"), Tok::Number("1".into()), Tok::RParen]);
519 assert_eq!(toks(" T(I,J)"), [w("T"), Tok::LParen, w("I"), w("J"), Tok::RParen]);
520 }
521
522 #[test]
523 fn an_exec_block_is_one_token() {
524 assert_eq!(
525 toks(" EXEC SQL SELECT A.B INTO :X FROM T WHERE C = 'END-EXEC'\n END-EXEC."),
526 [Tok::Exec("SQL SELECT A.B INTO :X FROM T WHERE C = 'END-EXEC'".into()), Tok::Period]
527 );
528 }
529
530 #[test]
531 fn a_comment_entry_holds_any_character() {
532 let program = concat!(
533 " IDENTIFICATION DIVISION.\n",
534 " PROGRAM-ID. CE3.\n",
535 " AUTHOR. Smith & Jones @ ACME #1, Café O'Grady.\n",
536 " INSTALLATION. \"HQ\n",
537 " ~ ^ ` { } | \\ ?\n",
538 " DATE-WRITTEN. 01/01/99.\n",
539 " PROCEDURE DIVISION.\n",
540 " GOBACK.\n",
541 );
542 let words = [w("IDENTIFICATION"), w("DIVISION"), Tok::Period, w("PROGRAM-ID"), Tok::Period, w("CE3"), Tok::Period];
543 let paragraphs = [w("AUTHOR"), Tok::Period, w("INSTALLATION"), Tok::Period, w("DATE-WRITTEN"), Tok::Period];
544 let procedure = [w("PROCEDURE"), w("DIVISION"), Tok::Period, w("GOBACK"), Tok::Period];
545 assert_eq!(toks(program), [&words[..], ¶graphs[..], &procedure[..]].concat());
546 }
547
548 #[test]
549 fn a_literal_continued_between_the_quotes_of_a_doubled_quote() {
550 let head = " \"A+0B-1C*2D";
551 let text = format!("{head}{}\"\n - \"\"9K(L)M>N<O\".\n", "=".repeat(72 - head.len() - 1));
552 assert_eq!(toks(&text), [Tok::Alnum(format!("A+0B-1C*2D{}\"9K(L)M>N<O", "=".repeat(72 - head.len() - 1))), Tok::Period]);
553 }
554
555 #[test]
556 fn a_continuation_that_opens_a_quote_after_a_closed_literal_is_a_second_literal() {
557 assert_eq!(toks(" VALUE 'ABC'\n - 'DEF'."), [w("VALUE"), Tok::Alnum("ABC".into()), Tok::Alnum("DEF".into()), Tok::Period]);
558 }
559
560 #[test]
561 fn an_ampersand_after_a_literal_is_named_as_concatenation() {
562 let error = |text: &str| lex(&source::read(text).unwrap()).unwrap_err().message;
563 assert!(error(" 'A' & 'B'").contains("literal concatenation with & is not Enterprise COBOL's"));
564 assert!(error(" NOTIFY=&SYSUID").contains("unexpected character '&'"));
565 }
566
567 #[test]
568 fn a_non_cobol_character_is_accepted_into_its_word_with_an_error() {
569 let lexed = lex(&source::read(" MOVE WS#1 TO %\u{1b} 'A@B'.").unwrap()).unwrap();
570 assert_eq!(lexed.iter().map(|t| t.tok.clone()).collect::<Vec<_>>(), [w("MOVE"), w("WS#1"), w("TO"), w("%\u{1b}"), Tok::Alnum("A@B".into()), Tok::Period]);
571 let messages: Vec<(usize, u32, &str, crate::Severity)> =
572 lexed.iter().enumerate().flat_map(|(i, t)| t.messages.iter().map(move |m| (i, m.pos.col, m.message.as_str(), m.severity))).collect();
573 assert_eq!(
574 messages,
575 [
576 (1, 19, "non-COBOL character '#': the character was accepted", crate::Severity::Error),
577 (3, 25, "non-COBOL character '%': the character was accepted", crate::Severity::Error),
578 (3, 26, "non-COBOL character '\\u{1b}': the character was accepted", crate::Severity::Error),
579 ]
580 );
581 let error = |text: &str| lex(&source::read(text).unwrap()).unwrap_err().message;
582 assert_eq!(error(" MOVE $X"), "unexpected character '$'");
583 assert_eq!(error(" MOVE \u{3042}"), "unexpected character '\u{3042}'");
584 }
585
586 #[test]
587 fn under_extended_not_equal_is_not_and_equals_and_an_ampersand_is_kept() {
588 let lexed = lex_under(&source::read(" IF A <> 'B' & X'C1' MOVE C & D").unwrap(), Compliance::Extended).unwrap();
589 let toks: Vec<Tok> = lexed.iter().map(|t| t.tok.clone()).collect();
590 assert_eq!(toks, [w("IF"), w("A"), w("NOT"), Tok::Eq, Tok::Alnum("B".into()), Tok::Ampersand, Tok::Hex(vec![0xC1]), w("MOVE"), w("C"), Tok::Ampersand, w("D")]);
591 assert_eq!(lexed[2].messages.iter().map(|m| (m.pos.col, m.message.as_str(), m.severity)).collect::<Vec<_>>(), [(17, NOT_EQUAL, crate::Severity::Warning)]);
592 assert_eq!(lexed[3].pos.col, 18);
593 let strict = |text: &str| lex(&source::read(text).unwrap()).map(|t| t.into_iter().map(|t| t.tok).collect::<Vec<_>>());
594 assert_eq!(strict(" A <> B"), Ok(vec![w("A"), Tok::Lt, Tok::Gt, w("B")]));
595 assert!(lex_under(&source::read(" NOTIFY=&SYSUID").unwrap(), Compliance::Extended).unwrap_err().message.contains("unexpected character '&'"));
596 }
597
598 #[test]
599 fn in_free_form_area_a_is_a_word_after_a_period_that_is_not_reserved() {
600 let text = "IDENTIFICATION DIVISION.\nPROCEDURE DIVISION.\nMAIN-P.\n MOVE A TO\n B.\n GOBACK.\n0100.\n";
601 let free = source::read_under(text, 0, false, Compliance::Extended).unwrap();
602 let lexed = lex_under(&free, Compliance::Extended).unwrap();
603 let marked: Vec<Tok> = lexed.iter().filter(|t| t.area_a).map(|t| t.tok.clone()).collect();
604 assert_eq!(marked, [w("MAIN-P"), Tok::Number("0100".into())]);
605 assert!(lexed[0].messages[0].message.starts_with("IWX0001-W"), "{:?}", lexed[0].messages);
606 assert!(lexed[1..].iter().all(|t| t.messages.is_empty()));
607 }
608
609 #[test]
610 fn area_a_is_marked() {
611 let t = lex(&source::read(" PARA.\n MOVE").unwrap()).unwrap();
612 assert!(t[0].area_a);
613 assert!(!t[2].area_a);
614 }
615
616 fn numbers(text: &str) -> Vec<String> {
617 toks(text).into_iter().filter_map(|t| if let Tok::Number(n) = t { Some(n) } else { None }).collect()
618 }
619
620 #[test]
621 fn under_decimal_point_is_comma_a_comma_between_digits_is_the_point() {
622 let text = " DECIMAL-POINT IS COMMA.\n 1,5 -,25 +3,0 ,75 T(1, 2) A,B 1.\n";
623 assert_eq!(numbers(text), ["1.5", "-.25", "+3.0", ".75", "1", "2", "1"]);
624 assert!(toks(text).contains(&w("B")));
625 assert!(toks(" DECIMAL-POINT IS COMMA.\n PIC ,99.").contains(&Tok::Pic(",99".into())));
626 assert!(lex(&source::read(" DECIMAL-POINT COMMA.\n MOVE 1.5").unwrap()).is_err());
627 }
628
629 #[test]
630 fn a_contained_program_keeps_the_decimal_comma_and_the_next_program_does_not() {
631 let text = concat!(
632 " PROGRAM-ID. A.\n DECIMAL-POINT IS COMMA.\n 1,5\n",
633 " PROGRAM-ID. B.\n 2,5\n END PROGRAM B.\n 3,5\n END PROGRAM A.\n",
634 " PROGRAM-ID. C.\n 4,5\n",
635 );
636 assert_eq!(numbers(text), ["1.5", "2.5", "3.5", "4", "5"]);
637 }
638}