Repository: huff-language/huff-rs (rust)

Issue:
Indicate where a parsing error occurred
Hi!

When a parsing error happens during compilation, the parser doesn't give any hint as to where the error was found. It just says what token was found and what kind of token was expected.

Ideally, it would be great to have the line number but if the parser would at least output where was the cursor when the parsing error happened, it would help debugging.



Pull request description:
feat: show cursor on parser error
fix #312

## Overview

When a parsing error happens, it can be very helpful to give a hint to the developer where the error happened. In a future PR, I would love to add the line number but for now, I think the cursor is a good quick win.

Before: 
```
Error: Unexpected Type: "#define" 
Expected one of: `view`, `pure`, `payable`, `nonpayable`.
```

After:
```
Error at token 27: Unexpected Type: "#define" 
Expected one of: `view`, `pure`, `payable`, `nonpayable`.
```


<!--
Thank you for creating a Pull Request!
Please provide a short description here and review the requirements below.
Bug fixes and new features should include tests.

Make sure to run these checks before opening a pr!
```sh
cargo check --all
cargo test --all --all-features
cargo +nightly fmt -- --check
cargo +nightly clippy --all --all-features -- -D warnings
```

Recommended method to auto format your code before pushing:
```sh
cargo +nightly fmt --all
```
-->


Patch:
diff --git a/huff_parser/src/lib.rs b/huff_parser/src/lib.rs
index 7d1c0d4..fc7589b 100644
--- a/huff_parser/src/lib.rs
+++ b/huff_parser/src/lib.rs
@@ -120,6 +120,7 @@ impl Parser {
                             kind: ParserErrorKind::InvalidDefinition(self.current_token.kind.clone()),
                             hint: Some("Definition must be one of: `function`, `event`, `constant`, `error`, `macro`, `fn`, or `test`.".to_string()),
                             spans: AstSpan(vec![self.current_token.span.clone()]),
+                            cursor: self.cursor,
                         });
                     }
                 };
@@ -133,6 +134,7 @@ impl Parser {
                         TokenKind::Include
                     )),
                     spans: AstSpan(self.spans.clone()),
+                    cursor: self.cursor,
                 });
             }
         }
@@ -158,6 +160,7 @@ impl Parser {
                     kind: ParserErrorKind::InvalidName(tok.clone()),
                     hint: Some(format!("Expected import string. Got: \"{tok}\"")),
                     spans: AstSpan(new_spans),
+                    cursor: self.cursor,
                 });
             }
         };
@@ -177,6 +180,7 @@ impl Parser {
                 kind: ParserErrorKind::UnexpectedType(self.current_token.kind.clone()),
                 hint: Some(format!("Expected: \"{kind}\"")),
                 spans: AstSpan(self.spans.clone()),
+                cursor: self.cursor,
             })
         }
     }
@@ -198,6 +202,7 @@ impl Parser {
                 kind: ParserErrorKind::DuplicateMacro(m.name.to_owned()),
                 hint: Some("MACRO names should be unique".to_string()),
                 spans: AstSpan(vec![m.span[2].clone()]),
+                cursor: self.cursor,
             })
         } else {
             Ok(())
@@ -257,6 +262,7 @@ impl Parser {
                     kind: ParserErrorKind::InvalidName(tok.clone()),
                     hint: Some(format!("Expected function name, found: \"{tok}\"")),
                     spans: AstSpan(self.spans.clone()),
+                    cursor: self.cursor,
                 });
             }
         };
@@ -276,6 +282,7 @@ impl Parser {
                         "Expected one of: `view`, `pure`, `payable`, `nonpayable`.".to_string(),
                     ),
                     spans: AstSpan(vec![self.current_token.span.clone()]),
+                    cursor: self.cursor,
                 })
             }
         };
@@ -319,6 +326,7 @@ impl Parser {
                     kind: ParserErrorKind::InvalidName(tok.clone()),
                     hint: Some(format!("Expected event name, found: \"{tok}\"")),
                     spans: AstSpan(self.spans.clone()),
+                    cursor: self.cursor,
                 });
             }
         };
