Skip to main content

huff_neo_lexer/
lib.rs

1mod context_stack;
2
3use context_stack::ContextStack;
4use huff_neo_utils::file::full_file_source::FullFileSource;
5use huff_neo_utils::lexer_context::Context;
6use huff_neo_utils::prelude::*;
7use lazy_static::lazy_static;
8use regex::Regex;
9use std::collections::HashMap;
10use std::{iter::Peekable, str::Chars};
11use tracing::{debug, error};
12
13lazy_static! {
14    static ref TOKEN: HashMap<String, TokenKind> = HashMap::from_iter(vec![
15        (TokenKind::Macro.to_string(), TokenKind::Macro),
16        (TokenKind::Fn.to_string(), TokenKind::Fn),
17        (TokenKind::Test.to_string(), TokenKind::Test),
18        (TokenKind::Function.to_string(), TokenKind::Function),
19        (TokenKind::Constant.to_string(), TokenKind::Constant),
20        (TokenKind::Error.to_string(), TokenKind::Error),
21        (TokenKind::Takes.to_string(), TokenKind::Takes),
22        (TokenKind::Returns.to_string(), TokenKind::Returns),
23        (TokenKind::Event.to_string(), TokenKind::Event),
24        (TokenKind::NonPayable.to_string(), TokenKind::NonPayable),
25        (TokenKind::Payable.to_string(), TokenKind::Payable),
26        (TokenKind::Indexed.to_string(), TokenKind::Indexed),
27        (TokenKind::View.to_string(), TokenKind::View),
28        (TokenKind::Pure.to_string(), TokenKind::Pure),
29        // First check for packed jump table
30        (TokenKind::JumpTablePacked.to_string(), TokenKind::JumpTablePacked),
31        // Match with jump table if not
32        (TokenKind::JumpTable.to_string(), TokenKind::JumpTable),
33        (TokenKind::CodeTable.to_string(), TokenKind::CodeTable),
34    ]);
35}
36
37// For constants only max 32 Bytes is allowed for hex string starting with 0x. 2 + 64 = 66 characters
38const MAX_HEX_LITERAL_LENGTH: usize = 66;
39
40/// ## Lexer
41///
42/// The lexer encapsulated in a struct.
43pub struct Lexer<'a> {
44    /// The source code as peekable chars.
45    /// WARN: SHOULD NEVER BE MODIFIED!
46    pub chars: Peekable<Chars<'a>>,
47    /// The byte offset of the last consumed character.
48    position: usize,
49    /// The byte offset after the last consumed character (position of next char).
50    byte_offset: usize,
51    /// The previous lexed Token.
52    /// NOTE: Cannot be a whitespace.
53    pub lookback: Option<Token>,
54    /// Bool indicating if we have reached EOF
55    pub eof: bool,
56    /// Current context stack.
57    context_stack: ContextStack,
58    /// Open square brackets inside the current decorator, so `[CONST]` arguments nest
59    decorator_depth: usize,
60    /// The raw source code.
61    pub source: FullFileSource<'a>,
62}
63
64pub type TokenResult = Result<Token, LexicalError>;
65
66impl<'a> Lexer<'a> {
67    pub fn new(source: FullFileSource<'a>) -> Self {
68        Lexer {
69            chars: source.source.chars().peekable(),
70            position: 0,
71            byte_offset: 0,
72            lookback: None,
73            eof: false,
74            context_stack: ContextStack::new(),
75            decorator_depth: 0,
76            source,
77        }
78    }
79
80    /// Consumes the next character and updates byte position tracking.
81    /// After calling, `position` holds the byte offset of the consumed char,
82    /// and `byte_offset` holds the byte offset after it.
83    pub fn consume(&mut self) -> Option<char> {
84        let c = self.chars.next()?;
85        self.position = self.byte_offset;
86        self.byte_offset += c.len_utf8();
87        Some(c)
88    }
89
90    /// Try to peek at the next character from the source
91    pub fn peek(&mut self) -> Option<char> {
92        self.chars.peek().copied()
93    }
94
95    fn next_token(&mut self) -> TokenResult {
96        if let Some(ch) = self.consume() {
97            let token = match ch {
98                '/' => {
99                    let mut comment_string = String::new();
100                    let start = self.position;
101                    comment_string.push(ch);
102                    if let Some(ch2) = self.peek() {
103                        match ch2 {
104                            '/' => {
105                                // Consume until newline
106                                comment_string.push(ch2);
107                                let (comment_string, start, end) = self.eat_while(Some(ch), |c| c != '\n');
108                                Ok(TokenKind::Comment(comment_string).into_token_with_span(self.source.relative_span_by_pos(start, end)))
109                            }
110                            '*' => {
111                                // ref: https://github.com/rust-lang/rust/blob/900c3540378c8422b8087ffa3db60fa6c8abfcad/compiler/rustc_lexer/src/lib.rs#L474
112                                let c = self.consume();
113                                comment_string.push(c.unwrap());
114                                let mut depth = 1usize;
115                                while let Some(c) = self.consume() {
116                                    match c {
117                                        '/' if self.peek() == Some('*') => {
118                                            comment_string.push(c);
119                                            let c2 = self.consume();
120                                            comment_string.push(c2.unwrap());
121                                            depth += 1;
122                                        }
123                                        '*' if self.peek() == Some('/') => {
124                                            comment_string.push(c);
125                                            let c2 = self.consume();
126                                            comment_string.push(c2.unwrap());
127                                            depth -= 1;
128                                            if depth == 0 {
129                                                // This block comment is closed, so for a
130                                                // construction like "/* */ */"
131                                                // there will be a successfully parsed block comment
132                                                // "/* */"
133                                                // and " */" will be processed separately.
134                                                break;
135                                            }
136                                        }
137                                        _ => {
138                                            comment_string.push(c);
139                                        }
140                                    }
141                                }
142
143                                Ok(TokenKind::Comment(comment_string)
144                                    .into_token_with_span(self.source.relative_span_by_pos(start, self.byte_offset)))
145                            }
146                            _ => self.single_char_token(TokenKind::Div),
147                        }
148                    } else {
149                        self.single_char_token(TokenKind::Div)
150                    }
151                }
152
153                // # keywords
154                '#' => {
155                    let (word, start, end) = self.eat_while(Some(ch), |ch| ch.is_ascii_alphabetic());
156
157                    let mut found_kind: Option<TokenKind> = None;
158
159                    let keys = [TokenKind::Define, TokenKind::Include];
160                    for kind in keys.into_iter() {
161                        let key = kind.to_string();
162                        if key == word {
163                            found_kind = Some(kind);
164                            break;
165                        }
166                    }
167
168                    if let Some(kind) = found_kind {
169                        Ok(kind.into_token_with_span(self.source.relative_span_by_pos(start, end)))
170                    } else if matches!(self.context_stack.top(), Context::Global | Context::Constant | Context::Abi)
171                        && self.peek().unwrap() == '['
172                    {
173                        // Constant and ABI definitions have no terminal token, so a decorator ends them
174                        if self.context_stack.top() != &Context::Global {
175                            debug!(target: "lexer", "POP CONTEXT {:?}", self.context_stack.top());
176                            self.context_stack.pop(1).map_err(|_| {
177                                LexicalError::new(LexicalErrorKind::StackUnderflow, self.source.relative_span_by_pos(start, end))
178                            })?;
179                        }
180                        debug!(target: "lexer", "PUSH CONTEXT Decorator");
181                        self.context_stack.push(Context::Decorator);
182                        self.decorator_depth = 0;
183                        // Return Pound token for just the '#' character
184                        Ok(TokenKind::Pound.into_token_with_span(self.source.relative_span_by_pos(start, start + 1)))
185                    } else {
186                        // Otherwise we don't support # prefixed identifiers
187                        error!(target: "lexer", "INVALID '#' CHARACTER USAGE in context {:?}", self.context_stack.top());
188                        return Err(LexicalError::new(
189                            LexicalErrorKind::InvalidCharacter('#'),
190                            self.source.relative_span_by_pos(start, end),
191                        ));
192                    }
193                }
194                // Alphabetical characters
195                ch if ch.is_alphabetic() || ch.eq(&'_') => {
196                    let (word, start, mut end) = self.eat_while(Some(ch), |c| c.is_alphanumeric() || c == '_');
197
198                    let mut found_kind: Option<TokenKind> = None;
199                    // Decorator names and arguments are never keywords
200                    if !matches!(self.context_stack.top(), &Context::MacroBody | &Context::Decorator)
201                        && let Some(kind) = TOKEN.get(&word)
202                    {
203                        debug!(target: "lexer", "FOUND KEYWORD OUTSIDE OF MacroBody '{:?}'", kind);
204                        found_kind = Some(kind.clone());
205                    }
206
207                    // Check to see if the found kind is, in fact, a keyword and not the name of
208                    // a function. If it is, set `found_kind` to `None` so that it is set to a
209                    // `TokenKind::Ident` in the following control flow.
210                    if !self.check_keyword_rules(&found_kind) {
211                        debug!(target: "lexer", "RESET KEYWORD TO NONE '{:?}'", found_kind);
212                        found_kind = None;
213                    }
214
215                    // Set the context based on the found token kind
216                    if let Some(kind) = &found_kind {
217                        let mut new_context = None;
218                        match kind {
219                            TokenKind::Macro | TokenKind::Fn | TokenKind::Test => {
220                                debug!(target: "lexer", "PUSH CONTEXT MacroDefinition");
221                                new_context = Some(Context::MacroDefinition);
222                            }
223                            TokenKind::Function | TokenKind::Event | TokenKind::Error => {
224                                debug!(target: "lexer", "PUSH CONTEXT Abi");
225                                new_context = Some(Context::Abi);
226                            }
227                            TokenKind::Constant => {
228                                debug!(target: "lexer", "PUSH CONTEXT Constant");
229                                new_context = Some(Context::Constant);
230                            }
231                            TokenKind::CodeTable => {
232                                debug!(target: "lexer", "PUSH CONTEXT CodeTableBody");
233                                new_context = Some(Context::CodeTableBody);
234                            }
235                            _ => (),
236                        }
237
238                        // For some contexts we have no terminal token, so we need to pop the top item
239                        if new_context.is_some() && self.context_stack.top() != &Context::Global {
240                            debug!(target: "lexer", "POP CONTEXT {:?}", self.context_stack.top());
241                            self.context_stack.pop(1).map_err(|_| {
242                                LexicalError::new(LexicalErrorKind::StackUnderflow, self.source.relative_span_by_pos(start, end))
243                            })?;
244                        }
245                        // Verify that the context is correct
246                        if new_context.is_some() && self.context_stack.top() != &Context::Global {
247                            return Err(LexicalError::new(
248                                LexicalErrorKind::UnexpectedContext(self.context_stack.top().clone()),
249                                self.source.relative_span_by_pos(start, end),
250                            ));
251                        }
252                        // Push the new context
253                        if let Some(new_context) = new_context {
254                            self.context_stack.push(new_context);
255                        }
256                    }
257
258                    // Check for free storage pointer builtin
259                    if word == "FREE_STORAGE_POINTER" {
260                        // Consume the parenthesis following the FREE_STORAGE_POINTER
261                        // Note: This will consume `FREE_STORAGE_POINTER)` or
262                        // `FREE_STORAGE_POINTER(` as well
263                        if let Some('(') = self.peek() {
264                            self.consume();
265                        }
266                        if let Some(')') = self.peek() {
267                            self.consume();
268                        }
269                        end += 2;
270                        debug!(target: "lexer", "FOUND FREE_STORAGE_POINTER");
271                        found_kind = Some(TokenKind::FreeStoragePointer);
272                    }
273
274                    // Check for __NOOP builtin constant
275                    if word == "__NOOP"
276                        && matches!(
277                            self.context_stack.top(),
278                            &Context::MacroBody | &Context::ForLoopBody | &Context::Constant | &Context::MacroArgs
279                        )
280                    {
281                        debug!(target: "lexer", "FOUND __NOOP");
282                        found_kind = Some(TokenKind::Noop);
283                    }
284
285                    if let Some(':') = self.peek() {
286                        debug!(target: "lexer", "FOUND LABEL '{:?}'", word);
287                        found_kind = Some(TokenKind::Label(word.clone()));
288                    }
289
290                    // A word between `<` and `>` refers to a macro argument, so a parameter may be
291                    // named like an opcode or a keyword, such as `<pc>`
292                    let is_arg_reference =
293                        self.checked_lookback(TokenKind::LeftAngle) && self.chars.clone().find(|c| !c.is_whitespace()) == Some('>');
294
295                    // Syntax sugar: true evaluates to 0x01, false evaluates to 0x00
296                    if !is_arg_reference
297                        && matches!(self.context_stack.top(), &Context::MacroBody | &Context::ForLoopBody | &Context::Constant | &Context::MacroArgs)
298                        && !self.checked_lookback(TokenKind::Constant) // allow to use `true` and `false` as identifiers
299                        && matches!(word.as_str(), "true" | "false")
300                    {
301                        debug!(target: "lexer", "FOUND BOOL '{:?}'", word);
302                        found_kind = Some(TokenKind::HexLiteral(if word.as_str() == "true" { "01".to_string() } else { "00".to_string() }));
303                        self.eat_while(None, |c| c.is_alphanumeric());
304                    }
305
306                    // Keywords and opcodes in MacroBody or ForLoopBody context
307                    if matches!(self.context_stack.top(), &Context::MacroBody | &Context::ForLoopBody)
308                        && found_kind.is_none()
309                        && !is_arg_reference
310                    {
311                        found_kind = match word.as_str() {
312                            "for" => Some(TokenKind::For),
313                            "in" => Some(TokenKind::In),
314                            "step" => Some(TokenKind::Step),
315                            "if" => Some(TokenKind::If),
316                            "else" => Some(TokenKind::Else),
317                            _ => OPCODES_MAP.get(&word).map(|o| TokenKind::Opcode(o.to_owned())),
318                        };
319                        if let Some(kind) = &found_kind {
320                            debug!(target: "lexer", "FOUND {:?}", kind);
321                        }
322                    }
323
324                    if self.context_stack.top() == &Context::AbiArgs {
325                        self.eat_abi_args(ch, &word, start, &mut end, &mut found_kind);
326                    }
327
328                    let kind = if let Some(kind) = found_kind {
329                        kind
330                    } else if matches!(
331                        self.context_stack.top(),
332                        &Context::MacroBody
333                            | &Context::BuiltinFunction
334                            | &Context::CodeTableBody
335                            | &Context::Constant
336                            | &Context::MacroArgs
337                            | &Context::ForLoopBody
338                            | &Context::Decorator
339                    ) && BuiltinFunctionKind::try_from(&word).is_ok()
340                    {
341                        TokenKind::BuiltinFunction(word)
342                    } else {
343                        TokenKind::Ident(word)
344                    };
345
346                    Ok(kind.into_token_with_span(self.source.relative_span_by_pos(start, end)))
347                }
348                // If it's the start of a hex literal
349                ch if ch == '0' && self.peek().unwrap() == 'x' => self.eat_hex_digit(ch),
350                '=' => {
351                    // Check if next char is also '=' for EqualEqual (==)
352                    if self.peek() == Some('=') {
353                        let start = self.position;
354                        self.consume(); // consume second '='
355                        Ok(TokenKind::EqualEqual.into_token_with_span(self.source.relative_span_by_pos(start, self.byte_offset)))
356                    } else {
357                        self.single_char_token(TokenKind::Assign)
358                    }
359                }
360                '!' => {
361                    // Check if next char is '=' for NotEqual (!=)
362                    if self.peek() == Some('=') {
363                        let start = self.position;
364                        self.consume(); // consume '='
365                        Ok(TokenKind::NotEqual.into_token_with_span(self.source.relative_span_by_pos(start, self.byte_offset)))
366                    } else {
367                        // '!' by itself is logical NOT
368                        self.single_char_token(TokenKind::Not)
369                    }
370                }
371                '(' => {
372                    match self.context_stack.top() {
373                        // A tuple type nests a parenthesis inside the arguments
374                        Context::Abi | Context::AbiArgs => {
375                            debug!(target: "lexer", "PUSH CONTEXT AbiArgs");
376                            self.context_stack.push(Context::AbiArgs)
377                        }
378                        Context::Decorator => {
379                            if let Some(TokenKind::BuiltinFunction(_)) = self.lookback.as_ref().map(|t| &t.kind) {
380                                debug!(target: "lexer", "PUSH CONTEXT BuiltinFunction");
381                                self.context_stack.push(Context::BuiltinFunction)
382                            }
383                        }
384                        Context::MacroBody | Context::ForLoopBody => match self.lookback.as_ref().unwrap().kind {
385                            TokenKind::BuiltinFunction(_) => {
386                                debug!(target: "lexer", "PUSH CONTEXT BuiltinFunction");
387                                self.context_stack.push(Context::BuiltinFunction)
388                            }
389                            TokenKind::For => {
390                                // Don't push MacroArgs context for `for(...)` - keep current context
391                                // so that loop keywords like `in` and `step` are recognized
392                                debug!(target: "lexer", "KEEP CONTEXT for 'for' loop");
393                            }
394                            _ => {
395                                debug!(target: "lexer", "PUSH CONTEXT MacroArgs");
396                                self.context_stack.push(Context::MacroArgs)
397                            }
398                        },
399                        _ => {}
400                    }
401                    self.single_char_token(TokenKind::OpenParen)
402                }
403                ')' => {
404                    if matches!(self.context_stack.top(), &Context::AbiArgs | &Context::BuiltinFunction | &Context::MacroArgs) {
405                        debug!(target: "lexer", "POP CONTEXT AbiArgs OR BuiltinFunction OR MacroArgs");
406                        self.context_stack.pop(1).map_err(|_| {
407                            LexicalError::new(
408                                LexicalErrorKind::StackUnderflow,
409                                self.source.relative_span_by_pos(self.position, self.byte_offset),
410                            )
411                        })?
412                    }
413                    self.single_char_token(TokenKind::CloseParen)
414                }
415                '[' => {
416                    if self.context_stack.top() == &Context::Decorator {
417                        self.decorator_depth += 1;
418                    }
419                    self.single_char_token(TokenKind::OpenBracket)
420                }
421                ']' => {
422                    if self.context_stack.top() == &Context::Decorator {
423                        self.decorator_depth = self.decorator_depth.saturating_sub(1);
424                        if self.decorator_depth == 0 {
425                            debug!(target: "lexer", "POP CONTEXT Decorator");
426                            self.context_stack.pop(1).map_err(|_| {
427                                LexicalError::new(
428                                    LexicalErrorKind::StackUnderflow,
429                                    self.source.relative_span_by_pos(self.position, self.byte_offset),
430                                )
431                            })?;
432                        }
433                    }
434                    self.single_char_token(TokenKind::CloseBracket)
435                }
436                '{' => {
437                    // Push ForLoopBody context in MacroBody or ForLoopBody context for nested braces
438                    if matches!(self.context_stack.top(), &Context::MacroBody | &Context::ForLoopBody) {
439                        debug!(target: "lexer", "PUSH CONTEXT ForLoopBody");
440                        self.context_stack.push(Context::ForLoopBody);
441                    }
442
443                    if self.context_stack.top() == &Context::MacroDefinition {
444                        // New stack: Global -> MacroBody
445                        debug!(target: "lexer", "REPLACE CONTEXT MacroDefinition to MacroBody");
446                        self.context_stack.replace(Context::MacroBody);
447                    }
448                    self.single_char_token(TokenKind::OpenBrace)
449                }
450                '}' => {
451                    if matches!(self.context_stack.top(), &Context::ForLoopBody | &Context::MacroBody | &Context::CodeTableBody) {
452                        debug!(target: "lexer", "POP CONTEXT {:?}", self.context_stack.top());
453                        self.context_stack.pop(1).map_err(|_| {
454                            LexicalError::new(
455                                LexicalErrorKind::StackUnderflow,
456                                self.source.relative_span_by_pos(self.position, self.byte_offset),
457                            )
458                        })?;
459                    }
460                    self.single_char_token(TokenKind::CloseBrace)
461                }
462                '+' => self.single_char_token(TokenKind::Add),
463                '-' => self.single_char_token(TokenKind::Sub),
464                '*' => self.single_char_token(TokenKind::Mul),
465                '%' => self.single_char_token(TokenKind::Mod),
466                '<' => {
467                    // Check if next char is '=' for LessEqual (<=)
468                    if self.peek() == Some('=') {
469                        let start = self.position;
470                        self.consume(); // consume '='
471                        Ok(TokenKind::LessEqual.into_token_with_span(self.source.relative_span_by_pos(start, self.byte_offset)))
472                    } else {
473                        self.single_char_token(TokenKind::LeftAngle)
474                    }
475                }
476                '>' => {
477                    // Check if next char is '=' for GreaterEqual (>=)
478                    if self.peek() == Some('=') {
479                        let start = self.position;
480                        self.consume(); // consume '='
481                        Ok(TokenKind::GreaterEqual.into_token_with_span(self.source.relative_span_by_pos(start, self.byte_offset)))
482                    } else {
483                        self.single_char_token(TokenKind::RightAngle)
484                    }
485                }
486                // NOTE: TokenKind::Div is lexed further up since it overlaps with comment
487                ':' => self.single_char_token(TokenKind::Colon),
488                '.' => {
489                    // Check if next char is also '.' for DoubleDot (..)
490                    if self.peek() == Some('.') {
491                        let start = self.position;
492                        self.consume(); // consume the second '.'
493                        Ok(TokenKind::DoubleDot.into_token_with_span(self.source.relative_span_by_pos(start, self.byte_offset)))
494                    } else {
495                        // Single dot is not supported
496                        error!(target: "lexer", "UNSUPPORTED TOKEN '.'");
497                        return Err(LexicalError::new(
498                            LexicalErrorKind::InvalidCharacter('.'),
499                            self.source.relative_span_by_pos(self.position, self.byte_offset),
500                        ));
501                    }
502                }
503                // identifiers
504                ',' => self.single_char_token(TokenKind::Comma),
505                '0'..='9' => self.eat_digit(ch),
506                // Lexes Spaces and Newlines as Whitespace
507                ch if ch.is_ascii_whitespace() => {
508                    let (_, start, end) = self.eat_while(Some(ch), |c| c.is_whitespace());
509                    Ok(TokenKind::Whitespace.into_token_with_span(self.source.relative_span_by_pos(start, end)))
510                }
511                // String literals. String literals can also be wrapped by single quotes
512                '"' | '\'' => Ok(self.eat_string_literal()),
513                ch => {
514                    error!(target: "lexer", "UNSUPPORTED TOKEN '{}'", ch);
515                    return Err(LexicalError::new(
516                        LexicalErrorKind::InvalidCharacter(ch),
517                        self.source.relative_span_by_pos(self.position, self.byte_offset),
518                    ));
519                }
520            }?;
521
522            if token.kind != TokenKind::Whitespace {
523                self.lookback = Some(token.clone());
524            }
525
526            Ok(token)
527        } else {
528            self.eof = true;
529            let eof_pos = self.source.source.len();
530            Ok(Token { kind: TokenKind::Eof, span: self.source.relative_span_by_pos(eof_pos, eof_pos) })
531        }
532    }
533
534    fn eat_abi_args(&mut self, ch: char, word: &String, start: usize, end: &mut usize, found_kind: &mut Option<TokenKind>) {
535        let curr_char = self.peek().unwrap();
536        if !['(', ')'].contains(&curr_char) {
537            let (partial_raw_type, _, abi_args_end) = self.eat_while(Some(ch), |c| c.is_alphanumeric() || c == '[' || c == ']');
538            let raw_type = word.clone() + &partial_raw_type[1..];
539
540            if raw_type == TokenKind::Calldata.to_string() {
541                debug!(target: "lexer", "FOUND CALLED DATA '{:?}'", raw_type);
542                *found_kind = Some(TokenKind::Calldata);
543            } else if raw_type == TokenKind::Memory.to_string() {
544                debug!(target: "lexer", "FOUND MEMORY '{:?}'", raw_type);
545                *found_kind = Some(TokenKind::Memory);
546            } else if raw_type == TokenKind::Storage.to_string() {
547                debug!(target: "lexer", "FOUND STORAGE '{:?}'", raw_type);
548                *found_kind = Some(TokenKind::Storage);
549            } else if EVM_TYPE_ARRAY_REGEX.is_match(&raw_type) {
550                // split to get array size and type
551                // TODO: support multi-dimensional arrays
552                let words: Vec<String> = Regex::new(r"\[").unwrap().split(&raw_type).map(|x| x.replace(']', "")).collect();
553                let mut size_vec: Vec<usize> = Vec::new();
554                // go over all array sizes
555                let sizes = words.get(1..words.len()).unwrap();
556                for size in sizes.iter() {
557                    match size.is_empty() {
558                        true => size_vec.push(0),
559                        false => {
560                            let arr_size: usize = size
561                                .parse::<usize>()
562                                .map_err(|_| {
563                                    let err = LexicalError {
564                                        kind: LexicalErrorKind::InvalidArraySize(words[1].clone()),
565                                        span: self.source.relative_span_by_pos(start, *end),
566                                    };
567                                    error!(target: "lexer", "{}", format!("{err:?}"));
568                                    err
569                                })
570                                .unwrap();
571                            size_vec.push(arr_size);
572                        }
573                    }
574                }
575                let primitive = PrimitiveEVMType::try_from(&words[0]);
576                if let Ok(primitive) = primitive {
577                    debug!(target: "lexer", "FOUND ARRAY '{:?}'", raw_type);
578                    *found_kind = Some(TokenKind::ArrayType(primitive, size_vec));
579                } else {
580                    let err = LexicalError {
581                        kind: LexicalErrorKind::InvalidPrimitiveType(words[0].clone()),
582                        span: self.source.relative_span_by_pos(start, *end),
583                    };
584                    error!(target: "lexer", "{}", format!("{err:?}"));
585                }
586            } else {
587                // We don't want to consider any argument names or the "indexed"
588                // keyword here.
589                let primitive = PrimitiveEVMType::try_from(word);
590                if let Ok(primitive) = primitive {
591                    debug!(target: "lexer", "FOUND PRIMITIVE '{:?}'", raw_type);
592                    *found_kind = Some(TokenKind::PrimitiveType(primitive));
593                }
594            }
595            *end = abi_args_end;
596        } else {
597            // We don't want to consider any argument names or the "indexed"
598            // keyword here.
599            let primitive = PrimitiveEVMType::try_from(word);
600            if let Ok(primitive) = primitive {
601                debug!(target: "lexer", "FOUND PRIMITIVE '{:?}'", word);
602                *found_kind = Some(TokenKind::PrimitiveType(primitive));
603            }
604        }
605    }
606
607    fn single_char_token(&self, token_kind: TokenKind) -> TokenResult {
608        Ok(token_kind.into_token_with_span(self.source.relative_span_by_pos(self.position, self.byte_offset)))
609    }
610
611    /// Keeps consuming tokens as long as the predicate is satisfied
612    fn eat_while<F: Fn(char) -> bool>(&mut self, initial_char: Option<char>, predicate: F) -> (String, usize, usize) {
613        let start = self.position;
614
615        // This function is only called when we want to continue consuming a character of the same
616        // type. For example, we see a digit, and we want to consume the whole integer.
617        // Therefore, the current character which triggered this function will need to be appended.
618        let mut word = String::new();
619        if let Some(init_char) = initial_char {
620            word.push(init_char)
621        }
622
623        // Keep checking that we are not at the EOF
624        while let Some(peek_char) = self.peek() {
625            // Then check for the predicate, if predicate matches append char and increment the
626            // cursor If not, return word. The next character will be analyzed on the
627            // next iteration of next_token, Which will increment the cursor
628            if !predicate(peek_char) {
629                return (word, start, self.byte_offset);
630            }
631            word.push(peek_char);
632
633            // If we arrive at this point, then the char has been added to the word and we should
634            // increment the cursor
635            self.consume();
636        }
637
638        (word, start, self.byte_offset)
639    }
640
641    fn eat_digit(&mut self, initial_char: char) -> TokenResult {
642        let (integer_str, start, end) = self.eat_while(Some(initial_char), |ch| ch.is_ascii_digit());
643
644        let integer = integer_str.parse().unwrap();
645        let integer_token = TokenKind::Integer(integer);
646
647        Ok(Token { kind: integer_token, span: self.source.relative_span_by_pos(start, end) })
648    }
649
650    fn eat_hex_digit(&mut self, initial_char: char) -> TokenResult {
651        let (integer_str, start, end) = self.eat_while(Some(initial_char), |ch| ch.is_ascii_hexdigit() | (ch == 'x'));
652        if integer_str.matches('x').count() != 1 {
653            return Err(LexicalError::new(
654                LexicalErrorKind::InvalidHexLiteral(integer_str.clone()),
655                self.source.relative_span_by_pos(start, end),
656            ));
657        }
658
659        // Always produce Bytes token for hex literals to ensure consistent handling
660        // Validation of length is context-dependent
661        if self.context_stack.top() == &Context::Constant && integer_str.len() > MAX_HEX_LITERAL_LENGTH {
662            return Err(LexicalError::new(
663                LexicalErrorKind::HexLiteralTooLong(integer_str.clone()),
664                self.source.relative_span_by_pos(start, end),
665            ));
666        }
667
668        // Preserve the original hex string without normalization to even length
669        // This allows padding functions and other consumers to determine the user's intent
670        let hex_string = integer_str[2..].to_lowercase();
671
672        // Hex literals must have at least one digit after "0x"
673        if hex_string.is_empty() {
674            return Err(LexicalError::new(
675                LexicalErrorKind::InvalidHexLiteral(integer_str.clone()),
676                self.source.relative_span_by_pos(start, end),
677            ));
678        }
679
680        let kind = TokenKind::HexLiteral(hex_string);
681
682        Ok(Token { kind, span: self.source.relative_span_by_pos(start, end) })
683    }
684
685    /// Skips white space. They are not significant in the source language
686    fn eat_whitespace(&mut self) -> (String, usize, usize) {
687        self.eat_while(None, |ch| ch.is_whitespace())
688    }
689
690    fn eat_string_literal(&mut self) -> Token {
691        let (str_literal, start_span, end_span) = self.eat_while(None, |ch| ch != '"' && ch != '\'');
692        let str_literal_token = TokenKind::Str(str_literal);
693        self.consume(); // Advance past the closing quote
694        str_literal_token.into_token_with_span(self.source.relative_span_by_pos(start_span, end_span + 1))
695    }
696
697    /// Checks the previous token kind against the input.
698    pub fn checked_lookback(&self, kind: TokenKind) -> bool {
699        self.lookback.as_ref().and_then(|t| if t.kind == kind { Some(true) } else { None }).is_some()
700    }
701
702    /// Check if a given keyword follows the keyword rules in the `source`. If not, it is a
703    /// `TokenKind::Ident`.
704    ///
705    /// Rules:
706    /// - The `macro`, `fn`, `test`, `function`, `constant`, `event`, `jumptable`,
707    ///   `jumptable__packed`, and `table` keywords must be preceded by a `#define` keyword.
708    /// - The `takes` keyword must be preceded by an assignment operator: `=`.
709    /// - The `nonpayable`, `payable`, `view`, and `pure` keywords must be preceeded by one of these
710    ///   keywords or a close paren.
711    /// - The `returns` keyword must be succeeded by an open parenthesis and must *not* be succeeded
712    ///   by a colon or preceded by the keyword `function`
713    pub fn check_keyword_rules(&mut self, found_kind: &Option<TokenKind>) -> bool {
714        match found_kind {
715            Some(TokenKind::Macro)
716            | Some(TokenKind::Fn)
717            | Some(TokenKind::Test)
718            | Some(TokenKind::Function)
719            | Some(TokenKind::Constant)
720            | Some(TokenKind::Error)
721            | Some(TokenKind::Event)
722            | Some(TokenKind::JumpTable)
723            | Some(TokenKind::JumpTablePacked)
724            | Some(TokenKind::CodeTable) => self.checked_lookback(TokenKind::Define),
725            Some(TokenKind::NonPayable) | Some(TokenKind::Payable) | Some(TokenKind::View) | Some(TokenKind::Pure) => {
726                let keys = [TokenKind::NonPayable, TokenKind::Payable, TokenKind::View, TokenKind::Pure, TokenKind::CloseParen];
727                for key in keys {
728                    if self.checked_lookback(key) {
729                        return true;
730                    }
731                }
732                false
733            }
734            Some(TokenKind::Takes) => self.checked_lookback(TokenKind::Assign),
735            Some(TokenKind::Returns) => {
736                self.eat_whitespace();
737                // Allow for loose and tight syntax (e.g. `returns   (0)`, `returns(0)`, ...)
738                self.peek().unwrap_or(')') == '(' && !self.checked_lookback(TokenKind::Function)
739            }
740            _ => true,
741        }
742    }
743
744    /// Lex all imports
745    /// Example import: `// #include "./Utils.huff"`
746    pub fn lex_imports(source: &str) -> Vec<String> {
747        let mut imports = vec![];
748        let mut peekable_source = source.chars().peekable();
749        let mut include_chars_iterator = "#include".chars().peekable();
750        while peekable_source.peek().is_some() {
751            while let Some(nc) = peekable_source.next() {
752                if nc.eq(&'/')
753                    && let Some(nnc) = peekable_source.peek()
754                {
755                    if nnc.eq(&'/') {
756                        // Iterate until newline
757                        while let Some(lc) = &peekable_source.next() {
758                            if lc.eq(&'\n') {
759                                break;
760                            }
761                        }
762                    } else if nnc.eq(&'*') {
763                        // Iterate until '*/'
764                        while let Some(lc) = peekable_source.next() {
765                            if lc.eq(&'*')
766                                && let Some(llc) = peekable_source.peek()
767                                && *llc == '/'
768                            {
769                                break;
770                            }
771                        }
772                    }
773                }
774                if include_chars_iterator.peek().is_none() {
775                    // Reset the include chars iterator
776                    include_chars_iterator = "#include".chars().peekable();
777
778                    // Skip over whitespace
779                    while peekable_source.peek().is_some() {
780                        if !peekable_source.peek().unwrap().is_whitespace() {
781                            break;
782                        } else {
783                            peekable_source.next();
784                        }
785                    }
786
787                    // Then we should have an import path between quotes
788                    #[allow(clippy::collapsible_match)]
789                    if let Some(char) = peekable_source.peek() {
790                        match char {
791                            '"' | '\'' => {
792                                peekable_source.next();
793                                let mut import = String::new();
794                                while peekable_source.peek().is_some() {
795                                    if let Some(c) = peekable_source.next() {
796                                        if matches!(c, '"' | '\'') {
797                                            imports.push(import);
798                                            break;
799                                        } else {
800                                            import.push(c);
801                                        }
802                                    }
803                                }
804                            }
805                            _ => { /* Ignore non-include tokens */ }
806                        }
807                    }
808                } else if nc.ne(&include_chars_iterator.next().unwrap()) {
809                    include_chars_iterator = "#include".chars().peekable();
810                    break;
811                }
812            }
813        }
814        imports
815    }
816}
817
818impl Iterator for Lexer<'_> {
819    type Item = TokenResult;
820
821    fn next(&mut self) -> Option<Self::Item> {
822        if self.eof { None } else { Some(self.next_token()) }
823    }
824}