1use crate::source::Source;
2use crate::{Error, Pos};
3
4#[derive(Clone, Debug, PartialEq, Eq)]
5pub enum Tok {
6 Word(String),
8 Number(String),
10 Alnum(String),
11 Hex(Vec<u8>),
12 National(String),
13 Pic(String),
14 Exec(String),
16 Period,
17 LParen,
18 RParen,
19 Colon,
20 Plus,
21 Minus,
22 Star,
23 Slash,
24 Power,
25 Eq,
26 Lt,
27 Gt,
28 Le,
29 Ge,
30}
31
32#[derive(Clone, Debug, PartialEq, Eq)]
33pub struct Token {
34 pub tok: Tok,
35 pub pos: Pos,
36 pub area_a: bool,
38}
39
40struct Lexer<'a> {
41 chars: Vec<char>,
42 positions: &'a [Pos],
43 at: usize,
44 tokens: Vec<Token>,
45}
46
47pub fn lex(source: &Source) -> Result<Vec<Token>, Error> {
48 let mut lx = Lexer { chars: source.text.chars().collect(), positions: &source.positions, at: 0, tokens: Vec::new() };
49 while lx.at < lx.chars.len() {
50 lx.next_token()?;
51 }
52 Ok(lx.tokens)
53}
54
55fn is_word_char(c: char) -> bool {
56 c.is_ascii_alphanumeric() || c == '-' || c == '_'
57}
58
59impl Lexer<'_> {
60 fn peek(&self, ahead: usize) -> Option<char> {
61 self.chars.get(self.at + ahead).copied()
62 }
63
64 fn pos(&self) -> Pos {
65 self.positions.get(self.at).copied().unwrap_or_default()
66 }
67
68 fn separator_follows(&self, ahead: usize) -> bool {
69 self.peek(ahead).is_none_or(|c| c == ' ' || c == '\n')
70 }
71
72 fn emit(&mut self, tok: Tok, pos: Pos) {
73 self.tokens.push(Token { tok, pos, area_a: (8..=11).contains(&pos.col) });
74 }
75
76 fn expecting_picture(&self) -> bool {
77 let words: Vec<&str> =
78 self.tokens.iter().rev().take(2).map(|t| if let Tok::Word(w) = &t.tok { w.as_str() } else { "" }).collect();
79 matches!(words.as_slice(), ["PIC" | "PICTURE", ..] | ["IS", "PIC" | "PICTURE"])
80 }
81
82 fn next_token(&mut self) -> Result<(), Error> {
83 let c = self.chars[self.at];
84 let pos = self.pos();
85 if c == ' ' || c == '\n' || c == ',' || c == ';' {
86 self.at += 1;
87 return Ok(());
88 }
89 if self.expecting_picture() {
90 return self.picture(pos);
91 }
92 let next = self.peek(1);
93 let quote_next = matches!(next, Some('\'' | '"'));
94 match c {
95 '\'' | '"' => {
96 let text = self.quoted(pos)?;
97 self.emit(Tok::Alnum(text), pos);
98 }
99 'X' | 'x' if quote_next => {
100 self.at += 1;
101 let text = self.quoted(pos)?;
102 let bytes = unhex(&text).ok_or_else(|| Error::at(pos, format!("X'{text}' is not an even number of hex digits")))?;
103 self.emit(Tok::Hex(bytes), pos);
104 }
105 'N' | 'n' if quote_next => {
106 self.at += 1;
107 let text = self.quoted(pos)?;
108 self.emit(Tok::National(text), pos);
109 }
110 '.' if self.separator_follows(1) => {
111 self.at += 1;
112 self.emit(Tok::Period, pos);
113 }
114 '+' | '-' if next.is_some_and(|n| n.is_ascii_digit() || (n == '.' && self.peek(2).is_some_and(|d| d.is_ascii_digit()))) => {
115 self.at += 1;
116 let digits = self.number_or_word(pos)?;
117 match digits {
118 Tok::Number(n) => self.emit(Tok::Number(format!("{c}{n}")), pos),
119 _ => return Err(Error::at(pos, "a sign must be followed by a number")),
120 }
121 }
122 _ if c.is_ascii_alphanumeric() || c == '.' => {
123 let tok = self.number_or_word(pos)?;
124 match tok {
125 Tok::Word(w) if w == "EXEC" || w == "EXECUTE" => {
126 let tok = self.exec_block(pos)?;
127 self.emit(tok, pos);
128 }
129 tok => self.emit(tok, pos),
130 }
131 }
132 _ => {
133 let (tok, len) = match (c, next) {
134 ('*', Some('*')) => (Tok::Power, 2),
135 ('<', Some('=')) => (Tok::Le, 2),
136 ('>', Some('=')) => (Tok::Ge, 2),
137 ('*', _) => (Tok::Star, 1),
138 ('/', _) => (Tok::Slash, 1),
139 ('+', _) => (Tok::Plus, 1),
140 ('-', _) => (Tok::Minus, 1),
141 ('=', _) => (Tok::Eq, 1),
142 ('<', _) => (Tok::Lt, 1),
143 ('>', _) => (Tok::Gt, 1),
144 ('(', _) => (Tok::LParen, 1),
145 (')', _) => (Tok::RParen, 1),
146 (':', _) => (Tok::Colon, 1),
147 _ => return Err(Error::at(pos, format!("unexpected character {c:?}"))),
148 };
149 self.at += len;
150 self.emit(tok, pos);
151 }
152 }
153 Ok(())
154 }
155
156 fn exec_block(&mut self, pos: Pos) -> Result<Tok, Error> {
158 let start = self.at;
159 let mut quote: Option<char> = None;
160 while let Some(c) = self.peek(0) {
161 match quote {
162 Some(q) if c == q => quote = None,
163 Some(_) => {}
164 None if c == '\'' || c == '"' => quote = Some(c),
165 None if c.eq_ignore_ascii_case(&'E') => {
166 let ahead: String = self.chars[self.at..].iter().take(8).collect();
167 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]));
168 if ahead.eq_ignore_ascii_case("END-EXEC") && boundary {
169 let text: String = self.chars[start..self.at].iter().collect();
170 self.at += 8;
171 return Ok(Tok::Exec(text.split_whitespace().collect::<Vec<_>>().join(" ")));
172 }
173 }
174 None => {}
175 }
176 self.at += 1;
177 }
178 Err(Error::at(pos, "EXEC with no END-EXEC"))
179 }
180
181 fn quoted(&mut self, pos: Pos) -> Result<String, Error> {
183 let quote = self.chars[self.at];
184 self.at += 1;
185 let mut text = String::new();
186 loop {
187 match self.peek(0) {
188 None | Some('\n') => return Err(Error::at(pos, "an unterminated literal")),
189 Some(c) if c == quote && self.peek(1) == Some(quote) => {
190 text.push(quote);
191 self.at += 2;
192 }
193 Some(c) if c == quote => {
194 self.at += 1;
195 return Ok(text);
196 }
197 Some(c) => {
198 text.push(c);
199 self.at += 1;
200 }
201 }
202 }
203 }
204
205 fn number_or_word(&mut self, pos: Pos) -> Result<Tok, Error> {
206 let start = self.at;
207 while self.peek(0).is_some_and(is_word_char) {
208 self.at += 1;
209 }
210 let run: String = self.chars[start..self.at].iter().collect();
211 let all_digits = run.chars().all(|c| c.is_ascii_digit());
212 if all_digits && self.peek(0) == Some('.') && self.peek(1).is_some_and(|c| c.is_ascii_digit()) {
213 self.at += 1;
214 let frac_start = self.at;
215 while self.peek(0).is_some_and(|c| c.is_ascii_digit()) {
216 self.at += 1;
217 }
218 let frac: String = self.chars[frac_start..self.at].iter().collect();
219 return Ok(Tok::Number(format!("{run}.{frac}")));
220 }
221 if run.is_empty() {
222 return Err(Error::at(pos, "unexpected '.'"));
223 }
224 Ok(if all_digits { Tok::Number(run) } else { Tok::Word(run.to_ascii_uppercase()) })
225 }
226
227 fn picture(&mut self, pos: Pos) -> Result<(), Error> {
228 let start = self.at;
229 while self.peek(0).is_some_and(|c| c != ' ' && c != '\n') {
230 self.at += 1;
231 }
232 let mut end = self.at;
233 while end > start && matches!(self.chars[end - 1], '.' | ',' | ';') {
234 end -= 1;
235 }
236 let text: String = self.chars[start..end].iter().collect();
237 if text.is_empty() {
238 return Err(Error::at(pos, "PICTURE with no character-string"));
239 }
240 self.at = end;
241 let text = text.to_ascii_uppercase();
242 let tok = if text == "IS" && self.tokens.last().is_some_and(|t| matches!(&t.tok, Tok::Word(w) if w == "PIC" || w == "PICTURE")) {
243 Tok::Word(text)
244 } else {
245 Tok::Pic(text)
246 };
247 self.emit(tok, pos);
248 Ok(())
249 }
250}
251
252fn unhex(text: &str) -> Option<Vec<u8>> {
253 if !text.len().is_multiple_of(2) || !text.is_ascii() {
254 return None;
255 }
256 (0..text.len()).step_by(2).map(|i| u8::from_str_radix(&text[i..i + 2], 16).ok()).collect()
257}
258
259#[cfg(test)]
260mod tests {
261 use super::*;
262 use crate::source;
263
264 fn toks(text: &str) -> Vec<Tok> {
265 lex(&source::read(text).unwrap()).unwrap().into_iter().map(|t| t.tok).collect()
266 }
267
268 fn w(s: &str) -> Tok {
269 Tok::Word(s.into())
270 }
271
272 #[test]
273 fn numbers_words_and_the_separator_period() {
274 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]);
275 assert_eq!(toks(" VALUE -12345."), [w("VALUE"), Tok::Number("-12345".into()), Tok::Period]);
276 assert_eq!(toks(" 100-MAIN."), [w("100-MAIN"), Tok::Period]);
277 }
278
279 #[test]
280 fn operators_need_spaces_and_signed_literals_do_not() {
281 assert_eq!(toks(" A - 1 ** 2"), [w("A"), Tok::Minus, Tok::Number("1".into()), Tok::Power, Tok::Number("2".into())]);
282 assert_eq!(toks(" >= <= ("), [Tok::Ge, Tok::Le, Tok::LParen]);
283 }
284
285 #[test]
286 fn literals() {
287 assert_eq!(toks(" 'IT''S' X'F1C1' N'AB'"), [Tok::Alnum("IT'S".into()), Tok::Hex(vec![0xF1, 0xC1]), Tok::National("AB".into())]);
288 }
289
290 #[test]
291 fn a_picture_is_one_token_and_keeps_its_own_periods() {
292 assert_eq!(toks(" PIC S9(3)V99 COMP-3."), [w("PIC"), Tok::Pic("S9(3)V99".into()), w("COMP-3"), Tok::Period]);
293 assert_eq!(toks(" PICTURE IS ZZ,ZZ9.99."), [w("PICTURE"), w("IS"), Tok::Pic("ZZ,ZZ9.99".into()), Tok::Period]);
294 }
295
296 #[test]
297 fn commas_between_operands_are_separators() {
298 assert_eq!(toks(" F(A, 1)"), [w("F"), Tok::LParen, w("A"), Tok::Number("1".into()), Tok::RParen]);
299 assert_eq!(toks(" T(I,J)"), [w("T"), Tok::LParen, w("I"), w("J"), Tok::RParen]);
300 }
301
302 #[test]
303 fn an_exec_block_is_one_token() {
304 assert_eq!(
305 toks(" EXEC SQL SELECT A.B INTO :X FROM T WHERE C = 'END-EXEC'\n END-EXEC."),
306 [Tok::Exec("SQL SELECT A.B INTO :X FROM T WHERE C = 'END-EXEC'".into()), Tok::Period]
307 );
308 }
309
310 #[test]
311 fn area_a_is_marked() {
312 let t = lex(&source::read(" PARA.\n MOVE").unwrap()).unwrap();
313 assert!(t[0].area_a);
314 assert!(!t[2].area_a);
315 }
316}