@@ -350,6 +358,7 @@ impl Parser {
                     kind: ParserErrorKind::UnexpectedType(tok),
                     hint: Some("Expected constant name.".to_string()),
                     spans: AstSpan(self.spans.clone()),
+                    cursor: self.cursor,
                 });
             }
         };
@@ -375,6 +384,7 @@ impl Parser {
                             .to_string(),
                     ),
                     spans: AstSpan(vec![self.current_token.span.clone()]),
+                    cursor: self.cursor,
                 });
             }
         };
@@ -403,6 +413,7 @@ impl Parser {
                     kind: ParserErrorKind::UnexpectedType(tok),
                     hint: Some("Expected error name.".to_string()),
                     spans: AstSpan(self.spans.clone()),
+                    cursor: self.cursor,
                 });
             }
         };
@@ -450,6 +461,7 @@ impl Parser {
                                 ),
                                 hint: Some(format!("Expected string for decorator flag: {s}")),
                                 spans: AstSpan(vec![self.current_token.span.clone()]),
+                                cursor: self.cursor,
                             });
                         }
                     }
@@ -466,6 +478,7 @@ impl Parser {
                                 ),
                                 hint: Some(format!("Expected literal for decorator flag: {s}")),
                                 spans: AstSpan(vec![self.current_token.span.clone()]),
+                                cursor: self.cursor,
                             });
                         }
                     }
@@ -475,6 +488,7 @@ impl Parser {
                             kind: ParserErrorKind::InvalidDecoratorFlag(s.clone()),
                             hint: Some(format!("Unknown decorator flag: {s}")),
                             spans: AstSpan(self.spans.clone()),
+                            cursor: self.cursor,
                         });
                     }
                 }
@@ -493,6 +507,7 @@ impl Parser {
                     )),
                     hint: Some(String::from("Unknown decorator flag")),
                     spans: AstSpan(self.spans.clone()),
+                    cursor: self.cursor,
                 });
             }
         }
@@ -597,6 +612,7 @@ impl Parser {
                                             "Literal {hex_literal:?} contains too many bytes for opcode \"{o:?}\""
                                         )),
                                         spans: AstSpan(curr_spans),
+                                        cursor: self.cursor,
                                     });
                                 }
 
@@ -614,6 +630,7 @@ impl Parser {
                                         o, self.current_token.kind
                                     )),
                                     spans: AstSpan(vec![self.current_token.span.clone()]),
+                                    cursor: self.cursor,
                                 })
                             }
                         }
@@ -704,6 +721,7 @@ impl Parser {
                         kind: ParserErrorKind::InvalidTokenInMacroBody(kind),
                         hint: None,
                         spans: AstSpan(vec![self.current_token.span.clone()]),
+                        cursor: self.cursor,
                     });
                 }
             };
@@ -817,6 +835,7 @@ impl Parser {
                         kind: ParserErrorKind::InvalidTokenInLabelDefinition(kind),
                         hint: None,
                         spans: AstSpan(vec![self.current_token.span.clone()]),
+                        cursor: self.cursor,
                     });
                 }
             };
@@ -948,6 +967,7 @@ impl Parser {
                                         "Argument names cannot be EVM types: {arg_str}"
                                     )),
                                     spans: AstSpan(vec![self.current_token.span.clone()]),
+                                    cursor: self.cursor,
                                 });
                             }
                         }
@@ -958,6 +978,7 @@ impl Parser {
                                 ),
                                 hint: Some(format!("Argument names cannot be EVM types: {ty}")),
                                 spans: AstSpan(vec![self.current_token.span.clone()]),
+                                cursor: self.cursor,
                             })
                         }
                         _ => { /* continue, valid string */ }
@@ -981,6 +1002,7 @@ impl Parser {
                     kind: ParserErrorKind::InvalidArgs(self.current_token.kind.clone()),
                     hint: None,
                     spans: AstSpan(vec![self.current_token.span.clone()]),
+                    cursor: self.cursor,
                 });
             }
 
@@ -1004,6 +1026,7 @@ impl Parser {
                     kind: ParserErrorKind::InvalidSingleArg(self.current_token.kind.clone()),
                     hint: Some("Expected number representing stack item count.".to_string()),
                     spans: AstSpan(single_arg_span),
+                    cursor: self.cursor,
                 })
             }
         };
@@ -1059,6 +1082,7 @@ impl Parser {
                                 .to_string(),
                         ),
                         spans: AstSpan(new_spans),
+                        cursor: self.cursor,
                     });
                 }
             }
@@ -1147,7 +1171,8 @@ impl Parser {
                                     ),
                                     hint: Some("Expected valid hex bytecode.".to_string()),
                                     spans: AstSpan(new_spans),
-                                });
+                                    cursor: self.cursor,
+                                })
                             }
                         } else {
                             StatementType::LabelCall(ident_str.to_string())
@@ -1162,7 +1187,8 @@ impl Parser {
                         kind: ParserErrorKind::InvalidTableBodyToken(kind.clone()),
                         hint: Some("Expected an identifier string.".to_string()),
                         spans: AstSpan(new_spans),
-                    });
+                        cursor: self.cursor,
+                    })
                 }
             };
         }
@@ -1189,6 +1215,7 @@ impl Parser {
                     kind: ParserErrorKind::InvalidConstant(kind),
                     hint: None,
                     spans: AstSpan(new_spans),
+                    cursor: self.cursor,
                 })
             }
         }
@@ -1221,6 +1248,7 @@ impl Parser {
                     kind: ParserErrorKind::InvalidArgCallIdent(kind),
                     hint: None,
                     spans: AstSpan(new_spans),
+                    cursor: self.cursor,
                 })
             }
         }
@@ -1250,6 +1278,7 @@ impl Parser {
                 kind: ParserErrorKind::InvalidArgs(kind),
                 hint: None,
                 spans: AstSpan(vec![self.current_token.span.clone()]),
+                cursor: self.cursor,
             }),
         }
     }
@@ -1267,7 +1296,8 @@ impl Parser {
                         kind: ParserErrorKind::InvalidUint256(size),
                         hint: None,
                         spans: AstSpan(vec![self.current_token.span.clone()]),
-                    });
+                        cursor: self.cursor,
+                    })
                 }
                 Ok(self.match_kind(self.current_token.kind.clone())?)
             }
@@ -1277,7 +1307,8 @@ impl Parser {
                         kind: ParserErrorKind::InvalidBytes(size),
                         hint: None,
                         spans: AstSpan(vec![self.current_token.span.clone()]),
-                    });
+                        cursor: self.cursor,
+                    })
                 }
                 Ok(self.match_kind(self.current_token.kind.clone())?)
             }
@@ -1291,7 +1322,8 @@ impl Parser {
                         kind: ParserErrorKind::InvalidInt(size),
                         hint: None,
                         spans: AstSpan(vec![self.current_token.span.clone()]),
-                    });
+                        cursor: self.cursor,
+                    })
                 }
                 let curr_token_kind = self.current_token.kind.clone();
                 self.consume();
diff --git a/huff_utils/src/error.rs b/huff_utils/src/error.rs
index d0e90a1..f942fc7 100644
--- a/huff_utils/src/error.rs
+++ b/huff_utils/src/error.rs
@@ -16,6 +16,8 @@ pub struct ParserError {
     pub hint: Option<String>,
     /// A collection of spans the Parser Error crosses
     pub spans: AstSpan,
+    /// Line number where the error was seen
+    pub cursor: usize,
 }
 
 /// A Type of Parser Error
@@ -326,7 +328,8 @@ impl fmt::Display for CompilerError {
                 ParserErrorKind::InvalidPush(op) => {
                     write!(
                         f,
-                        "\nError: Invalid use of \"{:?}\" \n{}\n",
+                        "\nError at token {}: Invalid use of \"{:?}\" \n{}\n",
+                        pe.cursor,
                         op,
                         pe.spans.error(pe.hint.as_ref())
                     )
@@ -334,7 +337,8 @@ impl fmt::Display for CompilerError {
                 ParserErrorKind::UnexpectedType(ut) => {
                     write!(
                         f,
-                        "\nError: Unexpected Type: \"{}\" \n{}\n",
+                        "\nError at token {}: Unexpected Type: \"{}\" \n{}\n",
+                        pe.cursor,
                         ut,
                         pe.spans.error(pe.hint.as_ref())
                     )
@@ -342,7 +346,8 @@ impl fmt::Display for CompilerError {
                 ParserErrorKind::InvalidTypeAsArgumentName(ut) => {
                     write!(
                         f,
-                        "\nError: Unexpected Argument Name is an EVM Type: \"{}\" \n{}\n",
+                        "\nError at token {}: Unexpected Argument Name is an EVM Type: \"{}\" \n{}\n",
+                        pe.cursor,
                         ut,
                         pe.spans.error(pe.hint.as_ref())
                     )
@@ -350,7 +355,8 @@ impl fmt::Display for CompilerError {
                 ParserErrorKind::InvalidDefinition(k) => {
                     write!(
                         f,
-                        "\nError: Invalid Defintion \"{}\"\n{}\n",
+                        "\nError at token {}: Invalid Defintion \"{}\"\n{}\n",
+                        pe.cursor,
                         k,
                         pe.spans.error(pe.hint.as_ref())
                     )
@@ -358,7 +364,8 @@ impl fmt::Display for CompilerError {
                 ParserErrorKind::InvalidConstantValue(cv) => {
                     write!(
                         f,
-                        "\nError: Invalid Constant Value: \"{}\" \n{}\n",
+                        "\nError at token {}: Invalid Constant Value: \"{}\" \n{}\n",
+                        pe.cursor,
                         cv,
                         pe.spans.error(pe.hint.as_ref())
                     )
@@ -366,7 +373,8 @@ impl fmt::Display for CompilerError {
                 ParserErrorKind::InvalidTokenInMacroBody(tmb) => {
                     write!(
                         f,
-                        "\nError: Invalid Token In Macro Body: \"{}\" \n{}\n",
+                        "\nError at token {}: Invalid Token In Macro Body: \"{}\" \n{}\n",
+                        pe.cursor,
                         tmb,
                         pe.spans.error(pe.hint.as_ref())
                     )
@@ -374,7 +382,8 @@ impl fmt::Display for CompilerError {
                 ParserErrorKind::InvalidTokenInLabelDefinition(tlb) => {
                     write!(
                         f,
-                        "\nError: Invalid Token In Label Defintiion: \"{}\" \n{}\n",
+                        "\nError at token {}: Invalid Token In Label Defintiion: \"{}\" \n{}\n",
+                        pe.cursor,
                         tlb,
                         pe.spans.error(pe.hint.as_ref())
                     )
@@ -382,7 +391,8 @@ impl fmt::Display for CompilerError {
                 ParserErrorKind::InvalidSingleArg(sa) => {
                     write!(
                         f,
-                        "\nError: Invalid Argument: \"{}\" \n{}\n",
+                        "\nError at token {}: Invalid Argument: \"{}\" \n{}\n",
+                        pe.cursor,
                         sa,
                         pe.spans.error(pe.hint.as_ref())
                     )
@@ -390,7 +400,8 @@ impl fmt::Display for CompilerError {
                 ParserErrorKind::InvalidTableBodyToken(tbt) => {
                     write!(
                         f,
-                        "\nError: Invalid Token In Table Body: \"{}\" \n{}\n",
+                        "\nError at token {}: Invalid Token In Table Body: \"{}\" \n{}\n",
+                        pe.cursor,
                         tbt,
                         pe.spans.error(pe.hint.as_ref())
                     )
@@ -398,7 +409,8 @@ impl fmt::Display for CompilerError {
                 ParserErrorKind::InvalidConstant(constant) => {
                     write!(
                         f,
-                        "\nError: Invalid Constant: \"{}\" \n{}\n",
+                        "\nError at token {}: Invalid Constant: \"{}\" \n{}\n",
+                        pe.cursor,
                         constant,
                         pe.spans.error(pe.hint.as_ref())
                     )
@@ -406,7 +418,8 @@ impl fmt::Display for CompilerError {
                 ParserErrorKind::InvalidArgCallIdent(aci) => {
                     write!(
                         f,
-                        "\nError: Invalid Argument Call Identifier: \"{}\" \n{}\n",
+                        "\nError at token {}: Invalid Argument Call Identifier: \"{}\" \n{}\n",
+                        pe.cursor,
                         aci,
                         pe.spans.error(pe.hint.as_ref())
                     )
@@ -414,7 +427,8 @@ impl fmt::Display for CompilerError {
                 ParserErrorKind::InvalidName(name) => {
                     write!(
                         f,
-                        "\nError: Invalid Name: \"{}\" \n{}\n",
+                        "\nError at token {}: Invalid Name: \"{}\" \n{}\n",
+                        pe.cursor,
                         name,
                         pe.spans.error(pe.hint.as_ref())
                     )
@@ -422,7 +436,8 @@ impl fmt::Display for CompilerError {
                 ParserErrorKind::InvalidArgs(args) => {
                     write!(
                         f,
-                        "\nError: Invalid Argument Type: \"{}\" \n{}\n",
+                        "\nError at token {}: Invalid Argument Type: \"{}\" \n{}\n",
+                        pe.cursor,
                         args,
                         pe.spans.error(pe.hint.as_ref())
                     )
@@ -430,7 +445,8 @@ impl fmt::Display for CompilerError {
                 ParserErrorKind::InvalidUint256(v) => {
                     write!(
                         f,
-                        "\nError: Invalid Uint256 Value: \"{}\" \n{}\n",
+                        "\nError at token {}: Invalid Uint256 Value: \"{}\" \n{}\n",
+                        pe.cursor,
                         v,
                         pe.spans.error(pe.hint.as_ref())
                     )
@@ -438,7 +454,8 @@ impl fmt::Display for CompilerError {
                 ParserErrorKind::InvalidBytes(b) => {
                     write!(
                         f,
-                        "\nError: Invalid Bytes Value: \"{}\" \n{}\n",
+                        "\nError at token {}: Invalid Bytes Value: \"{}\" \n{}\n",
+                        pe.cursor,
                         b,
                         pe.spans.error(pe.hint.as_ref())
                     )
@@ -446,7 +463,8 @@ impl fmt::Display for CompilerError {
                 ParserErrorKind::InvalidInt(i) => {
                     write!(
                         f,
-                        "\nError: Invalid Int Value: \"{}\" \n{}\n",
+                        "\nError at token {}: Invalid Int Value: \"{}\" \n{}\n",
+                        pe.cursor,
                         i,
                         pe.spans.error(pe.hint.as_ref())
                     )
@@ -454,7 +472,8 @@ impl fmt::Display for CompilerError {
                 ParserErrorKind::InvalidMacroArgs(ma) => {
                     write!(
                         f,
-                        "\nError: Invalid Macro Arguments: \"{}\" \n{}\n",
+                        "\nError at token {}: Invalid Macro Arguments: \"{}\" \n{}\n",
+                        pe.cursor,
                         ma,
                         pe.spans.error(pe.hint.as_ref())
                     )
@@ -462,14 +481,16 @@ impl fmt::Display for CompilerError {
                 ParserErrorKind::InvalidReturnArgs => {
                     write!(
                         f,
-                        "\nError: Invalid Return Arguments\n{}\n",
+                        "\nError at token {}: Invalid Return Arguments\n{}\n",
+                        pe.cursor,
                         pe.spans.error(pe.hint.as_ref())
                     )
                 }
                 ParserErrorKind::InvalidImportPath(ip) => {
                     write!(
                         f,
-                        "\nError: Invalid Import Path: \"{}\" \n{}\n",
+                        "\nError at token {}: Invalid Import Path: \"{}\" \n{}\n",
+                        pe.cursor,
                         ip,
                         pe.spans.error(pe.hint.as_ref())
                     )
@@ -477,7 +498,8 @@ impl fmt::Display for CompilerError {
                 ParserErrorKind::InvalidDecoratorFlag(df) => {
                     write!(
                         f,
-                        "\nError: Invalid Decorator Flag: \"{}\" \n{}\n",
+                        "\nError at token {}: Invalid Decorator Flag: \"{}\" \n{}\n",
+                        pe.cursor,
                         df,
                         pe.spans.error(pe.hint.as_ref())
                     )
@@ -485,7 +507,8 @@ impl fmt::Display for CompilerError {
                 ParserErrorKind::InvalidDecoratorFlagArg(dfa) => {
                     write!(
                         f,
-                        "\nError: Invalid Decorator Flag Argument: \"{}\" \n{}\n",
+                        "\nError at token {}: Invalid Decorator Flag Argument: \"{}\" \n{}\n",
+                        pe.cursor,
                         dfa,
                         pe.spans.error(pe.hint.as_ref())
                     )


Test patch:
diff --git a/huff_core/tests/parser_errors.rs b/huff_core/tests/parser_errors.rs
index a41a7c2..21feb71 100644
--- a/huff_core/tests/parser_errors.rs
+++ b/huff_core/tests/parser_errors.rs
@@ -42,6 +42,7 @@ fn test_invalid_macro_statement() {
                     kind: ParserErrorKind::InvalidTokenInMacroBody(TokenKind::FreeStoragePointer),
                     hint: None,
                     spans: AstSpan(vec![Span { start: const_start, end: const_end, file: None }]),
+                    cursor: 36,
                 }
             )
         }
@@ -72,6 +73,7 @@ fn test_unexpected_type() {
                         end: source.find("internal").unwrap_or(0) + "internal".len() - 1,
                         file: None
                     }]),
+                    cursor: 5,
                 }
             )
         }
@@ -103,6 +105,7 @@ fn test_invalid_definition() {
                         end: source.find("invalid").unwrap_or(0) + "invalid".len() - 1,
                         file: None
                     }]),
+                    cursor: 1,
                 }
             )
         }
@@ -147,6 +150,7 @@ fn test_invalid_constant_value() {
                             end: source.find(value).unwrap_or(0) + value.len() - 1,
                             file: None
                         }]),
+                        cursor: 4
                     }
                 )
             }
@@ -191,6 +195,7 @@ fn test_invalid_token_in_macro_body() {
                             end: source.rfind(value).unwrap_or(0) + value.len() - 1,
                             file: None
                         }]),
+                        cursor: 15,
                     }
                 )
             }
@@ -236,6 +241,7 @@ fn test_invalid_token_in_label_definition() {
                             end: source.rfind(value).unwrap_or(0) + value.len() - 1,
                             file: None
                         }]),
+                        cursor: 17,
                     }
                 )
             }
@@ -279,6 +285,7 @@ fn test_invalid_single_arg() {
                         ))),
                         hint: Some("Expected number representing stack item count.".to_string()),
                         spans: AstSpan(vec![Span { start: 34, end: 34, file: None }]),
+                        cursor: 8,
                     }
                 )
             }
diff --git a/huff_parser/tests/macro.rs b/huff_parser/tests/macro.rs
index a201799..f8ed7a8 100644
--- a/huff_parser/tests/macro.rs
+++ b/huff_parser/tests/macro.rs
@@ -1291,6 +1291,7 @@ fn test_duplicate_macro_error() {
                         end: occurrences[1].1,
                         file: None
                     }]),
+                    cursor: 58,
                 }
             )
         }