diff --git a/docs/parser-recovery.md b/docs/parser-recovery.md new file mode 100644 index 00000000000..e5d7be8efb5 --- /dev/null +++ b/docs/parser-recovery.md @@ -0,0 +1,1221 @@ +# Parser Recovery Issues + +This file tracks unique parser recovery failures found while improving syntax diagnostics. + +## Summary + +Status as of this pass: 31 unique recovery cases are documented, fixed, and covered by focused +parser tests. + +The recurring root cause was that local parse recovery only knew about the current closing token. +When a syntax error made that close token unreachable, recovery consumed later declarations or +entries and produced cascading diagnostics. The parser now has shared recovery boundaries for: + +- Top-level declarations: `TYPE`, `FUNCTION`, `FUNCTION_BLOCK`, `PROGRAM`, `CLASS`, `INTERFACE`, + global/config variable blocks, and action blocks. +- Member declarations: methods and properties inside class/function-block/interface contexts. +- Statement block ends and enclosing POU/member ends. +- Repeated element starts inside lists, such as variable declarations and `VAR_CONFIG` entries. +- Delimited expression/type regions where a missing comma, close delimiter, or keyword should not + consume the next useful element. + +The implementation follows the same broad approach as Coil's `recovery.rs`: centralize construct +starts and recovery boundaries, then let narrow parser regions stop when they see a sensible next +construct instead of blindly eating input until EOF or a distant close token. + +## Wrong `END_*` after `TYPE` struct consumes next top-level declaration + +Status: fixed, tested + +Minimal reproducer: + +```iecst +TYPE Position: + STRUCT + x: DINT; + END_STRUCT +END_POSITION + +FUNCTION_BLOCK FbA +END_FUNCTION_BLOCK +``` + +Current bad diagnostic behavior: + +The parser treated `END_POSITION` as the start of another type declaration inside the open +`TYPE` block. It then consumed the following `FUNCTION_BLOCK` while trying to recover the +invented type declaration, producing cascading errors in unrelated code. + +Expected improved behavior: + +Report the wrong `END_POSITION` near the malformed type declaration, then recover before the +following `FUNCTION_BLOCK` so that the function block is parsed independently. + +Recovery boundary: + +After a malformed top-level `TYPE` declaration, recover at the next top-level declaration keyword +such as `FUNCTION_BLOCK`, `FUNCTION`, `PROGRAM`, `CLASS`, `TYPE`, global variable blocks, actions, +or interfaces. + +## Missing `END_TYPE` before next top-level declaration + +Status: fixed, tested + +Minimal reproducer: + +```iecst +TYPE Position: + STRUCT + x: DINT; + END_STRUCT + +FUNCTION main +END_FUNCTION +``` + +Current bad diagnostic behavior: + +The parser remained inside the `TYPE` block and could consume the following top-level declaration +while recovering. + +Expected improved behavior: + +Report the missing `END_TYPE` and leave the following `FUNCTION` available to the top-level parser. + +Recovery boundary: + +The next top-level declaration keyword. + +## Missing `END_STRUCT` and `END_TYPE` before next top-level declaration + +Status: fixed, tested + +Minimal reproducer: + +```iecst +TYPE Position: + STRUCT + x: DINT; + +FUNCTION main +END_FUNCTION +``` + +Current bad diagnostic behavior: + +The inner `STRUCT` recovery consumed `FUNCTION main` and `END_FUNCTION` while searching for +`END_STRUCT`, then the outer `TYPE` recovery reported again at EOF. + +Expected improved behavior: + +Report the missing `END_STRUCT` and `END_TYPE` at `FUNCTION main`, then parse the function +independently. + +Recovery boundary: + +The enclosing `END_TYPE` or the next top-level declaration keyword. + +## Missing semicolon between struct members + +Status: fixed, tested + +Minimal reproducer: + +```iecst +TYPE Position: + STRUCT + x: DINT + y: DINT; + END_STRUCT +END_TYPE +``` + +Current bad diagnostic behavior: + +The parser reported the missing semicolon at `y: DINT`, but the recovery consumed that whole +member while searching for the semicolon, so `y` was dropped from the recovered struct AST. + +Expected improved behavior: + +Report the missing semicolon at `y`, stop at the next `identifier :` variable declaration start, +and parse `y` as the next struct member. + +Recovery boundary: + +The next variable declaration start, following Coil's `element_start` recovery pattern. + +## Missing colon before a variable or struct member type + +Status: fixed, tested + +Minimal reproducers: + +```iecst +TYPE Position: + STRUCT + x INT; + y: INT; + END_STRUCT +END_TYPE +``` + +```iecst +FUNCTION_BLOCK FbA + VAR + x INT; + y: INT; + END_VAR +END_FUNCTION_BLOCK +``` + +Current bad diagnostic behavior: + +The declaration parser treated the type name after `x` as another variable name. It then reported +multiple missing colon/comma diagnostics and finally reported the semicolon as an unexpected +datatype token. + +Expected improved behavior: + +Report one missing `:` at the apparent type name, parse `x` with that type, and continue with the +following declaration. + +Recovery boundary: + +After the first variable name in a declaration line, an identifier-like token followed by a +datatype suffix or declaration terminator can be treated as the datatype boundary for missing-colon +recovery. + +## Missing enum close paren before next top-level declaration + +Status: fixed, tested + +Minimal reproducer: + +```iecst +TYPE Color: + ( + Red, + Green + +FUNCTION main +END_FUNCTION +``` + +Current bad diagnostic behavior: + +The enum list recovery consumed `FUNCTION main` and `END_FUNCTION` while searching for `)`, then +reported the missing enum close, declaration semicolon, and `END_TYPE` at EOF. + +Expected improved behavior: + +Report the missing enum close, declaration semicolon, and `END_TYPE` at `FUNCTION main`, then +parse the function independently. + +Recovery boundary: + +The enclosing `END_TYPE` or the next top-level declaration keyword. + +## Missing comma between enum elements + +Status: fixed, tested + +Minimal reproducer: + +```iecst +TYPE Color: + ( + Red + Green, + Blue + ); +END_TYPE +``` + +Current bad diagnostic behavior: + +After parsing `Red`, the enum element list stopped because there was no comma. The enclosing enum +region then consumed `Green, Blue` while searching for `)`, producing a broad diagnostic and +dropping the later enum elements from the recovered AST. + +Expected improved behavior: + +Report the missing comma at `Green`, then continue parsing `Green` and `Blue` as enum elements. + +Recovery boundary: + +The next identifier-like enum element start inside an enum list. + +## Missing `OF` in array type before next top-level declaration + +Status: fixed, tested + +Minimal reproducer: + +```iecst +TYPE Matrix: + ARRAY [1..10] + +FUNCTION main +END_FUNCTION +``` + +Current bad diagnostic behavior: + +The array range parser searched for `OF` without the enclosing type recovery boundaries. It +consumed `FUNCTION main` and `END_FUNCTION` while recovering the array type, then reported the +remaining type declaration errors at EOF. + +Expected improved behavior: + +Report the missing `OF` at `FUNCTION main`, keep `FUNCTION main` available to the top-level +parser, and parse the function independently. + +Recovery boundary: + +The enclosing `END_TYPE` or the next top-level declaration keyword. + +## Missing `]` in array range before `OF` + +Status: fixed, tested + +Minimal reproducer: + +```iecst +TYPE Matrix: + ARRAY [1..10 OF DINT; +END_TYPE +``` + +Current bad diagnostic behavior: + +The parser reported that `OF` was not `]`, then consumed `OF` during recovery. The next diagnostic +was shifted onto `DINT`, as if the element type were an unexpected token instead of the parser +continuing after a recoverable missing bracket. + +Expected improved behavior: + +Report the missing `]` at `OF`, keep `OF` as the array grammar boundary, and continue parsing the +array element type. + +Recovery boundary: + +The array `OF` separator, without consuming it while reporting the missing bracket. + +## Missing comma in delimited expression list + +Status: fixed, tested + +Minimal reproducers: + +```iecst +TYPE Matrix: + ARRAY [1..10 20..30] OF DINT; +END_TYPE +``` + +```iecst +FUNCTION_BLOCK FbA + VAR + xs: ARRAY [1..10 20..30] OF DINT; + y: DINT; + END_VAR +END_FUNCTION_BLOCK +``` + +```iecst +FUNCTION_BLOCK FbA + VAR + xs: Arr := [1 2]; + y: DINT; + END_VAR +END_FUNCTION_BLOCK +``` + +```iecst +FUNCTION_BLOCK FbA + VAR + p: Position := (x := 1 y := 2); + z: DINT; + END_VAR +END_FUNCTION_BLOCK +``` + +```iecst +PROGRAM main + buz(a,b c); +END_PROGRAM +``` + +Current bad diagnostic behavior: + +Delimited expression lists stopped after the first element unless an explicit comma was present. +In array bounds this made the second range look like a missing `]` and then unexpected trailing +input before `OF`. In array literals, struct initializers, and call arguments, the following +expression looked like a missing closing delimiter or was dropped from the recovered AST. + +Expected improved behavior: + +Report a missing comma at the next expression element, preserve the following element in the +expression list, and continue parsing the array type or initializer normally. + +Recovery boundary: + +Inside `(...)` and `[...]` expression-list regions, an expression-start token after a completed +expression is treated as the next list element with a missing comma. + +## Missing `OF` in variable array type before next variable + +Status: fixed, tested + +Minimal reproducer: + +```iecst +FUNCTION_BLOCK FbA + VAR + xs: ARRAY [1..10] + y: DINT; + END_VAR +END_FUNCTION_BLOCK +``` + +Current bad diagnostic behavior: + +The array range recovery inside `xs` did not know about variable declaration element starts. It +consumed `y: DINT` while searching for `OF`, then produced duplicate `OF` and datatype +diagnostics at the following semicolon. + +Expected improved behavior: + +Report the missing `OF` at `y`, report the missing semicolon for `xs` at the same boundary, and +parse `y` as the next variable declaration. + +Recovery boundary: + +The next `identifier :` variable declaration start inside a variable block. + +## Missing `]` in string size before next declaration + +Status: fixed, tested + +Minimal reproducers: + +```iecst +FUNCTION_BLOCK FbA + VAR + s: STRING[10 + x: DINT; + END_VAR +END_FUNCTION_BLOCK +``` + +```iecst +TYPE Name: + STRING[10 + +FUNCTION other +END_FUNCTION +``` + +Current bad diagnostic behavior: + +The string-size expression recovery searched only for `]`, so a malformed `STRING[10` could +consume the following variable declaration or top-level declaration while looking for the missing +bracket. + +Expected improved behavior: + +Report the missing `]` at the next declaration boundary, report the enclosing missing semicolon or +`END_TYPE` where appropriate, and keep the following declaration available to the enclosing parser. + +Recovery boundary: + +The next `identifier :` variable declaration start in variable/member contexts, or the next +top-level declaration keyword in a type declaration context. + +## Missing comma in interface inheritance or implementation list + +Status: fixed, tested + +Minimal reproducers: + +```iecst +INTERFACE IBase +END_INTERFACE + +INTERFACE IOther +END_INTERFACE + +INTERFACE IFoo EXTENDS IBase IOther +END_INTERFACE +``` + +```iecst +INTERFACE IBase +END_INTERFACE + +INTERFACE IOther +END_INTERFACE + +FUNCTION_BLOCK FbA IMPLEMENTS IBase IOther +END_FUNCTION_BLOCK +``` + +Current bad diagnostic behavior: + +The parser accepted adjacent interface names as if a comma were present, so malformed `EXTENDS` +and `IMPLEMENTS` lists produced no syntax diagnostic. + +Expected improved behavior: + +Report the missing comma at the second interface name and continue parsing the list. + +Recovery boundary: + +The next identifier-like interface name in the comma-separated list. + +## Missing delimiter in generic parameter list + +Status: fixed, tested + +Minimal reproducers: + +```iecst +FUNCTION test : R +END_FUNCTION +``` + +```iecst +FUNCTION test`. When the final +`>` was missing before the function return type, recovery could continue to `END_FUNCTION` and +emit duplicate `OperatorGreater` diagnostics. + +Expected improved behavior: + +Report a missing comma at the next generic binding start, preserve both generic bindings in the +recovered AST, and report a missing `>` at the return-type `:` without consuming the rest of the +function declaration. + +Recovery boundary: + +Inside a generic parameter list, recover at the next `identifier :` generic binding start or at the +return-type `:` that follows the list. + +## Missing base type after `REF_TO` + +Status: fixed, tested + +Minimal reproducers: + +```iecst +TYPE RefInt: + REF_TO + +FUNCTION other +END_FUNCTION +``` + +```iecst +FUNCTION_BLOCK FbA + VAR + r: REF_TO + x: DINT; + END_VAR +END_FUNCTION_BLOCK +``` + +Current bad diagnostic behavior: + +When `REF_TO` or `POINTER TO` was missing its base type, nested datatype parsing did not know about +the enclosing recovery boundaries. At top level it reported an unexpected datatype token at the +next declaration, and in a variable block it could consume the next variable name as the missing +base type before reporting at the colon. + +Expected improved behavior: + +Report the missing base datatype at the recovery boundary, leave the following top-level +declaration or variable declaration available to its enclosing parser, and avoid swallowing the +next declaration as the reference base type. + +Recovery boundary: + +The next top-level declaration keyword or the next `identifier :` variable declaration start, +depending on the enclosing context. + +## Missing expression delimiter before statement semicolon + +Status: fixed, tested + +Minimal reproducers: + +```iecst +FUNCTION main + foo(1, 2; +END_FUNCTION +``` + +```iecst +FUNCTION main + x := arr[1; +END_FUNCTION +``` + +```iecst +FUNCTION_BLOCK FbA + VAR + p: Position := (x := 1, y := 2; + z: DINT; + END_VAR +END_FUNCTION_BLOCK +``` + +Current bad diagnostic behavior: + +Expression subregions recovered only to their own closing delimiter. When a call, subscript, or +initializer was missing a closing `)` or `]` before the statement semicolon, the parser reported +both a missing delimiter and an unexpected semicolon for the same source location. + +Expected improved behavior: + +Report only the missing delimiter at the semicolon, leave the semicolon for the enclosing +statement or variable declaration, and continue parsing the following statement or variable. + +Recovery boundary: + +The statement semicolon and enclosing statement/declaration block boundaries. + +## Missing delimiter in variable initializer before next variable + +Status: fixed, tested + +Minimal reproducers: + +```iecst +FUNCTION_BLOCK FbA + VAR + p: Position := (x := 1 + z: DINT; + END_VAR +END_FUNCTION_BLOCK +``` + +```iecst +FUNCTION_BLOCK FbA + VAR + p: DINT := foo(1 + z: DINT; + END_VAR +END_FUNCTION_BLOCK +``` + +```iecst +FUNCTION_BLOCK FbA + VAR + p: DINT := arr[1 + z: DINT; + END_VAR +END_FUNCTION_BLOCK +``` + +Current bad diagnostic behavior: + +Delimited expression recovery in initializers searched only for the missing `)` or `]` and broad +expression stop tokens. Without a semicolon after the bad initializer, it consumed the next +`identifier :` variable declaration as part of the initializer and emitted duplicate delimiter +diagnostics. + +Expected improved behavior: + +Report the missing delimiter at the next variable declaration boundary, report the missing +semicolon for the malformed declaration at the same boundary, and parse the following variable +independently. + +Recovery boundary: + +The next `identifier :` variable declaration start while recovering from a delimited expression +region. + +## Missing control-statement header delimiter before body + +Status: fixed, tested + +Minimal reproducers: + +```iecst +FUNCTION main + IF x + y := 1; + END_IF + z := 2; +END_FUNCTION +``` + +```iecst +FUNCTION main + FOR i := 0 10 DO + y := i; + END_FOR + z := 2; +END_FUNCTION +``` + +Current bad diagnostic behavior: + +Missing `THEN` in an `IF` header and missing `TO` in a `FOR` header aborted the current control +statement parsing path. The parser then treated the body or terminator as unrelated top-level +statements, producing cascades through `END_IF`/`END_FOR` and the following statement. + +Expected improved behavior: + +Report the missing header delimiter at the first token where it should have appeared, then keep +parsing the control statement body and following statement. + +Recovery boundary: + +The next body statement after `IF` condition when `THEN` is missing, and the final bound +expression after the `FOR` start expression when `TO` is missing. + +## Missing `OF` in `CASE` header before first label + +Status: fixed, tested + +Minimal reproducer: + +```iecst +FUNCTION main + CASE x + 1: y := 1; + END_CASE + z := 2; +END_FUNCTION +``` + +Current bad diagnostic behavior: + +The parser aborted the `CASE` statement as soon as `OF` was missing. The first label and +`END_CASE` were then parsed as unrelated statements, producing cascades through the case terminator +and following statement. + +Expected improved behavior: + +Report the missing `OF` at the first label and continue parsing the `CASE` arms. + +Recovery boundary: + +The first case label after the selector expression. + +## Missing `:` in `CASE` selection before body or next label + +Status: fixed, tested + +Minimal reproducers: + +```iecst +FUNCTION main + CASE x OF + 1 y := 1; + 2: y := 2; + END_CASE + z := 3; +END_FUNCTION +``` + +```iecst +FUNCTION main + CASE x OF + 1 + 2: y := 2; + END_CASE + z := 3; +END_FUNCTION +``` + +Current bad diagnostic behavior: + +When a case selection missed the `:` delimiter, the parser used the statement semicolon as the +next recovery point or treated the next label as an unexpected statement token. That either +dropped the malformed arm body or prevented the following case label from being parsed as a +separate arm. In the first-arm case, a later statement could also receive a misleading +`Missing Case-Condition` diagnostic. + +Expected improved behavior: + +Report the missing `:` at the first token that starts the arm body or the next `case-label:`, keep +the malformed arm local, and continue parsing later case arms and following statements. + +Recovery boundary: + +Inside a `CASE` statement, recover a missing selection delimiter at a plausible statement start, +the next `case-label:` start, `ELSE`, `END_CASE`, or an enclosing statement/top-level boundary. + +## Missing `END_REPEAT` after `UNTIL` condition before next statement + +Status: fixed, tested + +Minimal reproducer: + +```iecst +FUNCTION main + REPEAT + x := 1; + UNTIL x > 5 + z := 2; +END_FUNCTION +``` + +Current bad diagnostic behavior: + +The parser treated the following `z := 2;` statement as part of recovery while looking for +`END_REPEAT`, then also reported again at the enclosing `END_FUNCTION`. + +Expected improved behavior: + +Report the missing `END_REPEAT` at `z`, leave `z := 2;` available to the enclosing function body, +and avoid duplicate recovery diagnostics for the same missing terminator. + +Recovery boundary: + +The next statement after the `UNTIL` condition. + +## Missing POU end before next top-level declaration + +Status: fixed, tested + +Minimal reproducer: + +```iecst +FUNCTION_BLOCK FbA + VAR + x: DINT; + END_VAR + +FUNCTION main +END_FUNCTION +``` + +Current bad diagnostic behavior: + +The parser remains inside `FUNCTION_BLOCK FbA` and attempts to parse `FUNCTION main` as a +statement in the function block body. It reports several expression and semicolon diagnostics +before finally reporting that `END_FUNCTION` did not match `END_FUNCTION_BLOCK`. + +Expected improved behavior: + +Report the missing `END_FUNCTION_BLOCK` at the next top-level `FUNCTION` boundary and parse +`FUNCTION main` independently. + +Recovery boundary: + +The next top-level declaration keyword after a POU header, variable block, method/property block, +or implementation body when the expected `END_*` for the current POU has not appeared. + +## Missing `END_VAR` before next top-level declaration + +Status: fixed, tested + +Minimal reproducer: + +```iecst +FUNCTION_BLOCK FbA + VAR + x: DINT; + +FUNCTION main +END_FUNCTION +``` + +Current bad diagnostic behavior: + +The parser stayed inside the variable block, consumed `FUNCTION main` while searching for +`END_VAR`, and then also reported against `END_FUNCTION`. + +Expected improved behavior: + +Report the missing `END_VAR` at the `FUNCTION` boundary, then let the enclosing POU recovery +report the missing `END_FUNCTION_BLOCK` without consuming `FUNCTION main`. + +Recovery boundary: + +The next top-level declaration keyword. + +## Missing `END_VAR` before next member declaration + +Status: fixed, tested + +Minimal reproducer: + +```iecst +FUNCTION_BLOCK FbA + VAR + x: DINT; + + METHOD second + END_METHOD +END_FUNCTION_BLOCK +``` + +Current bad diagnostic behavior: + +The parser stayed inside the variable block, consumed `METHOD second` and `END_METHOD` while +searching for `END_VAR`, then cascaded again at `END_FUNCTION_BLOCK`. + +Expected improved behavior: + +Report the missing `END_VAR` at `METHOD second` and parse the method as the next member. + +Recovery boundary: + +The next member declaration keyword or enclosing POU/member end token. + +## Missing semicolon between variables in `VAR` block + +Status: fixed, tested + +Minimal reproducer: + +```iecst +FUNCTION_BLOCK FbA + VAR + x: DINT + y: DINT; + END_VAR +END_FUNCTION_BLOCK +``` + +Current bad diagnostic behavior: + +The parser reported the missing semicolon at `y: DINT`, but recovery consumed the following +variable while looking for the semicolon, so `y` was dropped from the recovered variable block. + +Expected improved behavior: + +Report the missing semicolon at `y`, stop at the next `identifier :` variable declaration start, +and parse `y` as the next variable. + +Recovery boundary: + +The next variable declaration start. + +## Missing `END_VAR` in `VAR_CONFIG` before next top-level declaration + +Status: fixed, tested + +Minimal reproducer: + +```iecst +VAR_CONFIG + main.x AT %QX0.0 : BOOL; + +FUNCTION main +END_FUNCTION +``` + +Current bad diagnostic behavior: + +The parser stayed inside `VAR_CONFIG`, consumed `FUNCTION main` and `END_FUNCTION` while looking +for `END_VAR`, and then reported another `END_VAR` error at EOF. + +Expected improved behavior: + +Report the missing `END_VAR` at `FUNCTION main` and parse the function independently. + +Recovery boundary: + +The next top-level declaration keyword. + +## Missing semicolon in `VAR_CONFIG` entry before next top-level declaration + +Status: fixed, tested + +Minimal reproducers: + +```iecst +VAR_CONFIG + main.x AT %QX0.0 : BOOL + +FUNCTION main +END_FUNCTION +``` + +```iecst +VAR_CONFIG + main.x AT %QW0 : DINT + main.y AT %QW1 : INT; +END_VAR +``` + +Current bad diagnostic behavior: + +The parser stayed inside the unterminated config entry, consumed `FUNCTION main` and +`END_FUNCTION` while searching for the entry semicolon, then also reported `END_VAR` at EOF. It +also consumed later `qualified.name AT ...` config entries in the same block as trailing text from +the bad entry. + +Expected improved behavior: + +Report the missing semicolon at `FUNCTION main` or the next config entry, report the still-missing +`END_VAR` at the same boundary when needed, and parse the following top-level declaration or config +entry independently. + +Recovery boundary: + +The enclosing `END_VAR`, the next top-level declaration keyword, or the next config entry start +(`qualified.name AT ...`). + +## Missing statement block terminator before next top-level declaration + +Status: fixed, tested + +Minimal reproducers: + +```iecst +FUNCTION main + IF TRUE THEN + x := 1; + +FUNCTION other +END_FUNCTION +``` + +```iecst +FUNCTION main + FOR i := 0 TO 10 DO + x := i; + +FUNCTION other +END_FUNCTION +``` + +```iecst +FUNCTION main + WHILE TRUE DO + x := 1; + +FUNCTION other +END_FUNCTION +``` + +```iecst +FUNCTION main + REPEAT + x := 1; + +FUNCTION other +END_FUNCTION +``` + +```iecst +FUNCTION main + CASE x OF + 1: y := 1; + +FUNCTION other +END_FUNCTION +``` + +Current bad diagnostic behavior: + +The parser stayed inside the open statement block, parsed the following `FUNCTION` as a statement, +and emitted expression and semicolon cascades before reaching `END_FUNCTION`. + +Expected improved behavior: + +Report the missing statement terminator (`END_IF`, `END_FOR`, `END_WHILE`, `UNTIL`, or +`END_CASE`) and the missing enclosing `END_FUNCTION` at the `FUNCTION other` boundary, then +parse `FUNCTION other` independently. + +Recovery boundary: + +The enclosing POU end token or the next top-level declaration keyword, based on the IEC statement +block grammar. + +## Missing `END_METHOD` before next member declaration + +Status: fixed, tested + +Minimal reproducer: + +```iecst +FUNCTION_BLOCK FbA + METHOD first + + METHOD second + END_METHOD +END_FUNCTION_BLOCK +``` + +Current bad diagnostic behavior: + +The parser stayed inside `METHOD first` and parsed `METHOD second` as a statement, producing +expression and semicolon diagnostics before reaching `END_METHOD`. + +Expected improved behavior: + +Report the missing `END_METHOD` at `METHOD second` and parse `METHOD second` as a separate member. + +Recovery boundary: + +The next member declaration keyword, such as `METHOD`, `PROPERTY_GET`, or `PROPERTY_SET`. + +## Missing `END_PROPERTY` before next member declaration + +Status: fixed, tested + +Minimal reproducer: + +```iecst +FUNCTION_BLOCK FbA + PROPERTY_GET first: DINT + first := 1; + + METHOD second + END_METHOD +END_FUNCTION_BLOCK +``` + +Current bad diagnostic behavior: + +The parser stayed inside the property body, consumed `METHOD second` as a statement, and cascaded +through `END_METHOD` to the enclosing `END_FUNCTION_BLOCK`. + +Expected improved behavior: + +Report the missing `END_PROPERTY` at `METHOD second` and parse `METHOD second` as a separate +member. + +Recovery boundary: + +The next member declaration keyword, such as `METHOD`, `PROPERTY_GET`, or `PROPERTY_SET`. + +## Missing `END_ACTION` before next action declaration + +Status: fixed, tested + +Minimal reproducer: + +```iecst +PROGRAM Main +END_PROGRAM + +ACTIONS Main + ACTION first + x := 1; + + ACTION second + x := 2; + END_ACTION +END_ACTIONS +``` + +Current bad diagnostic behavior: + +The parser stayed inside `ACTION first` and parsed `ACTION second` as a statement, producing +expression and semicolon diagnostics before it reached the later `END_ACTION`. + +Expected improved behavior: + +Report the missing `END_ACTION` at `ACTION second` and parse `ACTION second` independently. + +Recovery boundary: + +The next `ACTION` declaration inside an `ACTIONS` block, or `END_ACTIONS`. + +## Missing `END_ACTIONS` before next top-level declaration + +Status: fixed, tested + +Minimal reproducer: + +```iecst +PROGRAM Main +END_PROGRAM + +ACTIONS Main + ACTION first + x := 1; + END_ACTION + +FUNCTION other +END_FUNCTION +``` + +Current bad diagnostic behavior: + +The parser stayed inside the `ACTIONS` block, reported that `FUNCTION` was not an `ACTION`, +then consumed `FUNCTION other` while recovering to `END_ACTIONS` and reported again at EOF. + +Expected improved behavior: + +Report the missing `END_ACTIONS` at `FUNCTION other` and parse `FUNCTION other` as a separate +top-level declaration. + +Recovery boundary: + +The next top-level declaration keyword. + +## Missing `END_METHOD` before `END_INTERFACE` + +Status: fixed, tested + +Minimal reproducer: + +```iecst +INTERFACE IFoo + METHOD first + +END_INTERFACE + +FUNCTION main +END_FUNCTION +``` + +Current bad diagnostic behavior: + +The parser stayed inside the interface method body, consumed `END_INTERFACE` and the following +`FUNCTION main`, then reported additional method/interface errors at EOF. + +Expected improved behavior: + +Report the missing `END_METHOD` at `END_INTERFACE`, close the interface, and parse the following +`FUNCTION` independently. + +Recovery boundary: + +The enclosing interface end token. + +## Missing hardware access in `VAR_CONFIG` entry before type tail + +Status: fixed, tested + +Minimal reproducer: + +```iecst +VAR_CONFIG + main.x AT : BOOL; + main.y AT %QW1 : INT; +END_VAR + +FUNCTION other +END_FUNCTION +``` + +Current bad diagnostic behavior: + +The parser reported the useful missing hardware access at `:`, then reported a second unexpected +token diagnostic for the `: BOOL` tail while trying to recover to the entry semicolon. + +Expected improved behavior: + +Report only the missing hardware access for the malformed entry, skip the rest of that entry, and +continue parsing the next `VAR_CONFIG` entry and following top-level declaration. + +Recovery boundary: + +The current config entry semicolon, the next config entry, `END_VAR`, or the next top-level +declaration. diff --git a/src/lexer.rs b/src/lexer.rs index 867e089c872..2b46755e6b2 100644 --- a/src/lexer.rs +++ b/src/lexer.rs @@ -31,7 +31,7 @@ macro_rules! expect_token { ($lexer:expr, $token:expr, $return_value:expr) => { if $lexer.token != $token { $lexer.accept_diagnostic(Diagnostic::unexpected_token_found( - format!("{:?}", $token).as_str(), + $token.to_string().as_str(), $lexer.slice(), $lexer.location(), )); @@ -82,7 +82,7 @@ impl<'a> ParseSession<'a> { pub fn try_consume_or_report(&mut self, token: Token) { if !self.try_consume(token) { - self.accept_diagnostic(Diagnostic::missing_token(format!("{token:?}").as_str(), self.location())); + self.accept_diagnostic(Diagnostic::missing_token(token.to_string().as_str(), self.location())); } } @@ -143,6 +143,26 @@ impl<'a> ParseSession<'a> { self.lexer.slice() } + pub fn peek_token(&self) -> Token { + let mut lexer = self.lexer.clone(); + lexer.next().unwrap_or(Token::End) + } + + pub fn token_appears_before(&self, needle: Token, boundaries: &[Token]) -> bool { + let mut lexer = self.lexer.clone(); + while let Some(token) = lexer.next() { + if token == needle { + return true; + } + + if boundaries.contains(&token) { + return false; + } + } + + false + } + pub fn location(&self) -> SourceLocation { self.source_range_factory.create_range(self.range()) } @@ -167,7 +187,7 @@ impl<'a> ParseSession<'a> { if let Some(expected_token) = self.closing_keywords.pop() { if !expected_token.contains(&self.token) { self.accept_diagnostic(Diagnostic::unexpected_token_found( - format!("{:?}", expected_token[0]).as_str(), + expected_token[0].to_string().as_str(), format!("'{}'", self.slice()).as_str(), self.location(), )); @@ -202,11 +222,12 @@ impl<'a> ParseSession<'a> { if start.end != self.range().end { let range = start.start..end; self.accept_diagnostic(Diagnostic::unexpected_token_found( - format!( - "{:?}", - self.closing_keywords.last().and_then(|it| it.first()).unwrap_or(&Token::End) //only show first expected token - ) - .as_str(), + self.closing_keywords + .last() + .and_then(|it| it.first()) + .unwrap_or(&Token::End) + .to_string() + .as_str(), format!("'{}'", self.slice_region(range.clone())).as_str(), self.source_range_factory.create_range(range), )); @@ -218,7 +239,7 @@ impl<'a> ParseSession<'a> { .closing_keywords .last() .expect("parse-recovery has no closing-keyword to recover from."); //illegal state! invalid use of parser-recovery? - let expected_tokens = format!("{closing:?}"); + let expected_tokens = Token::display_list(closing); self.accept_diagnostic(Diagnostic::missing_token(expected_tokens.as_str(), self.location())); } } diff --git a/src/lexer/tokens.rs b/src/lexer/tokens.rs index 7eeb92e0b1d..0895b0d401a 100644 --- a/src/lexer/tokens.rs +++ b/src/lexer/tokens.rs @@ -1,3 +1,5 @@ +use std::fmt; + use logos::Logos; use plc_ast::ast::{DirectAccessType, HardwareAccessType}; @@ -440,6 +442,14 @@ pub enum Token { } impl Token { + pub fn display_list(tokens: &[Token]) -> String { + match tokens { + [] => "end of file".into(), + [token] => token.to_string(), + tokens => tokens.iter().map(ToString::to_string).collect::>().join(" or "), + } + } + /// Returns true if the current token represents any `VAR(_*)` keyword pub fn is_var(&self) -> bool { matches!( @@ -460,3 +470,143 @@ impl Token { matches!(self, Token::Identifier | Token::KeywordPropertyGet | Token::KeywordPropertySet) } } + +impl fmt::Display for Token { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Token::Error => write!(f, "unknown token"), + Token::PropertyExternal => write!(f, "`@EXTERNAL`"), + Token::PropertyByRef => write!(f, "`{{ref}}`"), + Token::PropertyConstant => write!(f, "`@CONSTANT`"), + Token::PropertySized => write!(f, "`{{sized}}`"), + Token::KeywordProgram => write!(f, "`PROGRAM`"), + Token::KeywordClass => write!(f, "`CLASS`"), + Token::KeywordEndClass => write!(f, "`END_CLASS`"), + Token::KeywordExtends => write!(f, "`EXTENDS`"), + Token::KeywordImplements => write!(f, "`IMPLEMENTS`"), + Token::KeywordInterface => write!(f, "`INTERFACE`"), + Token::KeywordEndInterface => write!(f, "`END_INTERFACE`"), + Token::KeywordVarInput => write!(f, "`VAR_INPUT`"), + Token::KeywordVarOutput => write!(f, "`VAR_OUTPUT`"), + Token::KeywordVar => write!(f, "`VAR`"), + Token::KeywordVarConfig => write!(f, "`VAR_CONFIG`"), + Token::KeywordAbstract => write!(f, "`ABSTRACT`"), + Token::KeywordFinal => write!(f, "`FINAL`"), + Token::KeywordMethod => write!(f, "`METHOD`"), + Token::KeywordEndMethod => write!(f, "`END_METHOD`"), + Token::KeywordSuper => write!(f, "`SUPER`"), + Token::KeywordThis => write!(f, "`THIS`"), + Token::KeywordPropertyGet => write!(f, "`PROPERTY_GET`"), + Token::KeywordPropertySet => write!(f, "`PROPERTY_SET`"), + Token::KeywordEndProperty => write!(f, "`END_PROPERTY`"), + Token::KeywordConstant => write!(f, "`CONSTANT`"), + Token::KeywordRetain => write!(f, "`RETAIN`"), + Token::KeywordNonRetain => write!(f, "`NON_RETAIN`"), + Token::KeywordVarTemp => write!(f, "`VAR_TEMP`"), + Token::KeywordAccessPublic => write!(f, "`PUBLIC`"), + Token::KeywordAccessPrivate => write!(f, "`PRIVATE`"), + Token::KeywordAccessInternal => write!(f, "`INTERNAL`"), + Token::KeywordAccessProtected => write!(f, "`PROTECTED`"), + Token::KeywordOverride => write!(f, "`OVERRIDE`"), + Token::KeywordVarGlobal => write!(f, "`VAR_GLOBAL`"), + Token::KeywordVarInOut => write!(f, "`VAR_IN_OUT`"), + Token::KeywordVarExternal => write!(f, "`VAR_EXTERNAL`"), + Token::KeywordEndVar => write!(f, "`END_VAR`"), + Token::KeywordEndProgram => write!(f, "`END_PROGRAM`"), + Token::KeywordFunction => write!(f, "`FUNCTION`"), + Token::KeywordEndFunction => write!(f, "`END_FUNCTION`"), + Token::KeywordFunctionBlock => write!(f, "`FUNCTION_BLOCK`"), + Token::KeywordEndFunctionBlock => write!(f, "`END_FUNCTION_BLOCK`"), + Token::KeywordType => write!(f, "`TYPE`"), + Token::KeywordStruct => write!(f, "`STRUCT`"), + Token::KeywordEndType => write!(f, "`END_TYPE`"), + Token::KeywordEndStruct => write!(f, "`END_STRUCT`"), + Token::KeywordActions => write!(f, "`ACTIONS`"), + Token::KeywordAction => write!(f, "`ACTION`"), + Token::KeywordEndAction => write!(f, "`END_ACTION`"), + Token::KeywordEndActions => write!(f, "`END_ACTIONS`"), + Token::KeywordColon => write!(f, "`:`"), + Token::KeywordSemicolon => write!(f, "`;`"), + Token::KeywordAssignment => write!(f, "`:=`"), + Token::KeywordOutputAssignment => write!(f, "`=>`"), + Token::KeywordReferenceAssignment => write!(f, "`REF=`"), + Token::KeywordParensOpen => write!(f, "`(`"), + Token::KeywordParensClose => write!(f, "`)`"), + Token::KeywordSquareParensOpen => write!(f, "`[`"), + Token::KeywordSquareParensClose => write!(f, "`]`"), + Token::KeywordComma => write!(f, "`,`"), + Token::KeywordDotDotDot => write!(f, "`...`"), + Token::KeywordDotDot => write!(f, "`..`"), + Token::KeywordDot => write!(f, "`.`"), + Token::KeywordIf => write!(f, "`IF`"), + Token::KeywordThen => write!(f, "`THEN`"), + Token::KeywordElseIf => write!(f, "`ELSIF`"), + Token::KeywordElse => write!(f, "`ELSE`"), + Token::KeywordEndIf => write!(f, "`END_IF`"), + Token::KeywordFor => write!(f, "`FOR`"), + Token::KeywordTo => write!(f, "`TO`"), + Token::KeywordBy => write!(f, "`BY`"), + Token::KeywordDo => write!(f, "`DO`"), + Token::KeywordEndFor => write!(f, "`END_FOR`"), + Token::KeywordWhile => write!(f, "`WHILE`"), + Token::KeywordEndWhile => write!(f, "`END_WHILE`"), + Token::KeywordRepeat => write!(f, "`REPEAT`"), + Token::KeywordUntil => write!(f, "`UNTIL`"), + Token::KeywordEndRepeat => write!(f, "`END_REPEAT`"), + Token::KeywordCase => write!(f, "`CASE`"), + Token::KeywordReturn => write!(f, "`RETURN`"), + Token::KeywordExit => write!(f, "`EXIT`"), + Token::KeywordContinue => write!(f, "`CONTINUE`"), + Token::KeywordPointer => write!(f, "`POINTER`"), + Token::KeywordFunctionPointer => write!(f, "`__FPOINTER`"), + Token::KeywordRef => write!(f, "`REF_TO`"), + Token::KeywordReferenceTo => write!(f, "`REFERENCE TO`"), + Token::KeywordArray => write!(f, "`ARRAY`"), + Token::KeywordString => write!(f, "`STRING`"), + Token::KeywordWideString => write!(f, "`WSTRING`"), + Token::KeywordOf => write!(f, "`OF`"), + Token::KeywordAt => write!(f, "`AT`"), + Token::KeywordEndCase => write!(f, "`END_CASE`"), + Token::OperatorPlus => write!(f, "`+`"), + Token::OperatorMinus => write!(f, "`-`"), + Token::OperatorMultiplication => write!(f, "`*`"), + Token::OperatorExponent => write!(f, "`**`"), + Token::OperatorDivision => write!(f, "`/`"), + Token::OperatorEqual => write!(f, "`=`"), + Token::OperatorNotEqual => write!(f, "`<>`"), + Token::OperatorLess => write!(f, "`<`"), + Token::OperatorGreater => write!(f, "`>`"), + Token::OperatorLessOrEqual => write!(f, "`<=`"), + Token::OperatorGreaterOrEqual => write!(f, "`>=`"), + Token::OperatorAmp => write!(f, "`&`"), + Token::OperatorDeref => write!(f, "`^`"), + Token::OperatorModulo => write!(f, "`MOD`"), + Token::OperatorAndThen => write!(f, "`AND_THEN`"), + Token::OperatorAnd => write!(f, "`AND`"), + Token::OperatorOrElse => write!(f, "`OR_ELSE`"), + Token::OperatorOr => write!(f, "`OR`"), + Token::OperatorXor => write!(f, "`XOR`"), + Token::OperatorNot => write!(f, "`NOT`"), + Token::Identifier => write!(f, "identifier"), + Token::LiteralIntegerHex + | Token::LiteralIntegerOct + | Token::LiteralIntegerBin + | Token::LiteralInteger => { + write!(f, "integer literal") + } + Token::LiteralNull => write!(f, "`NULL`"), + Token::LiteralTrue => write!(f, "`TRUE`"), + Token::LiteralFalse => write!(f, "`FALSE`"), + Token::LiteralDate => write!(f, "date literal"), + Token::LiteralDateAndTime => write!(f, "date and time literal"), + Token::LiteralTimeOfDay => write!(f, "time of day literal"), + Token::LiteralTime => write!(f, "time literal"), + Token::DirectAccess(_) => write!(f, "direct access"), + Token::HardwareAccess(_) => write!(f, "hardware access"), + Token::LiteralString => write!(f, "string literal"), + Token::LiteralWideString => write!(f, "wide string literal"), + Token::TypeCastPrefix => write!(f, "type cast prefix"), + Token::End => write!(f, "end of file"), + } + } +} diff --git a/src/parser.rs b/src/parser.rs index bf1d951c484..f17896f281e 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -39,6 +39,7 @@ use self::{ mod control_parser; pub mod expressions_parser; +mod recovery; #[cfg(test)] pub mod tests; @@ -107,7 +108,9 @@ pub fn parse(mut lexer: ParseSession, lnk: LinkageType, file_name: &'static str) constant = false; } KeywordAction => { - if let Some(implementation) = parse_action(&mut lexer, linkage, None) { + if let Some(implementation) = + parse_action(&mut lexer, linkage, None, recovery::TOP_LEVEL_START) + { unit.implementations.push(implementation); } } @@ -153,23 +156,29 @@ fn parse_actions( linkage: LinkageType, default_container: &str, ) -> Vec { - parse_any_in_region(lexer, vec![KeywordEndActions], |lexer| { + let recovery_tokens = recovery::ACTIONS_BLOCK_BOUNDARY; + parse_any_in_region_until(lexer, vec![KeywordEndActions], recovery_tokens, |lexer| { lexer.advance(); let container = if lexer.token == Identifier { lexer.slice_and_advance() } else { default_container.into() }; let mut impls = vec![]; //Go through each action - while lexer.token != KeywordEndActions && !lexer.is_end_of_stream() { + while lexer.token != KeywordEndActions + && !lexer.is_end_of_stream() + && !recovery_tokens.contains(&lexer.token) + { match lexer.token { KeywordAction => { - if let Some(implementation) = parse_action(lexer, linkage, Some(&container)) { + if let Some(implementation) = + parse_action(lexer, linkage, Some(&container), recovery::ACTION_START) + { impls.push(implementation); } } _ => { lexer.accept_diagnostic(Diagnostic::unexpected_token_found( - "KeywordAction", + KeywordAction.to_string().as_str(), lexer.slice(), lexer.location(), )); @@ -211,15 +220,30 @@ fn parse_interface(lexer: &mut ParseSession) -> (Interface, Vec) while let Identifier = lexer.token { let (name, location) = parse_identifier(lexer).expect("unreachable, already matched here"); extensions.push(Identifier { name, location }); - lexer.try_consume(KeywordComma); + + if lexer.try_consume(KeywordComma) { + continue; + } + + if lexer.token == Identifier { + lexer.accept_diagnostic(Diagnostic::missing_token( + KeywordComma.to_string().as_str(), + lexer.location(), + )); + } } } loop { match lexer.token { KeywordMethod => { - if let Some((method, imp)) = - parse_method(lexer, &name, DeclarationKind::Abstract, LinkageType::Internal, false) - { + if let Some((method, imp)) = parse_method( + lexer, + &name, + DeclarationKind::Abstract, + LinkageType::Internal, + false, + &[KeywordEndInterface], + ) { // This is temporary? At some point we'll support them but for now it's a diagnostic if !imp.statements.is_empty() { lexer.accept_diagnostic( @@ -235,7 +259,8 @@ fn parse_interface(lexer: &mut ParseSession) -> (Interface, Vec) } KeywordPropertyGet | KeywordPropertySet => { - if let Some((ident, property_implementation)) = parse_property(lexer, &[KeywordEndInterface]) + if let Some((ident, property_implementation)) = + parse_property(lexer, &[KeywordEndInterface], recovery::MEMBER_START) { if !property_implementation.body.is_empty() { lexer.accept_diagnostic( @@ -302,177 +327,200 @@ fn parse_pou( KeywordEndFunctionBlock, KeywordEndClass, ]; - let result = parse_any_in_region(lexer, closing_tokens.clone(), |lexer| { - // parse polymorphism mode for all pou types - // check in validator if pou type allows polymorphism - let poly_mode = parse_polymorphism_mode(lexer, &kind); - - let (name, name_location) = - parse_identifier(lexer).unwrap_or_else(|| ("".to_string(), SourceLocation::undefined())); // parse POU name - - let generics = parse_generics(lexer); - - with_scope(lexer, name.clone(), |lexer| { - // TODO: Parse USING directives - let super_class = parse_super_class(lexer); - let interfaces = parse_interface_declarations(lexer); - - // parse an optional return type - // classes do not have a return type (check in validator) - let return_type = parse_return_type(lexer); - - // parse variable declarations. note that var in/out/inout - // blocks are not allowed inside of class declarations. - let mut variable_blocks = vec![]; - let allowed_var_types = [ - KeywordVar, - KeywordVarInput, - KeywordVarOutput, - KeywordVarInOut, - KeywordVarTemp, - KeywordVarExternal, - ]; - while allowed_var_types.contains(&lexer.token) { - variable_blocks.push(parse_variable_block(lexer, LinkageType::Internal)); - } + let top_level_recovery_tokens = recovery::TOP_LEVEL_START; + let result = + parse_any_in_region_until(lexer, closing_tokens.clone(), top_level_recovery_tokens, |lexer| { + // parse polymorphism mode for all pou types + // check in validator if pou type allows polymorphism + let poly_mode = parse_polymorphism_mode(lexer, &kind); + + let (name, name_location) = + parse_identifier(lexer).unwrap_or_else(|| ("".to_string(), SourceLocation::undefined())); // parse POU name + + let generics = parse_generics(lexer); + + with_scope(lexer, name.clone(), |lexer| { + // TODO: Parse USING directives + let super_class = parse_super_class(lexer); + let interfaces = parse_interface_declarations(lexer); + + // parse an optional return type + // classes do not have a return type (check in validator) + let return_type = parse_return_type(lexer); + + // parse variable declarations. note that var in/out/inout + // blocks are not allowed inside of class declarations. + let mut variable_blocks = vec![]; + let allowed_var_types = [ + KeywordVar, + KeywordVarInput, + KeywordVarOutput, + KeywordVarInOut, + KeywordVarTemp, + KeywordVarExternal, + ]; + while allowed_var_types.contains(&lexer.token) { + variable_blocks.push(parse_variable_block(lexer, LinkageType::Internal)); + } - let mut impl_pous = Vec::new(); - let mut properties = FxIndexMap::default(); - let mut implementations = Vec::new(); - - // classes and function blocks can have methods. methods consist of a Pou part - // and an implementation part. That's why we get another (Pou, Implementation) - // tuple out of parse_method() that has to be added to the list of Pous and - // implementations. Note that function blocks have to start with the method - // declarations before their implementation. - // all other Pous need to be checked in the validator if they can have methods. - while matches!( - lexer.token, - KeywordMethod | KeywordPropertyGet | KeywordPropertySet | PropertyConstant - ) { - match lexer.token { - KeywordMethod => { - if !matches!(kind, PouType::FunctionBlock | PouType::Class | PouType::Program) { - let location = lexer.source_range_factory.create_range(lexer.last_range.clone()); - lexer.accept_diagnostic( - Diagnostic::new(format!("Methods cannot be declared in a {kind}")) - .with_location(location), - ); - } + let mut impl_pous = Vec::new(); + let mut properties = FxIndexMap::default(); + let mut implementations = Vec::new(); + + // classes and function blocks can have methods. methods consist of a Pou part + // and an implementation part. That's why we get another (Pou, Implementation) + // tuple out of parse_method() that has to be added to the list of Pous and + // implementations. Note that function blocks have to start with the method + // declarations before their implementation. + // all other Pous need to be checked in the validator if they can have methods. + while matches!( + lexer.token, + KeywordMethod | KeywordPropertyGet | KeywordPropertySet | PropertyConstant + ) { + match lexer.token { + KeywordMethod => { + if !matches!(kind, PouType::FunctionBlock | PouType::Class | PouType::Program) { + let location = + lexer.source_range_factory.create_range(lexer.last_range.clone()); + lexer.accept_diagnostic( + Diagnostic::new(format!("Methods cannot be declared in a {kind}")) + .with_location(location), + ); + } - if let Some((pou, implementation)) = - parse_method(lexer, &name, DeclarationKind::Concrete, linkage, false) - { - impl_pous.push(pou); - implementations.push(implementation); - } - } - KeywordPropertyGet | KeywordPropertySet => { - if !matches!(kind, PouType::FunctionBlock | PouType::Class | PouType::Program) { - let location = lexer.source_range_factory.create_range(lexer.last_range.clone()); - lexer.accept_diagnostic( - Diagnostic::new(format!("Properties cannot be declared in a {kind}")) - .with_location(location), - ); + if let Some((pou, implementation)) = parse_method( + lexer, + &name, + DeclarationKind::Concrete, + linkage, + false, + &[expected_end_token], + ) { + impl_pous.push(pou); + implementations.push(implementation); + } } + KeywordPropertyGet | KeywordPropertySet => { + if !matches!(kind, PouType::FunctionBlock | PouType::Class | PouType::Program) { + let location = + lexer.source_range_factory.create_range(lexer.last_range.clone()); + lexer.accept_diagnostic( + Diagnostic::new(format!("Properties cannot be declared in a {kind}")) + .with_location(location), + ); + } - if let Some((ident, property_implementation)) = - parse_property(lexer, &[expected_end_token]) - { - insert_property(&mut properties, ident, property_implementation); + if let Some((ident, property_implementation)) = + parse_property(lexer, &[expected_end_token], recovery::MEMBER_START) + { + insert_property(&mut properties, ident, property_implementation); + } } - } - PropertyConstant => { - let pragma_location = lexer.location(); - lexer.advance(); - - match lexer.token { - KeywordMethod => { - if !matches!(kind, PouType::FunctionBlock | PouType::Class | PouType::Program) - { - lexer.accept_diagnostic( - Diagnostic::new(format!("Methods cannot be declared in a {kind}")) + PropertyConstant => { + let pragma_location = lexer.location(); + lexer.advance(); + + match lexer.token { + KeywordMethod => { + if !matches!( + kind, + PouType::FunctionBlock | PouType::Class | PouType::Program + ) { + lexer.accept_diagnostic( + Diagnostic::new(format!( + "Methods cannot be declared in a {kind}" + )) .with_location(pragma_location.clone()), - ); - } - - if let Some((pou, implementation)) = - parse_method(lexer, &name, DeclarationKind::Concrete, linkage, true) - { - impl_pous.push(pou); - implementations.push(implementation); + ); + } + + if let Some((pou, implementation)) = parse_method( + lexer, + &name, + DeclarationKind::Concrete, + linkage, + true, + &[expected_end_token], + ) { + impl_pous.push(pou); + implementations.push(implementation); + } } - } - KeywordPropertyGet | KeywordPropertySet => { - if !matches!(kind, PouType::FunctionBlock | PouType::Class | PouType::Program) - { - lexer.accept_diagnostic( - Diagnostic::new(format!("Properties cannot be declared in a {kind}")) + KeywordPropertyGet | KeywordPropertySet => { + if !matches!( + kind, + PouType::FunctionBlock | PouType::Class | PouType::Program + ) { + lexer.accept_diagnostic( + Diagnostic::new(format!( + "Properties cannot be declared in a {kind}" + )) .with_location(pragma_location.clone()), - ); + ); + } + + lexer.accept_diagnostic(Diagnostic::const_pragma_is_not_allowed( + pragma_location.span(&lexer.location()), + )); + + if let Some((ident, property_implementation)) = + parse_property(lexer, &[expected_end_token], recovery::MEMBER_START) + { + insert_property(&mut properties, ident, property_implementation); + } } - - lexer.accept_diagnostic(Diagnostic::const_pragma_is_not_allowed( - pragma_location.span(&lexer.location()), - )); - - if let Some((ident, property_implementation)) = - parse_property(lexer, &[expected_end_token]) - { - insert_property(&mut properties, ident, property_implementation); + _ => { + lexer.accept_diagnostic(Diagnostic::unexpected_token_found( + KeywordMethod.to_string().as_str(), + lexer.slice(), + lexer.location(), + )); } } - _ => { - lexer.accept_diagnostic(Diagnostic::unexpected_token_found( - "KeywordMethod", - lexer.slice(), - lexer.location(), - )); - } } + _ => unreachable!(), } - _ => unreachable!(), } - } - - // a class may not contain an implementation - // check in validator - implementations.push(parse_implementation( - lexer, - linkage, - kind.clone(), - &name, - &name, - !generics.is_empty(), - name_location.clone(), - )); - let mut pous = vec![Pou { - name, - id: lexer.next_id(), - kind, - variable_blocks, - return_type, - location: lexer.source_range_factory.create_range(start..lexer.range().end), - name_location, - poly_mode, - generics, - linkage, - super_class, - interfaces, - is_const: constant, - properties: properties.into_values().collect(), - }]; - pous.append(&mut impl_pous); + // a class may not contain an implementation + // check in validator + implementations.push(parse_implementation( + lexer, + linkage, + kind.clone(), + &name, + &name, + !generics.is_empty(), + name_location.clone(), + top_level_recovery_tokens, + )); - (pous, implementations) - }) - }); + let mut pous = vec![Pou { + name, + id: lexer.next_id(), + kind, + variable_blocks, + return_type, + location: lexer.source_range_factory.create_range(start..lexer.range().end), + name_location, + poly_mode, + generics, + linkage, + super_class, + interfaces, + is_const: constant, + properties: properties.into_values().collect(), + }]; + pous.append(&mut impl_pous); + + (pous, implementations) + }) + }); //check if we ended on the right end-keyword if closing_tokens.contains(&lexer.last_token) && lexer.last_token != expected_end_token { lexer.accept_diagnostic(Diagnostic::unexpected_token_found( - format!("{expected_end_token:?}").as_str(), + expected_end_token.to_string().as_str(), lexer.slice_region(lexer.last_range.clone()), lexer.source_range_factory.create_range(lexer.last_range.clone()), )); @@ -485,32 +533,59 @@ fn parse_pou( fn parse_generics(lexer: &mut ParseSession) -> Vec { if lexer.try_consume(Token::OperatorLess) { - parse_any_in_region(lexer, vec![Token::OperatorGreater], |lexer| { - let mut generics = vec![]; - loop { - //identifier - if let Some((name, _)) = parse_identifier(lexer) { - lexer.try_consume_or_report(Token::KeywordColon); + parse_any_in_region_until_element_when( + lexer, + vec![Token::OperatorGreater], + &[Token::KeywordColon], + None, + is_generic_parameter_start, + |lexer| { + let mut generics = vec![]; + while !lexer.closes_open_region(&lexer.token) && lexer.token != Token::KeywordColon { + // Parse one generic binding. + if let Some((name, _)) = parse_identifier(lexer) { + lexer.try_consume_or_report(Token::KeywordColon); + + // Parse the generic type nature. + if let Some(nature) = + parse_identifier(lexer).map(|(it, _)| parse_type_nature(lexer, &it)) + { + generics.push(GenericBinding { name, nature }); + } + } - //Expect a type nature - if let Some(nature) = parse_identifier(lexer).map(|(it, _)| parse_type_nature(lexer, &it)) - { - generics.push(GenericBinding { name, nature }); + // Continue at either an explicit comma or the next binding start. + if lexer.try_consume(Token::KeywordComma) { + continue; + } + + if is_generic_parameter_start(lexer) { + lexer.accept_diagnostic(Diagnostic::missing_token( + Token::KeywordComma.to_string().as_str(), + lexer.location(), + )); + continue; + } + + if lexer.token == Token::OperatorGreater { + break; } - } - if !lexer.try_consume(Token::KeywordComma) || lexer.try_consume(Token::OperatorGreater) { break; } - } - generics - }) + generics + }, + ) } else { vec![] } } +fn is_generic_parameter_start(lexer: &ParseSession) -> bool { + lexer.token.is_identifier_like() && matches!(lexer.peek_token(), Token::KeywordColon) +} + /// Parses the comma seperated identifiers after an `IMPLEMENTS` keyword, e.g. `bar` and `baz` in /// `INTERFACE foo IMPLEMENTS bar` fn parse_interface_declarations(lexer: &mut ParseSession) -> Vec { @@ -537,6 +612,13 @@ fn parse_interface_declarations(lexer: &mut ParseSession) -> Vec { Token::Identifier => { let (name, location) = parse_identifier(lexer).expect("Identifier already matched"); declarations.push(Identifier { name, location }); + + if lexer.token == Token::Identifier { + lexer.accept_diagnostic(Diagnostic::missing_token( + KeywordComma.to_string().as_str(), + lexer.location(), + )); + } } Token::KeywordComma => lexer.advance(), @@ -663,8 +745,10 @@ fn parse_method( declaration_kind: DeclarationKind, linkage: LinkageType, constant: bool, + outer_end_tokens: &[Token], ) -> Option<(Pou, Implementation)> { - parse_any_in_region(lexer, vec![KeywordEndMethod], |lexer| { + let recovery_tokens = recovery::combine(recovery::MEMBER_START, outer_end_tokens); + parse_any_in_region_until(lexer, vec![KeywordEndMethod], &recovery_tokens, |lexer| { // Method declarations look like this: // METHOD [AccessModifier] [ABSTRACT|FINAL] [OVERRIDE] [: return_type] // ... @@ -707,6 +791,7 @@ fn parse_method( &call_name, !generics.is_empty(), name_location.clone(), + &recovery_tokens, ); // parse_implementation() will default-initialize the fields it @@ -739,6 +824,7 @@ fn parse_method( fn parse_property( lexer: &mut ParseSession, outer_end_tokens: &[Token], + recovery_tokens: &[Token], ) -> Option<(Identifier, PropertyImplementation)> { let kind_location = lexer.location(); let kind = match lexer.token { @@ -757,7 +843,7 @@ fn parse_property( .with_location(lexer.location()) .with_error_code("E001"), ); - recover_property(lexer, outer_end_tokens); + recover_property(lexer, outer_end_tokens, recovery_tokens); return None; }; @@ -767,7 +853,7 @@ fn parse_property( .with_location(name_location.clone()) .with_error_code("E001"), ); - recover_property(lexer, outer_end_tokens); + recover_property(lexer, outer_end_tokens, recovery_tokens); return None; } @@ -777,18 +863,18 @@ fn parse_property( .with_location(lexer.last_location()) .with_error_code("E001"), ); - recover_property(lexer, outer_end_tokens); + recover_property(lexer, outer_end_tokens, recovery_tokens); return None; } let Some((datatype, initializer)) = parse_data_type_definition(lexer, None) else { - recover_property(lexer, outer_end_tokens); + recover_property(lexer, outer_end_tokens, recovery_tokens); return None; }; if let Some(init) = initializer { lexer.accept_diagnostic(Diagnostic::unexpected_token_found( - format!("{KeywordEndProperty:?}").as_str(), + KeywordEndProperty.to_string().as_str(), "Initializer", init.get_location(), )); @@ -802,7 +888,10 @@ fn parse_property( } let mut body = Vec::new(); - while lexer.token != End && lexer.token != KeywordEndProperty && !outer_end_tokens.contains(&lexer.token) + while lexer.token != End + && lexer.token != KeywordEndProperty + && !outer_end_tokens.contains(&lexer.token) + && !recovery_tokens.contains(&lexer.token) { body.push(parse_control(lexer)); } @@ -812,7 +901,7 @@ fn parse_property( lexer.advance(); } else { lexer.accept_diagnostic(Diagnostic::missing_token( - format!("{KeywordEndProperty:?}").as_str(), + KeywordEndProperty.to_string().as_str(), lexer.location(), )); } @@ -847,8 +936,11 @@ fn insert_property( } } -fn recover_property(lexer: &mut ParseSession, outer_end_tokens: &[Token]) { - while lexer.token != End && lexer.token != KeywordEndProperty && !outer_end_tokens.contains(&lexer.token) +fn recover_property(lexer: &mut ParseSession, outer_end_tokens: &[Token], recovery_tokens: &[Token]) { + while lexer.token != End + && lexer.token != KeywordEndProperty + && !outer_end_tokens.contains(&lexer.token) + && !recovery_tokens.contains(&lexer.token) { lexer.advance(); } @@ -911,9 +1003,10 @@ fn parse_implementation( type_name: &str, generic: bool, name_location: SourceLocation, + recovery_tokens: &[Token], ) -> Implementation { let start = lexer.range().start; - let statements = parse_body_standalone(lexer); + let statements = parse_body_standalone_until(lexer, recovery_tokens); let end_location = lexer.location(); //Location of the current token, which shoudl be the //end token Implementation { @@ -935,12 +1028,13 @@ fn parse_action( lexer: &mut ParseSession, linkage: LinkageType, container: Option<&str>, + recovery_tokens: &[Token], ) -> Option { lexer.advance(); //Consume the Action keyword let closing_tokens = vec![KeywordEndAction, KeywordEndProgram, KeywordEndFunction, KeywordEndFunctionBlock]; - parse_any_in_region(lexer, closing_tokens.clone(), |lexer| { + parse_any_in_region_until(lexer, closing_tokens.clone(), recovery_tokens, |lexer| { let name_or_container = lexer.slice_and_advance(); let (container, name, name_location) = if let Some(container) = container { @@ -966,11 +1060,12 @@ fn parse_action( &container, false, name_location, + recovery_tokens, ); //lets see if we ended on the right END_ keyword if closing_tokens.contains(&lexer.last_token) && lexer.last_token != KeywordEndAction { lexer.accept_diagnostic(Diagnostic::unexpected_token_found( - format!("{KeywordEndAction:?}").as_str(), + KeywordEndAction.to_string().as_str(), lexer.slice(), lexer.location(), )) @@ -983,27 +1078,50 @@ fn parse_action( fn parse_type(lexer: &mut ParseSession, linkage: LinkageType) -> Vec { lexer.advance(); // consume the TYPE - parse_any_in_region(lexer, vec![KeywordEndType], |lexer| { - let mut declarations = vec![]; - while !lexer.closes_open_region(&lexer.token) { - let name = lexer.slice_and_advance(); - let name_location = lexer.last_location(); - lexer.try_consume_or_report(KeywordColon); + parse_any_in_region_until_element_when( + lexer, + vec![KeywordEndType], + &[], + None, + is_type_top_level_recovery_boundary, + |lexer| { + let mut declarations = vec![]; + while !lexer.closes_open_region(&lexer.token) && !is_type_top_level_recovery_boundary(lexer) { + if lexer.token == Identifier && lexer.slice().to_ascii_uppercase().starts_with("END_") { + lexer.accept_diagnostic(Diagnostic::unexpected_token_found( + KeywordEndType.to_string().as_str(), + lexer.slice(), + lexer.location(), + )); + lexer.advance(); + break; + } - let result = parse_full_data_type_definition(lexer, Some(name)); + let name = lexer.slice_and_advance(); + let name_location = lexer.last_location(); + lexer.try_consume_or_report(KeywordColon); - if let Some((DataTypeDeclaration::Definition { data_type, .. }, initializer)) = result { - declarations.push(UserTypeDeclaration { - data_type: *data_type, - initializer, - location: name_location, - scope: lexer.scope.clone(), - linkage, - }); + let result = parse_full_data_type_definition_recovering_when( + lexer, + Some(name), + &[KeywordEndType], + Some(recovery::ElementStart::VariableDeclaration), + is_type_top_level_recovery_boundary, + ); + + if let Some((DataTypeDeclaration::Definition { data_type, .. }, initializer)) = result { + declarations.push(UserTypeDeclaration { + data_type: *data_type, + initializer, + location: name_location, + scope: lexer.scope.clone(), + linkage, + }); + } } - } - declarations - }) + declarations + }, + ) } type DataTypeWithInitializer = (DataTypeDeclaration, Option); @@ -1011,39 +1129,83 @@ type DataTypeWithInitializer = (DataTypeDeclaration, Option); fn parse_full_data_type_definition( lexer: &mut ParseSession, name: Option, +) -> Option { + parse_full_data_type_definition_recovering( + lexer, + name, + &[], + Some(recovery::ElementStart::VariableDeclaration), + ) +} + +fn parse_full_data_type_definition_recovering( + lexer: &mut ParseSession, + name: Option, + recovery_tokens: &[Token], + element_start: Option, +) -> Option { + parse_full_data_type_definition_recovering_when( + lexer, + name, + recovery_tokens, + element_start, + never_recovery_boundary, + ) +} + +fn parse_full_data_type_definition_recovering_when( + lexer: &mut ParseSession, + name: Option, + recovery_tokens: &[Token], + element_start: Option, + is_recovery_boundary: fn(&ParseSession) -> bool, ) -> Option { let end_keyword = if lexer.token == KeywordStruct { KeywordEndStruct } else { KeywordSemicolon }; - let parsed_datatype = parse_any_in_region(lexer, vec![end_keyword], |lexer| { - let sized = lexer.try_consume(PropertySized); - if lexer.try_consume(KeywordDotDotDot) { - Some(( - DataTypeDeclaration::Definition { - data_type: Box::new(DataType::VarArgs { referenced_type: None, sized }), - location: lexer.last_location(), - scope: lexer.scope.clone(), - }, - None, - )) - } else { - parse_data_type_definition(lexer, name).map(|(type_def, initializer)| { - if lexer.try_consume(KeywordDotDotDot) { - ( - DataTypeDeclaration::Definition { - data_type: Box::new(DataType::VarArgs { - referenced_type: Some(Box::new(type_def)), - sized, - }), - location: lexer.last_location(), - scope: lexer.scope.clone(), - }, - None, - ) - } else { - (type_def, initializer) - } - }) - } - }); + let parsed_datatype = parse_any_in_region_until_element_when( + lexer, + vec![end_keyword], + recovery_tokens, + element_start, + is_recovery_boundary, + |lexer| { + let sized = lexer.try_consume(PropertySized); + if lexer.try_consume(KeywordDotDotDot) { + Some(( + DataTypeDeclaration::Definition { + data_type: Box::new(DataType::VarArgs { referenced_type: None, sized }), + location: lexer.last_location(), + scope: lexer.scope.clone(), + }, + None, + )) + } else { + parse_data_type_definition_recovering_when( + lexer, + name, + recovery_tokens, + element_start, + is_recovery_boundary, + ) + .map(|(type_def, initializer)| { + if lexer.try_consume(KeywordDotDotDot) { + ( + DataTypeDeclaration::Definition { + data_type: Box::new(DataType::VarArgs { + referenced_type: Some(Box::new(type_def)), + sized, + }), + location: lexer.last_location(), + scope: lexer.scope.clone(), + }, + None, + ) + } else { + (type_def, initializer) + } + }) + } + }, + ); // The standard allows semicolons at the end of an `END_STRUCT` keyword, hence if we parsed // a struct, try to also consume a semicolon if it exists @@ -1058,6 +1220,24 @@ fn parse_full_data_type_definition( fn parse_data_type_definition( lexer: &mut ParseSession, name: Option, +) -> Option { + parse_data_type_definition_until(lexer, name, &[]) +} + +fn parse_data_type_definition_until( + lexer: &mut ParseSession, + name: Option, + recovery_tokens: &[Token], +) -> Option { + parse_data_type_definition_recovering_when(lexer, name, recovery_tokens, None, never_recovery_boundary) +} + +fn parse_data_type_definition_recovering_when( + lexer: &mut ParseSession, + name: Option, + recovery_tokens: &[Token], + element_start: Option, + is_recovery_boundary: fn(&ParseSession) -> bool, ) -> Option { let start = lexer.location(); if lexer.try_consume(KeywordStruct) { @@ -1072,7 +1252,7 @@ fn parse_data_type_definition( None, )) } else if lexer.try_consume(KeywordArray) { - parse_array_type_definition(lexer, name) + parse_array_type_definition(lexer, name, recovery_tokens, element_start, is_recovery_boundary) } else if lexer.try_consume(KeywordPointer) { let start_pos = lexer.last_range.start; //Report wrong keyword @@ -1089,29 +1269,66 @@ fn parse_data_type_definition( lexer.advance(); } - parse_pointer_definition(lexer, name, start_pos, None, false, false) + parse_pointer_definition_recovering_when( + lexer, + name, + start_pos, + None, + false, + false, + recovery_tokens, + element_start, + is_recovery_boundary, + ) } else if lexer.try_consume(KeywordRef) { - parse_pointer_definition(lexer, name, lexer.last_range.start, None, true, false) + parse_pointer_definition_recovering_when( + lexer, + name, + lexer.last_range.start, + None, + true, + false, + recovery_tokens, + element_start, + is_recovery_boundary, + ) } else if lexer.try_consume(KeywordReferenceTo) { - parse_pointer_definition( + parse_pointer_definition_recovering_when( lexer, name, lexer.last_range.start, Some(AutoDerefType::Reference), true, false, + recovery_tokens, + element_start, + is_recovery_boundary, ) } else if lexer.try_consume(KeywordParensOpen) { - parse_enum_type_definition(lexer, name) + parse_enum_type_definition_until(lexer, name, recovery_tokens, is_recovery_boundary) } else if lexer.token == KeywordString || lexer.token == KeywordWideString { - parse_string_type_definition(lexer, name) + parse_string_type_definition_recovering_when( + lexer, + name, + recovery_tokens, + element_start, + is_recovery_boundary, + ) } else if lexer.token == Identifier { parse_type_reference_type_definition(lexer, name) } else { + if recovery_tokens.contains(&lexer.token) + || element_start.is_some_and(|element_start| recovery::at_element_start(lexer, element_start)) + || is_recovery_boundary(lexer) + { + lexer.accept_diagnostic(Diagnostic::missing_token("DataTypeDefinition", lexer.location())); + return None; + } + //no datatype? lexer.accept_diagnostic(Diagnostic::unexpected_token_found( "DataTypeDefinition", - format!("{:?}", lexer.token).as_str(), + lexer.token.to_string().as_str(), lexer.location(), )); None @@ -1126,7 +1343,46 @@ fn parse_pointer_definition( type_safe: bool, is_function: bool, ) -> Option<(DataTypeDeclaration, Option)> { - parse_data_type_definition(lexer, None).map(|(decl, initializer)| { + parse_pointer_definition_recovering_when( + lexer, + name, + start_pos, + auto_deref, + type_safe, + is_function, + &[], + None, + never_recovery_boundary, + ) +} + +fn parse_pointer_definition_recovering_when( + lexer: &mut ParseSession, + name: Option, + start_pos: usize, + auto_deref: Option, + type_safe: bool, + is_function: bool, + recovery_tokens: &[Token], + element_start: Option, + is_recovery_boundary: fn(&ParseSession) -> bool, +) -> Option<(DataTypeDeclaration, Option)> { + if recovery_tokens.contains(&lexer.token) + || element_start.is_some_and(|element_start| recovery::at_element_start(lexer, element_start)) + || is_recovery_boundary(lexer) + { + lexer.accept_diagnostic(Diagnostic::missing_token("DataTypeDefinition", lexer.location())); + return None; + } + + parse_data_type_definition_recovering_when( + lexer, + None, + recovery_tokens, + element_start, + is_recovery_boundary, + ) + .map(|(decl, initializer)| { ( DataTypeDeclaration::Definition { data_type: Box::new(DataType::PointerType { @@ -1234,54 +1490,75 @@ fn parse_type_reference_type_definition( } } -fn parse_string_size_expression(lexer: &mut ParseSession) -> Option { +fn parse_string_size_expression_recovering_when( + lexer: &mut ParseSession, + recovery_tokens: &[Token], + element_start: Option, + is_recovery_boundary: fn(&ParseSession) -> bool, +) -> Option { let opening_token = lexer.token; if lexer.try_consume(KeywordSquareParensOpen) || lexer.try_consume(KeywordParensOpen) { let opening_location = lexer.range().start; let closing_tokens = vec![KeywordSquareParensClose, KeywordParensClose]; - parse_any_in_region(lexer, closing_tokens, |lexer| { - let size_expr = parse_expression(lexer); - let error_range = lexer.source_range_factory.create_range(opening_location..lexer.range().end); - - // Don't emit warnings if this looks like an enum (will be caught by validation). - // e.g. `TYPE attemptAtStringEnum : STRING (a := 1, b := 2);` should not warn about parentheses. - let is_enum_like = matches!(size_expr.get_stmt(), AstStatement::ExpressionList(_)); - - if (opening_token == KeywordParensOpen && lexer.token == KeywordSquareParensClose) - || (opening_token == KeywordSquareParensOpen && lexer.token == KeywordParensClose) - { - lexer.accept_diagnostic( - Diagnostic::new("Mismatched types of parentheses around string size expression") - .with_location(error_range) - .with_error_code("E009"), - ); - } else if !is_enum_like - && (opening_token == KeywordParensOpen || lexer.token == KeywordParensClose) - { - lexer.accept_diagnostic(Diagnostic::new( + parse_any_in_region_until_element_when( + lexer, + closing_tokens, + recovery_tokens, + element_start, + is_recovery_boundary, + |lexer| { + let size_expr = parse_expression(lexer); + let error_range = + lexer.source_range_factory.create_range(opening_location..lexer.range().end); + + // Don't emit warnings if this looks like an enum (will be caught by validation). + // e.g. `TYPE attemptAtStringEnum : STRING (a := 1, b := 2);` should not warn about parentheses. + let is_enum_like = matches!(size_expr.get_stmt(), AstStatement::ExpressionList(_)); + + if (opening_token == KeywordParensOpen && lexer.token == KeywordSquareParensClose) + || (opening_token == KeywordSquareParensOpen && lexer.token == KeywordParensClose) + { + lexer.accept_diagnostic( + Diagnostic::new("Mismatched types of parentheses around string size expression") + .with_location(error_range) + .with_error_code("E009"), + ); + } else if !is_enum_like + && (opening_token == KeywordParensOpen || lexer.token == KeywordParensClose) + { + lexer.accept_diagnostic(Diagnostic::new( "Unusual type of parentheses around string size expression, consider using square parentheses '[]'"). with_location(error_range) .with_error_code("E014") ); - } + } - Some(size_expr) - }) + Some(size_expr) + }, + ) } else { None } } -fn parse_string_type_definition( +fn parse_string_type_definition_recovering_when( lexer: &mut ParseSession, name: Option, + recovery_tokens: &[Token], + element_start: Option, + is_recovery_boundary: fn(&ParseSession) -> bool, ) -> Option<(DataTypeDeclaration, Option)> { let text = lexer.slice().to_string(); let start = lexer.range().start; let is_wide = lexer.token == KeywordWideString; lexer.advance(); - let size = parse_string_size_expression(lexer); + let size = parse_string_size_expression_recovering_when( + lexer, + recovery_tokens, + element_start, + is_recovery_boundary, + ); let end = lexer.last_range.end; let location = lexer.source_range_factory.create_range(start..end); @@ -1327,13 +1604,22 @@ fn parse_string_type_definition( )) } -fn parse_enum_type_definition( +fn parse_enum_type_definition_until( lexer: &mut ParseSession, name: Option, + recovery_tokens: &[Token], + is_recovery_boundary: fn(&ParseSession) -> bool, ) -> Option<(DataTypeDeclaration, Option)> { let start = lexer.last_location(); - let elements = parse_any_in_region(lexer, vec![KeywordParensClose], parse_enum_elements)?; + let elements = parse_any_in_region_until_element_when( + lexer, + vec![KeywordParensClose], + recovery_tokens, + None, + is_recovery_boundary, + parse_enum_elements, + )?; // Check for Codesys-style type specification after the enum list // TYPE COLOR : (...) DWORD; let numeric_type = @@ -1364,13 +1650,26 @@ fn parse_enum_elements(lexer: &mut ParseSession) -> Option { let element = parse_enum_element(lexer)?; elements.push(element); - if !lexer.try_consume(KeywordComma) { - break; + if lexer.try_consume(KeywordComma) { + if lexer.closes_open_region(&lexer.token) { + break; + } + continue; } if lexer.closes_open_region(&lexer.token) { break; } + + if lexer.token.is_identifier_like() { + lexer.accept_diagnostic(Diagnostic::missing_token( + KeywordComma.to_string().as_str(), + lexer.location(), + )); + continue; + } + + break; } // Handle empty enum case (no elements parsed) @@ -1406,23 +1705,51 @@ fn parse_enum_element(lexer: &mut ParseSession) -> Option { fn parse_array_type_definition( lexer: &mut ParseSession, name: Option, + recovery_tokens: &[Token], + element_start: Option, + is_recovery_boundary: fn(&ParseSession) -> bool, ) -> Option<(DataTypeDeclaration, Option)> { let start = lexer.last_range.start; - let range = parse_any_in_region(lexer, vec![KeywordOf], |lexer| { - // Parse Array range - - expect_token!(lexer, KeywordSquareParensOpen, None); - lexer.advance(); + let range_recovery_tokens = recovery::combine(recovery_tokens, &[KeywordOf]); + let range = parse_any_in_region_until_element_when( + lexer, + vec![KeywordOf], + recovery_tokens, + element_start, + is_recovery_boundary, + |lexer| { + // Parse Array range + + expect_token!(lexer, KeywordSquareParensOpen, None); + lexer.advance(); - let range_statement = parse_expression(lexer); + let range_statement = parse_any_in_region_until_element_when( + lexer, + vec![KeywordSquareParensClose], + &range_recovery_tokens, + element_start, + is_recovery_boundary, + parse_expression, + ); - expect_token!(lexer, KeywordSquareParensClose, None); - lexer.advance(); + Some(range_statement) + }, + )?; - Some(range_statement) - })?; + if recovery_tokens.contains(&lexer.token) + || element_start.is_some_and(|element_start| recovery::at_element_start(lexer, element_start)) + || is_recovery_boundary(lexer) + { + return None; + } - let inner_type_defintion = parse_data_type_definition(lexer, None); + let inner_type_defintion = parse_data_type_definition_recovering_when( + lexer, + None, + recovery_tokens, + element_start, + is_recovery_boundary, + ); inner_type_defintion.map(|(reference, initializer)| { let reference_end = reference.get_location().to_range().map(|it| it.end).unwrap_or(0); let location = lexer.source_range_factory.create_range(start..reference_end); @@ -1472,12 +1799,14 @@ fn parse_array_type_definition( /// parse a body and recovers until the given `end_keywords` fn parse_body_in_region(lexer: &mut ParseSession, end_keywords: Vec) -> Vec { - parse_any_in_region(lexer, end_keywords, parse_body_standalone) + parse_any_in_region_until(lexer, end_keywords, recovery::STATEMENT_BLOCK_BOUNDARY, |lexer| { + parse_body_standalone_until(lexer, recovery::STATEMENT_BLOCK_BOUNDARY) + }) } -fn parse_body_standalone(lexer: &mut ParseSession) -> Vec { +fn parse_body_standalone_until(lexer: &mut ParseSession, recovery_tokens: &[Token]) -> Vec { let mut statements = Vec::new(); - while !lexer.closes_open_region(&lexer.token) { + while !lexer.closes_open_region(&lexer.token) && !recovery_tokens.contains(&lexer.token) { statements.push(parse_control(lexer)); } statements @@ -1510,17 +1839,80 @@ pub fn parse_any_in_region T>( closing_tokens: Vec, parse_fn: F, ) -> T { + parse_any_in_region_until(lexer, closing_tokens, &[], parse_fn) +} + +pub fn parse_any_in_region_until T>( + lexer: &mut ParseSession, + closing_tokens: Vec, + recovery_tokens: &[Token], + parse_fn: F, +) -> T { + parse_any_in_region_until_element(lexer, closing_tokens, recovery_tokens, None, parse_fn) +} + +pub fn parse_any_in_region_until_element T>( + lexer: &mut ParseSession, + closing_tokens: Vec, + recovery_tokens: &[Token], + element_start: Option, + parse_fn: F, +) -> T { + parse_any_in_region_until_element_when( + lexer, + closing_tokens, + recovery_tokens, + element_start, + never_recovery_boundary, + parse_fn, + ) +} + +pub fn parse_any_in_region_until_element_when( + lexer: &mut ParseSession, + closing_tokens: Vec, + recovery_tokens: &[Token], + element_start: Option, + is_recovery_boundary: fn(&ParseSession) -> bool, + parse_fn: F, +) -> T +where + F: FnOnce(&mut ParseSession) -> T, +{ lexer.enter_region(closing_tokens); let result = parse_fn(lexer); // try to recover by eating everything until // we believe the parser is able to continue - lexer.recover_until_close(); - lexer.close_region(); + let reached_recovery_boundary = recovery_tokens.contains(&lexer.token) + || element_start.is_some_and(|element_start| recovery::at_element_start(lexer, element_start)); + let reached_recovery_boundary = reached_recovery_boundary || is_recovery_boundary(lexer); + + if reached_recovery_boundary { + if let Some(expected_tokens) = lexer.closing_keywords.pop() { + let expected_token = expected_tokens.first().unwrap_or(&Token::End); + lexer.accept_diagnostic(Diagnostic::missing_token( + expected_token.to_string().as_str(), + lexer.location(), + )); + } + } else { + lexer.recover_until_close(); + lexer.close_region(); + } result } +fn is_type_top_level_recovery_boundary(lexer: &ParseSession) -> bool { + recovery::TOP_LEVEL_START.contains(&lexer.token) + && !lexer.token_appears_before(KeywordEndType, recovery::TOP_LEVEL_START) +} + +fn never_recovery_boundary(_: &ParseSession) -> bool { + false +} + fn parse_reference(lexer: &mut ParseSession) -> AstNode { if let Some(statement) = expressions_parser::parse_call_statement(lexer) { statement @@ -1577,7 +1969,12 @@ fn parse_variable_block(lexer: &mut ParseSession, linkage: LinkageType) -> Varia let access = parse_access_modifier(lexer); - let mut variables = parse_any_in_region(lexer, vec![KeywordEndVar], parse_variable_list); + let mut variables = parse_any_in_region_until( + lexer, + vec![KeywordEndVar], + recovery::VARIABLE_BLOCK_BOUNDARY, + parse_variable_list, + ); if constant && !matches!(variable_block_type, VariableBlockType::External) { // sneak in the DefaultValue-Statements if no initializers were defined @@ -1599,13 +1996,19 @@ fn parse_variable_list(lexer: &mut ParseSession) -> Vec { } fn parse_config_variables(lexer: &mut ParseSession) -> Vec { - parse_any_in_region(lexer, vec![KeywordEndVar], |lexer| { + parse_any_in_region_until(lexer, vec![KeywordEndVar], recovery::TOP_LEVEL_START, |lexer| { lexer.advance(); let mut variables = vec![]; + let config_item_recovery_tokens = recovery::combine(recovery::TOP_LEVEL_START, &[KeywordEndVar]); + while lexer.token == Identifier { - if let Some(configured_var) = - parse_any_in_region(lexer, vec![KeywordSemicolon], try_parse_config_var) - { + if let Some(configured_var) = parse_any_in_region_until_element( + lexer, + vec![KeywordSemicolon], + &config_item_recovery_tokens, + Some(recovery::ElementStart::ConfigVariable), + try_parse_config_var, + ) { variables.push(configured_var); } } @@ -1623,6 +2026,7 @@ fn try_parse_config_var(lexer: &mut ParseSession) -> Option { let HardwareAccess((direction, access_type)) = lexer.token else { lexer.accept_diagnostic(Diagnostic::missing_token("hardware access", lexer.location())); + skip_malformed_config_var_tail(lexer); return None; }; @@ -1630,7 +2034,7 @@ fn try_parse_config_var(lexer: &mut ParseSession) -> Option { if !lexer.try_consume(KeywordColon) { lexer.accept_diagnostic(Diagnostic::missing_token( - format!("{KeywordColon:?}").as_str(), + KeywordColon.to_string().as_str(), lexer.location(), )); } @@ -1638,7 +2042,7 @@ fn try_parse_config_var(lexer: &mut ParseSession) -> Option { parse_data_type_definition(lexer, None).map(|(dt, init)| { if init.is_some() { lexer.accept_diagnostic(Diagnostic::unexpected_token_found( - format!("{KeywordSemicolon:?}").as_str(), + KeywordSemicolon.to_string().as_str(), "Initializer", lexer.last_location().span(&lexer.location()), )) @@ -1647,11 +2051,20 @@ fn try_parse_config_var(lexer: &mut ParseSession) -> Option { }) } +fn skip_malformed_config_var_tail(lexer: &mut ParseSession) { + while !matches!(lexer.token, KeywordSemicolon | KeywordEndVar | Token::End) + && !recovery::TOP_LEVEL_START.contains(&lexer.token) + && !recovery::at_element_start(lexer, recovery::ElementStart::ConfigVariable) + { + lexer.advance(); + } +} + fn parse_aliasing(lexer: &mut ParseSession, names: &(String, Range)) -> Option { let reference = parse_reference(lexer); if !lexer.try_consume(KeywordColon) { lexer.accept_diagnostic(Diagnostic::missing_token( - format!("{KeywordColon:?}").as_str(), + KeywordColon.to_string().as_str(), lexer.location(), )); } @@ -1660,7 +2073,7 @@ fn parse_aliasing(lexer: &mut ParseSession, names: &(String, Range)) -> O let datatype = parse_pointer_definition(lexer, None, *start, Some(AutoDerefType::Alias), true, false); if !lexer.try_consume(KeywordSemicolon) { lexer.accept_diagnostic(Diagnostic::missing_token( - format!("{KeywordSemicolon:?}").as_str(), + KeywordSemicolon.to_string().as_str(), lexer.location(), )); } @@ -1690,10 +2103,14 @@ fn parse_variable_line(lexer: &mut ParseSession) -> Vec { break; } + if var_names.len() == 1 && is_missing_colon_before_type_name(lexer) { + break; + } + if !lexer.try_consume(KeywordComma) { let next_token_start = lexer.range().start; lexer.accept_diagnostic(Diagnostic::missing_token( - format!("{KeywordColon:?} or {KeywordComma:?}").as_str(), + format!("{KeywordColon} or {KeywordComma}").as_str(), lexer.source_range_factory.create_range(identifier_end..next_token_start), )); } @@ -1726,7 +2143,7 @@ fn parse_variable_line(lexer: &mut ParseSession) -> Vec { // colon has to come before the data type if !lexer.try_consume(KeywordColon) { lexer.accept_diagnostic(Diagnostic::missing_token( - format!("{KeywordColon:?}").as_str(), + KeywordColon.to_string().as_str(), lexer.location(), )); } @@ -1768,6 +2185,19 @@ fn parse_variable_line(lexer: &mut ParseSession) -> Vec { variables } +fn is_missing_colon_before_type_name(lexer: &ParseSession) -> bool { + lexer.token.is_identifier_like() + && matches!( + lexer.peek_token(), + KeywordSemicolon + | KeywordAssignment + | KeywordReferenceAssignment + | KeywordParensOpen + | KeywordSquareParensOpen + | KeywordDot + ) +} + fn parse_hardware_access( lexer: &mut ParseSession, hardware_access_type: HardwareAccessType, diff --git a/src/parser/control_parser.rs b/src/parser/control_parser.rs index ad7e49e9486..bc07c917a81 100644 --- a/src/parser/control_parser.rs +++ b/src/parser/control_parser.rs @@ -1,5 +1,5 @@ use plc_ast::{ - ast::{AstFactory, AstNode, AstStatement}, + ast::{AstFactory, AstNode}, control_statements::{CaseStatement, ConditionalBlock, ForLoopStatement, IfStatement, LoopStatement}, }; use plc_diagnostics::diagnostics::Diagnostic; @@ -7,10 +7,11 @@ use plc_diagnostics::diagnostics::Diagnostic; // Copyright (c) 2020 Ghaith Hachem and Mathias Rieder use crate::{ expect_token, - lexer::Token::*, - parser::{parse_any_in_region, parse_body_in_region}, + lexer::Token::{self, *}, + parser::{parse_any_in_region_until_element_when, parse_body_in_region}, }; +use super::recovery; use super::ParseSession; use super::{parse_expression, parse_reference, parse_statement}; @@ -53,12 +54,7 @@ fn parse_if_statement(lexer: &mut ParseSession) -> AstNode { while lexer.last_token == KeywordElseIf || lexer.last_token == KeywordIf { let condition = parse_expression(lexer); - expect_token!( - lexer, - KeywordThen, - AstFactory::create_empty_statement(lexer.location(), lexer.next_id()) - ); - lexer.advance(); + lexer.try_consume_or_report(KeywordThen); let condition_block = ConditionalBlock { condition: Box::new(condition), @@ -97,8 +93,18 @@ fn parse_for_statement(lexer: &mut ParseSession) -> AstNode { lexer.advance(); let start_expression = parse_expression(lexer); - expect_token!(lexer, KeywordTo, AstFactory::create_empty_statement(lexer.location(), lexer.next_id())); - lexer.advance(); + if !lexer.try_consume(KeywordTo) { + if lexer.token == Identifier && lexer.peek_token() == Identifier { + lexer.accept_diagnostic(Diagnostic::unexpected_token_found( + KeywordTo.to_string().as_str(), + lexer.slice(), + lexer.location(), + )); + return AstFactory::create_empty_statement(lexer.location(), lexer.next_id()); + } + + lexer.accept_diagnostic(Diagnostic::missing_token(KeywordTo.to_string().as_str(), lexer.location())); + } let end_expression = parse_expression(lexer); let step = if lexer.token == KeywordBy { @@ -150,7 +156,9 @@ fn parse_repeat_statement(lexer: &mut ParseSession) -> AstNode { let body = parse_body_in_region(lexer, vec![KeywordUntil, KeywordEndRepeat]); //UNTIL let condition = if lexer.last_token == KeywordUntil { - parse_any_in_region(lexer, vec![KeywordEndRepeat], parse_expression) + let condition = parse_expression(lexer); + lexer.try_consume_or_report(KeywordEndRepeat); + condition } else { AstFactory::create_empty_statement(lexer.location(), lexer.next_id()) }; @@ -169,47 +177,20 @@ fn parse_case_statement(lexer: &mut ParseSession) -> AstNode { let selector = parse_expression(lexer); - expect_token!(lexer, KeywordOf, AstFactory::create_empty_statement(lexer.location(), lexer.next_id())); - - lexer.advance(); + lexer.try_consume_or_report(KeywordOf); let mut case_blocks = Vec::new(); - if lexer.token != KeywordEndCase && lexer.token != KeywordElse { - let body = parse_body_in_region(lexer, vec![KeywordEndCase, KeywordElse]); - - let mut current_condition = None; - let mut current_body = vec![]; - for statement in body { - if let AstNode { stmt: AstStatement::CaseCondition(condition), .. } = statement { - if let Some(condition) = current_condition { - let block = ConditionalBlock { condition, body: current_body }; - case_blocks.push(block); - current_body = vec![]; - } - current_condition = Some(condition); - } else { - //If no current condition is available, log a diagnostic and add an empty condition - if current_condition.is_none() { - lexer.accept_diagnostic( - Diagnostic::new("Missing Case-Condition") - .with_error_code("E012") - .with_location(lexer.location()), - ); - current_condition = - Some(Box::new(AstFactory::create_empty_statement(lexer.location(), lexer.next_id()))); - } - current_body.push(statement); - } - } - if let Some(condition) = current_condition { - let block = ConditionalBlock { condition, body: current_body }; - case_blocks.push(block); - } + + while !lexer.closes_open_region(&lexer.token) && !is_case_block_boundary(lexer) { + let condition = parse_case_condition(lexer); + let body = parse_case_selection_body(lexer); + case_blocks.push(ConditionalBlock { condition, body }); } - let else_block = if lexer.last_token == KeywordElse { + let else_block = if lexer.try_consume(KeywordElse) { parse_body_in_region(lexer, vec![KeywordEndCase]) } else { + lexer.try_consume_or_report(KeywordEndCase); vec![] }; @@ -226,3 +207,109 @@ fn parse_case_statement(lexer: &mut ParseSession) -> AstNode { lexer.next_id(), ) } + +fn parse_case_condition(lexer: &mut ParseSession) -> Box { + if lexer.try_consume(KeywordColon) { + return Box::new(AstFactory::create_empty_statement(lexer.last_location(), lexer.next_id())); + } + + let condition = parse_any_in_region_until_element_when( + lexer, + vec![KeywordColon], + recovery::STATEMENT_BLOCK_BOUNDARY, + None, + is_case_condition_recovery_boundary, + parse_expression, + ); + + Box::new(condition) +} + +fn parse_case_selection_body(lexer: &mut ParseSession) -> Vec { + let recovery_tokens = + recovery::combine(&[KeywordEndCase, KeywordElse], recovery::STATEMENT_BLOCK_BOUNDARY); + let mut statements = Vec::new(); + while !lexer.closes_open_region(&lexer.token) + && !is_case_block_boundary(lexer) + && lexer.token != KeywordColon + && !is_case_selection_start(lexer) + { + let statement = match lexer.token { + KeywordIf | KeywordFor | KeywordWhile | KeywordRepeat | KeywordCase | KeywordReturn + | KeywordContinue | KeywordExit => parse_control_statement(lexer), + _ => parse_case_body_statement(lexer, &recovery_tokens), + }; + statements.push(statement); + } + + statements +} + +fn parse_case_body_statement(lexer: &mut ParseSession, recovery_tokens: &[Token]) -> AstNode { + let result = parse_any_in_region_until_element_when( + lexer, + vec![KeywordSemicolon, KeywordColon], + recovery_tokens, + None, + is_case_selection_start, + parse_expression, + ); + if lexer.last_token == KeywordColon { + let location = result.location.span(&lexer.last_location()); + AstFactory::create_case_condition(result, location, lexer.next_id()) + } else { + result + } +} + +fn is_case_condition_recovery_boundary(lexer: &ParseSession) -> bool { + is_case_block_boundary(lexer) || is_case_selection_start(lexer) || is_statement_start(lexer) +} + +fn is_case_block_boundary(lexer: &ParseSession) -> bool { + lexer.token == KeywordEndCase + || lexer.token == KeywordElse + || recovery::STATEMENT_BLOCK_BOUNDARY.contains(&lexer.token) +} + +fn is_case_selection_start(lexer: &ParseSession) -> bool { + matches!( + lexer.token, + Identifier + | KeywordPropertyGet + | KeywordPropertySet + | LiteralIntegerHex + | LiteralIntegerOct + | LiteralIntegerBin + | LiteralInteger + | LiteralNull + | LiteralTrue + | LiteralFalse + | LiteralDate + | LiteralDateAndTime + | LiteralTimeOfDay + | LiteralTime + | LiteralString + | LiteralWideString + | TypeCastPrefix + | KeywordParensOpen + ) && lexer.token_appears_before(KeywordColon, &[KeywordSemicolon, KeywordEndCase, KeywordElse]) +} + +fn is_statement_start(lexer: &ParseSession) -> bool { + matches!( + lexer.token, + Identifier + | KeywordPropertyGet + | KeywordPropertySet + | KeywordIf + | KeywordFor + | KeywordWhile + | KeywordRepeat + | KeywordCase + | KeywordReturn + | KeywordContinue + | KeywordExit + | KeywordSuper + ) +} diff --git a/src/parser/expressions_parser.rs b/src/parser/expressions_parser.rs index 70f99720372..dff89703fa3 100644 --- a/src/parser/expressions_parser.rs +++ b/src/parser/expressions_parser.rs @@ -4,7 +4,7 @@ use crate::{ expect_token, lexer::Token::*, lexer::{ParseSession, Token}, - parser::parse_any_in_region, + parser::parse_any_in_region_until_element, }; use core::str::Split; use plc_ast::{ @@ -15,7 +15,7 @@ use plc_diagnostics::diagnostics::Diagnostic; use plc_source::source_location::SourceLocation; use std::{ops::Range, str::FromStr}; -use super::parse_hardware_access; +use super::{parse_hardware_access, recovery}; macro_rules! parse_left_associative_expression { ($lexer: expr, $action : expr, @@ -52,28 +52,74 @@ pub fn parse_expression(lexer: &mut ParseSession) -> AstNode { pub fn parse_expression_list(lexer: &mut ParseSession) -> AstNode { let start = lexer.location(); let left = parse_range_statement(lexer); - if lexer.token == KeywordComma { - let mut expressions = vec![]; - // this starts an expression list - while lexer.token == KeywordComma { + + let mut expressions = vec![]; + loop { + if lexer.token == KeywordComma { lexer.advance(); if !lexer.closes_open_region(&lexer.token) { expressions.push(parse_range_statement(lexer)); } - } - // we may have parsed no additional expression because of trailing comma - if !expressions.is_empty() { - expressions.insert(0, left); - return AstFactory::create_expression_list( - expressions, - start.span(&lexer.last_location()), - lexer.next_id(), - ); + } else if lexer.closes_open_region(&lexer.token) || !is_expression_list_recovery_start(lexer) { + break; + } else { + lexer.accept_diagnostic(Diagnostic::missing_token( + KeywordComma.to_string().as_str(), + lexer.location(), + )); + expressions.push(parse_range_statement(lexer)); } } + + // We may have parsed no additional expression because of a trailing comma. + if !expressions.is_empty() { + expressions.insert(0, left); + return AstFactory::create_expression_list( + expressions, + start.span(&lexer.last_location()), + lexer.next_id(), + ); + } + left } +fn is_expression_list_recovery_start(lexer: &ParseSession) -> bool { + let in_delimited_expression_list = lexer.closing_keywords.last().is_some_and(|tokens| { + tokens.contains(&KeywordParensClose) || tokens.contains(&KeywordSquareParensClose) + }); + + in_delimited_expression_list + && starts_expression(&lexer.token) + && !recovery::at_element_start(lexer, recovery::ElementStart::VariableDeclaration) +} + +fn starts_expression(token: &Token) -> bool { + matches!( + token, + OperatorPlus + | OperatorMinus + | KeywordParensOpen + | KeywordSquareParensOpen + | KeywordSuper + | KeywordThis + | LiteralInteger + | LiteralIntegerBin + | LiteralIntegerOct + | LiteralIntegerHex + | LiteralDate + | LiteralTimeOfDay + | LiteralTime + | LiteralDateAndTime + | LiteralString + | LiteralWideString + | LiteralTrue + | LiteralFalse + | LiteralNull + | DirectAccess(_) + ) || token.is_identifier_like() +} + pub(crate) fn parse_range_statement(lexer: &mut ParseSession) -> AstNode { let start = parse_or_expression(lexer); @@ -282,18 +328,23 @@ fn parse_atomic_leaf_expression(lexer: &mut ParseSession<'_>) -> Option } } KeywordParensOpen => { - parse_any_in_region(lexer, vec![KeywordParensClose], |lexer| { - lexer.advance(); // eat KeywordParensOpen - - let start = lexer.last_location(); - let expr = parse_expression(lexer); - - Some(AstFactory::create_paren_expression( - expr, - start.span(&lexer.location()), - lexer.next_id(), - )) - }) + lexer.advance(); // eat KeywordParensOpen + parse_any_in_region_until_element( + lexer, + vec![KeywordParensClose], + recovery::EXPRESSION_REGION_BOUNDARY, + Some(recovery::ElementStart::VariableDeclaration), + |lexer| { + let start = lexer.last_location(); + let expr = parse_expression(lexer); + + Some(AstFactory::create_paren_expression( + expr, + start.span(&lexer.location()), + lexer.next_id(), + )) + }, + ) } token if token.is_identifier_like() => Some(parse_identifier(lexer)), KeywordSuper => { @@ -357,10 +408,29 @@ fn parse_array_literal(lexer: &mut ParseSession) -> Option { let start = lexer.range().start; expect_token!(lexer, KeywordSquareParensOpen, None); lexer.advance(); + + // Let expression-list recovery see the array literal delimiter while preserving the + // array-specific mismatched-delimiter diagnostics below. + lexer.enter_region(vec![KeywordSquareParensClose]); let elements = Some(Box::new(parse_expression(lexer))); + lexer.closing_keywords.pop(); + let end = lexer.range().end; - expect_token!(lexer, KeywordSquareParensClose, None); - lexer.advance(); + if lexer.try_consume(KeywordSquareParensClose) { + // Consumed the expected array literal terminator. + } else if recovery::EXPRESSION_REGION_BOUNDARY.contains(&lexer.token) { + lexer.accept_diagnostic(Diagnostic::missing_token( + KeywordSquareParensClose.to_string().as_str(), + lexer.location(), + )); + } else { + lexer.accept_diagnostic(Diagnostic::unexpected_token_found( + KeywordSquareParensClose.to_string().as_str(), + lexer.slice(), + lexer.location(), + )); + return None; + } Some(AstNode::new_literal( AstLiteral::new_array(elements), @@ -403,14 +473,20 @@ pub fn parse_call_statement(lexer: &mut ParseSession) -> Option { reference_loc.span(&lexer.location()), ) } else { - parse_any_in_region(lexer, vec![KeywordParensClose], |lexer| { - AstFactory::create_call_statement( - reference, - Some(parse_expression_list(lexer)), - lexer.next_id(), - reference_loc.span(&lexer.location()), - ) - }) + parse_any_in_region_until_element( + lexer, + vec![KeywordParensClose], + recovery::EXPRESSION_REGION_BOUNDARY, + Some(recovery::ElementStart::VariableDeclaration), + |lexer| { + AstFactory::create_call_statement( + reference, + Some(parse_expression_list(lexer)), + lexer.next_id(), + reference_loc.span(&lexer.location()), + ) + }, + ) }; // Postfix chain after the call: `foo()[...]`, `foo()^`, or combinations @@ -418,7 +494,13 @@ pub fn parse_call_statement(lexer: &mut ParseSession) -> Option { let mut result = call; loop { if lexer.try_consume(KeywordSquareParensOpen) { - let index = parse_any_in_region(lexer, vec![KeywordSquareParensClose], parse_expression); + let index = parse_any_in_region_until_element( + lexer, + vec![KeywordSquareParensClose], + recovery::EXPRESSION_REGION_BOUNDARY, + Some(recovery::ElementStart::VariableDeclaration), + parse_expression, + ); result = AstFactory::create_index_reference( index, Some(result), @@ -507,8 +589,13 @@ pub fn parse_qualified_reference(lexer: &mut ParseSession) -> Option { } (Some(base), Some(KeywordSquareParensOpen)) => { lexer.advance(); - let index_reference = - parse_any_in_region(lexer, vec![KeywordSquareParensClose], parse_expression); + let index_reference = parse_any_in_region_until_element( + lexer, + vec![KeywordSquareParensClose], + recovery::EXPRESSION_REGION_BOUNDARY, + Some(recovery::ElementStart::VariableDeclaration), + parse_expression, + ); let new_location = base.get_location().span(&lexer.last_location()); current = Some({ AstFactory::create_index_reference( diff --git a/src/parser/recovery.rs b/src/parser/recovery.rs new file mode 100644 index 00000000000..aa4d5814a1b --- /dev/null +++ b/src/parser/recovery.rs @@ -0,0 +1,156 @@ +use crate::lexer::{ + ParseSession, + Token::{self, *}, +}; + +#[derive(Clone, Copy)] +pub enum ElementStart { + ConfigVariable, + VariableDeclaration, +} + +pub const TOP_LEVEL_START: &[Token] = &[ + PropertyExternal, + PropertyConstant, + KeywordInterface, + KeywordVarGlobal, + KeywordVarConfig, + KeywordProgram, + KeywordClass, + KeywordFunction, + KeywordFunctionBlock, + KeywordAction, + KeywordActions, + KeywordType, +]; + +pub const MEMBER_START: &[Token] = &[PropertyConstant, KeywordMethod, KeywordPropertyGet, KeywordPropertySet]; + +pub const ACTION_START: &[Token] = &[KeywordAction, KeywordEndActions]; + +pub const ACTIONS_BLOCK_BOUNDARY: &[Token] = &[ + PropertyExternal, + PropertyConstant, + KeywordInterface, + KeywordVarGlobal, + KeywordVarConfig, + KeywordProgram, + KeywordClass, + KeywordFunction, + KeywordFunctionBlock, + KeywordType, +]; + +pub const STATEMENT_BLOCK_BOUNDARY: &[Token] = &[ + PropertyExternal, + PropertyConstant, + KeywordInterface, + KeywordVarGlobal, + KeywordVarConfig, + KeywordProgram, + KeywordClass, + KeywordFunction, + KeywordFunctionBlock, + KeywordAction, + KeywordActions, + KeywordType, + KeywordEndProgram, + KeywordEndFunction, + KeywordEndFunctionBlock, + KeywordEndMethod, + KeywordEndProperty, + KeywordEndAction, + KeywordEndClass, +]; + +pub const VARIABLE_BLOCK_BOUNDARY: &[Token] = &[ + PropertyExternal, + PropertyConstant, + KeywordInterface, + KeywordVarGlobal, + KeywordVarConfig, + KeywordProgram, + KeywordClass, + KeywordFunction, + KeywordFunctionBlock, + KeywordAction, + KeywordActions, + KeywordType, + KeywordMethod, + KeywordPropertyGet, + KeywordPropertySet, + KeywordEndProgram, + KeywordEndFunction, + KeywordEndFunctionBlock, + KeywordEndMethod, + KeywordEndProperty, + KeywordEndAction, + KeywordEndClass, + KeywordEndInterface, +]; + +pub const EXPRESSION_REGION_BOUNDARY: &[Token] = &[ + KeywordSemicolon, + PropertyExternal, + PropertyConstant, + KeywordInterface, + KeywordVarGlobal, + KeywordVarConfig, + KeywordProgram, + KeywordClass, + KeywordFunction, + KeywordFunctionBlock, + KeywordAction, + KeywordActions, + KeywordType, + KeywordEndProgram, + KeywordEndFunction, + KeywordEndFunctionBlock, + KeywordEndMethod, + KeywordEndProperty, + KeywordEndAction, + KeywordEndClass, +]; + +const CONFIG_VARIABLE_BOUNDARY: &[Token] = &[ + KeywordSemicolon, + KeywordEndVar, + PropertyExternal, + PropertyConstant, + KeywordInterface, + KeywordVarGlobal, + KeywordVarConfig, + KeywordProgram, + KeywordClass, + KeywordFunction, + KeywordFunctionBlock, + KeywordAction, + KeywordActions, + KeywordType, + KeywordEndProgram, + KeywordEndFunction, + KeywordEndFunctionBlock, + KeywordEndMethod, + KeywordEndProperty, + KeywordEndAction, + KeywordEndClass, + End, +]; + +pub fn combine(primary: &[Token], secondary: &[Token]) -> Vec { + let mut tokens = primary.to_vec(); + tokens.extend(secondary); + tokens +} + +pub fn at_element_start(lexer: &ParseSession, element_start: ElementStart) -> bool { + match element_start { + ElementStart::ConfigVariable => { + lexer.token.is_identifier_like() + && lexer.token_appears_before(KeywordAt, CONFIG_VARIABLE_BOUNDARY) + } + ElementStart::VariableDeclaration => { + lexer.token.is_identifier_like() && matches!(lexer.peek_token(), KeywordColon) + } + } +} diff --git a/src/parser/tests/function_parser_tests.rs b/src/parser/tests/function_parser_tests.rs index 7b02ccff8cb..f45e548db8a 100644 --- a/src/parser/tests/function_parser_tests.rs +++ b/src/parser/tests/function_parser_tests.rs @@ -570,8 +570,8 @@ fn reserved_keywords_as_variable_names_are_recognized_as_errors() { "; let (_, diagnostics) = parse_buffered(source); - insta::assert_snapshot!(diagnostics, @r" - error[E007]: Unexpected token: expected KeywordEndVar but found ': DINT; + insta::assert_snapshot!(diagnostics, @" + error[E007]: Unexpected token: expected `END_VAR` but found ': DINT; public : DINT; property : DINT; @@ -588,7 +588,7 @@ fn reserved_keywords_as_variable_names_are_recognized_as_errors() { 8 │ │ end_property : DINT; 9 │ │ end_get : DINT; 10 │ │ end_set : DINT; - │ ╰───────────────────────────────^ Unexpected token: expected KeywordEndVar but found ': DINT; + │ ╰───────────────────────────────^ Unexpected token: expected `END_VAR` but found ': DINT; public : DINT; property : DINT; diff --git a/src/parser/tests/parse_errors.rs b/src/parser/tests/parse_errors.rs index c5509b9547b..3902d6cc7f1 100644 --- a/src/parser/tests/parse_errors.rs +++ b/src/parser/tests/parse_errors.rs @@ -2,4 +2,5 @@ mod parse_error_classes_tests; mod parse_error_containers_tests; mod parse_error_literals_tests; mod parse_error_messages_test; +mod parse_error_recovery_tests; mod parse_error_statements_tests; diff --git a/src/parser/tests/parse_errors/parse_error_containers_tests.rs b/src/parser/tests/parse_errors/parse_error_containers_tests.rs index ce7f93aa24e..6e37ee37c8d 100644 --- a/src/parser/tests/parse_errors/parse_error_containers_tests.rs +++ b/src/parser/tests/parse_errors/parse_error_containers_tests.rs @@ -189,18 +189,18 @@ fn super_is_a_reserved_keyword() { // Related: https://github.com/PLC-lang/rusty/issues/1408 let diagnostics = parse_and_report_parse_errors_buffered(src); - assert_snapshot!(diagnostics, @r" + assert_snapshot!(diagnostics, @" error[E006]: Expected a name for the interface definition but got nothing ┌─ :2:5 │ 2 │ INTERFACE super END_INTERFACE │ ^^^^^^^^^ Expected a name for the interface definition but got nothing - error[E006]: Missing expected Token KeywordEndInterface + error[E006]: Missing expected Token `END_INTERFACE` ┌─ :2:15 │ 2 │ INTERFACE super END_INTERFACE - │ ^^^^^ Missing expected Token KeywordEndInterface + │ ^^^^^ Missing expected Token `END_INTERFACE` error[E007]: Unexpected token: expected StartKeyword but found super ┌─ :2:15 @@ -220,13 +220,13 @@ fn super_is_a_reserved_keyword() { 3 │ PROGRAM super │ ^^^^^ Unexpected token: expected Identifier but found super - error[E007]: Unexpected token: expected KeywordSemicolon but found 'VAR + error[E007]: Unexpected token: expected `;` but found 'VAR super' ┌─ :4:9 │ 4 │ ╭ VAR 5 │ │ super : INT; - │ ╰─────────────────^ Unexpected token: expected KeywordSemicolon but found 'VAR + │ ╰─────────────────^ Unexpected token: expected `;` but found 'VAR super' error[E007]: Unexpected token: expected Literal but found END_VAR @@ -235,26 +235,26 @@ fn super_is_a_reserved_keyword() { 6 │ END_VAR │ ^^^^^^^ Unexpected token: expected Literal but found END_VAR - error[E007]: Unexpected token: expected KeywordSemicolon but found 'END_VAR + error[E007]: Unexpected token: expected `;` but found 'END_VAR METHOD super END_METHOD' ┌─ :6:9 │ 6 │ ╭ END_VAR 7 │ │ METHOD super END_METHOD - │ ╰───────────────────────────────^ Unexpected token: expected KeywordSemicolon but found 'END_VAR + │ ╰───────────────────────────────^ Unexpected token: expected `;` but found 'END_VAR METHOD super END_METHOD' - error[E006]: Missing expected Token [KeywordSemicolon, KeywordColon] + error[E006]: Missing expected Token `;` or `:` ┌─ :8:5 │ 8 │ END_PROGRAM - │ ^^^^^^^^^^^ Missing expected Token [KeywordSemicolon, KeywordColon] + │ ^^^^^^^^^^^ Missing expected Token `;` or `:` - error[E007]: Unexpected token: expected KeywordSemicolon but found 'END_PROGRAM' + error[E007]: Unexpected token: expected `;` but found 'END_PROGRAM' ┌─ :8:5 │ 8 │ END_PROGRAM - │ ^^^^^^^^^^^ Unexpected token: expected KeywordSemicolon but found 'END_PROGRAM' + │ ^^^^^^^^^^^ Unexpected token: expected `;` but found 'END_PROGRAM' "); } @@ -273,18 +273,18 @@ fn this_is_a_reserved_keyword() { // TODO(mhasel): the parser produces a lot of noise for keyword errors, // we need to find a way to handle keywords as identifiers let diagnostics = parse_and_validate_buffered(src); - assert_snapshot!(diagnostics, @r" + assert_snapshot!(diagnostics, @" error[E006]: Expected a name for the interface definition but got nothing ┌─ :2:5 │ 2 │ INTERFACE this END_INTERFACE │ ^^^^^^^^^ Expected a name for the interface definition but got nothing - error[E006]: Missing expected Token KeywordEndInterface + error[E006]: Missing expected Token `END_INTERFACE` ┌─ :2:15 │ 2 │ INTERFACE this END_INTERFACE - │ ^^^^ Missing expected Token KeywordEndInterface + │ ^^^^ Missing expected Token `END_INTERFACE` error[E007]: Unexpected token: expected StartKeyword but found this ┌─ :2:15 @@ -304,13 +304,13 @@ fn this_is_a_reserved_keyword() { 3 │ PROGRAM this │ ^^^^ Unexpected token: expected Identifier but found this - error[E007]: Unexpected token: expected KeywordSemicolon but found 'VAR + error[E007]: Unexpected token: expected `;` but found 'VAR this' ┌─ :4:9 │ 4 │ ╭ VAR 5 │ │ this : INT; - │ ╰────────────────^ Unexpected token: expected KeywordSemicolon but found 'VAR + │ ╰────────────────^ Unexpected token: expected `;` but found 'VAR this' error[E007]: Unexpected token: expected Literal but found END_VAR @@ -319,26 +319,26 @@ fn this_is_a_reserved_keyword() { 6 │ END_VAR │ ^^^^^^^ Unexpected token: expected Literal but found END_VAR - error[E007]: Unexpected token: expected KeywordSemicolon but found 'END_VAR + error[E007]: Unexpected token: expected `;` but found 'END_VAR METHOD this END_METHOD' ┌─ :6:9 │ 6 │ ╭ END_VAR 7 │ │ METHOD this END_METHOD - │ ╰──────────────────────────────^ Unexpected token: expected KeywordSemicolon but found 'END_VAR + │ ╰──────────────────────────────^ Unexpected token: expected `;` but found 'END_VAR METHOD this END_METHOD' - error[E006]: Missing expected Token [KeywordSemicolon, KeywordColon] + error[E006]: Missing expected Token `;` or `:` ┌─ :8:5 │ 8 │ END_PROGRAM - │ ^^^^^^^^^^^ Missing expected Token [KeywordSemicolon, KeywordColon] + │ ^^^^^^^^^^^ Missing expected Token `;` or `:` - error[E007]: Unexpected token: expected KeywordSemicolon but found 'END_PROGRAM' + error[E007]: Unexpected token: expected `;` but found 'END_PROGRAM' ┌─ :8:5 │ 8 │ END_PROGRAM - │ ^^^^^^^^^^^ Unexpected token: expected KeywordSemicolon but found 'END_PROGRAM' + │ ^^^^^^^^^^^ Unexpected token: expected `;` but found 'END_PROGRAM' error[E079]: Case condition used outside of case statement! Did you mean to use ';'? ┌─ :3:13 diff --git a/src/parser/tests/parse_errors/parse_error_messages_test.rs b/src/parser/tests/parse_errors/parse_error_messages_test.rs index fbc59b58003..ce0116aaa29 100644 --- a/src/parser/tests/parse_errors/parse_error_messages_test.rs +++ b/src/parser/tests/parse_errors/parse_error_messages_test.rs @@ -107,7 +107,7 @@ fn test_incomplete_var_config_block() { "; let diagnostics = parse_and_validate_buffered(src); - assert_snapshot!(diagnostics, @r" + assert_snapshot!(diagnostics, @" error[E006]: Missing expected Token AT ┌─ :4:26 │ @@ -126,17 +126,17 @@ fn test_incomplete_var_config_block() { 5 │ instance2.bar AT; │ ^ Missing expected Token hardware access - error[E006]: Missing expected Token KeywordColon + error[E006]: Missing expected Token `:` ┌─ :6:40 │ 6 │ instance3.bar AT %IX3.1; - │ ^ Missing expected Token KeywordColon + │ ^ Missing expected Token `:` - error[E007]: Unexpected token: expected DataTypeDefinition but found KeywordSemicolon + error[E007]: Unexpected token: expected DataTypeDefinition but found `;` ┌─ :6:40 │ 6 │ instance3.bar AT %IX3.1; - │ ^ Unexpected token: expected DataTypeDefinition but found KeywordSemicolon + │ ^ Unexpected token: expected DataTypeDefinition but found `;` error[E006]: Missing expected Token AT ┌─ :8:31 @@ -150,19 +150,13 @@ fn test_incomplete_var_config_block() { 8 │ instance5.bar : BOOL; │ ^ Missing expected Token hardware access - error[E007]: Unexpected token: expected KeywordSemicolon but found ': BOOL' - ┌─ :8:31 - │ - 8 │ instance5.bar : BOOL; - │ ^^^^^^ Unexpected token: expected KeywordSemicolon but found ': BOOL' - - error[E007]: Unexpected token: expected KeywordEndVar but found 'AT %IX3.1; + error[E007]: Unexpected token: expected `END_VAR` but found 'AT %IX3.1; %IX3.1;' ┌─ :9:17 │ 9 │ ╭ AT %IX3.1; 10 │ │ %IX3.1; - │ ╰───────────────────────^ Unexpected token: expected KeywordEndVar but found 'AT %IX3.1; + │ ╰───────────────────────^ Unexpected token: expected `END_VAR` but found 'AT %IX3.1; %IX3.1;' error[E101]: Template variable `bar` does not exist diff --git a/src/parser/tests/parse_errors/parse_error_recovery_tests.rs b/src/parser/tests/parse_errors/parse_error_recovery_tests.rs new file mode 100644 index 00000000000..014c425ea19 --- /dev/null +++ b/src/parser/tests/parse_errors/parse_error_recovery_tests.rs @@ -0,0 +1,1544 @@ +use crate::test_utils::tests::parse_buffered; +use insta::assert_snapshot; +use plc_ast::{ + ast::{AstStatement, DataType, GenericBinding, TypeNature}, + control_statements::AstControlStatement, +}; + +#[test] +fn type_with_wrong_end_keyword_recovers_at_next_top_level_declaration() { + let src = r" + TYPE Position: + STRUCT + x: DINT; + END_STRUCT + END_POSITION + + FUNCTION_BLOCK FbA + END_FUNCTION_BLOCK + "; + + let (unit, diagnostics) = parse_buffered(src); + + assert_snapshot!(diagnostics, @" + error[E007]: Unexpected token: expected `END_TYPE` but found END_POSITION + ┌─ :6:9 + │ + 6 │ END_POSITION + │ ^^^^^^^^^^^^ Unexpected token: expected `END_TYPE` but found END_POSITION + + error[E006]: Missing expected Token `END_TYPE` + ┌─ :8:9 + │ + 8 │ FUNCTION_BLOCK FbA + │ ^^^^^^^^^^^^^^ Missing expected Token `END_TYPE` + "); + assert_eq!(unit.pous.len(), 1); + assert_eq!(unit.pous[0].name, "FbA"); +} + +#[test] +fn type_missing_end_type_recovers_at_next_top_level_declaration() { + let src = r" + TYPE Position: + STRUCT + x: DINT; + END_STRUCT + + FUNCTION main + END_FUNCTION + "; + + let (unit, diagnostics) = parse_buffered(src); + + assert_snapshot!(diagnostics, @" + error[E006]: Missing expected Token `END_TYPE` + ┌─ :7:9 + │ + 7 │ FUNCTION main + │ ^^^^^^^^ Missing expected Token `END_TYPE` + "); + assert_eq!(unit.pous.len(), 1); + assert_eq!(unit.pous[0].name, "main"); +} + +#[test] +fn struct_missing_end_recovers_at_next_top_level_declaration() { + let src = r" + TYPE Position: + STRUCT + x: DINT; + + FUNCTION main + END_FUNCTION + "; + + let (unit, diagnostics) = parse_buffered(src); + + assert_snapshot!(diagnostics, @" + error[E006]: Missing expected Token `END_STRUCT` + ┌─ :6:9 + │ + 6 │ FUNCTION main + │ ^^^^^^^^ Missing expected Token `END_STRUCT` + + error[E006]: Missing expected Token `END_TYPE` + ┌─ :6:9 + │ + 6 │ FUNCTION main + │ ^^^^^^^^ Missing expected Token `END_TYPE` + "); + assert_eq!(unit.pous.len(), 1); + assert_eq!(unit.pous[0].name, "main"); +} + +#[test] +fn struct_missing_member_semicolon_recovers_at_next_member() { + let src = r" + TYPE Position: + STRUCT + x: DINT + y: DINT; + END_STRUCT + END_TYPE + "; + + let (unit, diagnostics) = parse_buffered(src); + + assert_snapshot!(diagnostics, @" + error[E006]: Missing expected Token `;` + ┌─ :5:17 + │ + 5 │ y: DINT; + │ ^ Missing expected Token `;` + "); + + let DataType::StructType { variables, .. } = &unit.user_types[0].data_type else { + panic!("expected struct type"); + }; + assert_eq!(variables.len(), 2); + assert_eq!(variables[0].name, "x"); + assert_eq!(variables[1].name, "y"); +} + +#[test] +fn struct_member_missing_colon_before_type_recovers_at_type_name() { + let src = r" + TYPE Position: + STRUCT + x INT; + y: INT; + END_STRUCT + END_TYPE + "; + + let (unit, diagnostics) = parse_buffered(src); + + assert_snapshot!(diagnostics, @" + error[E006]: Missing expected Token `:` + ┌─ :4:19 + │ + 4 │ x INT; + │ ^^^ Missing expected Token `:` + "); + + let DataType::StructType { variables, .. } = &unit.user_types[0].data_type else { + panic!("expected struct type"); + }; + assert_eq!(variables.len(), 2); + assert_eq!(variables[0].name, "x"); + assert_eq!(variables[1].name, "y"); +} + +#[test] +fn enum_missing_close_paren_recovers_at_next_top_level_declaration() { + let src = r" + TYPE Color: + ( + Red, + Green + + FUNCTION main + END_FUNCTION + "; + + let (unit, diagnostics) = parse_buffered(src); + + assert_snapshot!(diagnostics, @" + error[E006]: Missing expected Token `)` + ┌─ :7:9 + │ + 7 │ FUNCTION main + │ ^^^^^^^^ Missing expected Token `)` + + error[E006]: Missing expected Token `;` + ┌─ :7:9 + │ + 7 │ FUNCTION main + │ ^^^^^^^^ Missing expected Token `;` + + error[E006]: Missing expected Token `END_TYPE` + ┌─ :7:9 + │ + 7 │ FUNCTION main + │ ^^^^^^^^ Missing expected Token `END_TYPE` + "); + assert_eq!(unit.pous.len(), 1); + assert_eq!(unit.pous[0].name, "main"); +} + +#[test] +fn enum_missing_comma_recovers_at_next_enum_element() { + let src = r" + TYPE Color: + ( + Red + Green, + Blue + ); + END_TYPE + "; + + let (unit, diagnostics) = parse_buffered(src); + + assert_snapshot!(diagnostics, @" + error[E006]: Missing expected Token `,` + ┌─ :5:17 + │ + 5 │ Green, + │ ^^^^^ Missing expected Token `,` + "); + + let DataType::EnumType { elements, .. } = &unit.user_types[0].data_type else { + panic!("expected enum type"); + }; + let AstStatement::ExpressionList(elements) = elements.get_stmt() else { + panic!("expected enum element list"); + }; + assert_eq!(elements.len(), 3); +} + +#[test] +fn array_missing_of_recovers_at_next_top_level_declaration() { + let src = r" + TYPE Matrix: + ARRAY [1..10] + + FUNCTION main + END_FUNCTION + "; + + let (unit, diagnostics) = parse_buffered(src); + + assert_snapshot!(diagnostics, @" + error[E006]: Missing expected Token `OF` + ┌─ :5:9 + │ + 5 │ FUNCTION main + │ ^^^^^^^^ Missing expected Token `OF` + + error[E006]: Missing expected Token `;` + ┌─ :5:9 + │ + 5 │ FUNCTION main + │ ^^^^^^^^ Missing expected Token `;` + + error[E006]: Missing expected Token `END_TYPE` + ┌─ :5:9 + │ + 5 │ FUNCTION main + │ ^^^^^^^^ Missing expected Token `END_TYPE` + "); + assert_eq!(unit.pous.len(), 1); + assert_eq!(unit.pous[0].name, "main"); +} + +#[test] +fn array_missing_close_bracket_keeps_of_element_type() { + let src = r" + TYPE Matrix: + ARRAY [1..10 OF DINT; + END_TYPE + "; + + let (unit, diagnostics) = parse_buffered(src); + + assert_snapshot!(diagnostics, @" + error[E006]: Missing expected Token `]` + ┌─ :3:26 + │ + 3 │ ARRAY [1..10 OF DINT; + │ ^^ Missing expected Token `]` + "); + assert_eq!(unit.user_types.len(), 1); + assert!(matches!(unit.user_types[0].data_type, DataType::ArrayType { .. })); +} + +#[test] +fn array_bounds_missing_comma_recovers_at_next_range() { + let src = r" + TYPE Matrix: + ARRAY [1..10 20..30] OF DINT; + END_TYPE + + FUNCTION other + END_FUNCTION + "; + + let (unit, diagnostics) = parse_buffered(src); + + assert_snapshot!(diagnostics, @" + error[E006]: Missing expected Token `,` + ┌─ :3:26 + │ + 3 │ ARRAY [1..10 20..30] OF DINT; + │ ^^ Missing expected Token `,` + "); + assert_eq!(unit.user_types.len(), 1); + assert!(matches!(unit.user_types[0].data_type, DataType::ArrayType { .. })); + assert_eq!(unit.pous.len(), 1); + assert_eq!(unit.pous[0].name, "other"); +} + +#[test] +fn variable_array_missing_of_recovers_at_next_variable() { + let src = r" + FUNCTION_BLOCK FbA + VAR + xs: ARRAY [1..10] + y: DINT; + END_VAR + END_FUNCTION_BLOCK + "; + + let (unit, diagnostics) = parse_buffered(src); + + assert_snapshot!(diagnostics, @" + error[E006]: Missing expected Token `OF` + ┌─ :5:17 + │ + 5 │ y: DINT; + │ ^ Missing expected Token `OF` + + error[E006]: Missing expected Token `;` + ┌─ :5:17 + │ + 5 │ y: DINT; + │ ^ Missing expected Token `;` + "); + + let variables = &unit.pous[0].variable_blocks[0].variables; + assert_eq!(variables.len(), 1); + assert_eq!(variables[0].name, "y"); +} + +#[test] +fn variable_array_bounds_missing_comma_recovers_at_next_range() { + let src = r" + FUNCTION_BLOCK FbA + VAR + xs: ARRAY [1..10 20..30] OF DINT; + y: DINT; + END_VAR + END_FUNCTION_BLOCK + "; + + let (unit, diagnostics) = parse_buffered(src); + + assert_snapshot!(diagnostics, @" + error[E006]: Missing expected Token `,` + ┌─ :4:34 + │ + 4 │ xs: ARRAY [1..10 20..30] OF DINT; + │ ^^ Missing expected Token `,` + "); + + let variables = &unit.pous[0].variable_blocks[0].variables; + assert_eq!(variables.len(), 2); + assert_eq!(variables[0].name, "xs"); + assert_eq!(variables[1].name, "y"); +} + +#[test] +fn variable_missing_colon_before_type_recovers_at_type_name() { + let src = r" + FUNCTION_BLOCK FbA + VAR + x INT; + y: INT; + END_VAR + END_FUNCTION_BLOCK + "; + + let (unit, diagnostics) = parse_buffered(src); + + assert_snapshot!(diagnostics, @" + error[E006]: Missing expected Token `:` + ┌─ :4:19 + │ + 4 │ x INT; + │ ^^^ Missing expected Token `:` + "); + + let variables = &unit.pous[0].variable_blocks[0].variables; + assert_eq!(variables.len(), 2); + assert_eq!(variables[0].name, "x"); + assert_eq!(variables[1].name, "y"); +} + +#[test] +fn string_size_missing_close_bracket_recovers_at_next_variable() { + let src = r" + FUNCTION_BLOCK FbA + VAR + s: STRING[10 + x: DINT; + END_VAR + END_FUNCTION_BLOCK + "; + + let (unit, diagnostics) = parse_buffered(src); + + assert_snapshot!(diagnostics, @" + error[E006]: Missing expected Token `]` + ┌─ :5:17 + │ + 5 │ x: DINT; + │ ^ Missing expected Token `]` + + error[E006]: Missing expected Token `;` + ┌─ :5:17 + │ + 5 │ x: DINT; + │ ^ Missing expected Token `;` + "); + + let variables = &unit.pous[0].variable_blocks[0].variables; + assert_eq!(variables.len(), 2); + assert_eq!(variables[0].name, "s"); + assert_eq!(variables[1].name, "x"); +} + +#[test] +fn string_size_missing_close_bracket_recovers_at_next_top_level_declaration() { + let src = r" + TYPE Name: + STRING[10 + + FUNCTION other + END_FUNCTION + "; + + let (unit, diagnostics) = parse_buffered(src); + + assert_snapshot!(diagnostics, @" + error[E006]: Missing expected Token `]` + ┌─ :5:9 + │ + 5 │ FUNCTION other + │ ^^^^^^^^ Missing expected Token `]` + + error[E006]: Missing expected Token `;` + ┌─ :5:9 + │ + 5 │ FUNCTION other + │ ^^^^^^^^ Missing expected Token `;` + + error[E006]: Missing expected Token `END_TYPE` + ┌─ :5:9 + │ + 5 │ FUNCTION other + │ ^^^^^^^^ Missing expected Token `END_TYPE` + "); + assert_eq!(unit.pous.len(), 1); + assert_eq!(unit.pous[0].name, "other"); +} + +#[test] +fn reference_type_missing_base_recovers_at_next_top_level_declaration() { + let src = r" + TYPE RefInt: + REF_TO + + FUNCTION other + END_FUNCTION + "; + + let (unit, diagnostics) = parse_buffered(src); + + assert_snapshot!(diagnostics, @" + error[E006]: Missing expected Token DataTypeDefinition + ┌─ :5:9 + │ + 5 │ FUNCTION other + │ ^^^^^^^^ Missing expected Token DataTypeDefinition + + error[E006]: Missing expected Token `;` + ┌─ :5:9 + │ + 5 │ FUNCTION other + │ ^^^^^^^^ Missing expected Token `;` + + error[E006]: Missing expected Token `END_TYPE` + ┌─ :5:9 + │ + 5 │ FUNCTION other + │ ^^^^^^^^ Missing expected Token `END_TYPE` + "); + assert_eq!(unit.pous.len(), 1); + assert_eq!(unit.pous[0].name, "other"); +} + +#[test] +fn reference_variable_missing_base_recovers_at_next_variable() { + let src = r" + FUNCTION_BLOCK FbA + VAR + r: REF_TO + x: DINT; + END_VAR + END_FUNCTION_BLOCK + "; + + let (unit, diagnostics) = parse_buffered(src); + + assert_snapshot!(diagnostics, @" + error[E006]: Missing expected Token DataTypeDefinition + ┌─ :5:17 + │ + 5 │ x: DINT; + │ ^ Missing expected Token DataTypeDefinition + + error[E006]: Missing expected Token `;` + ┌─ :5:17 + │ + 5 │ x: DINT; + │ ^ Missing expected Token `;` + "); + let variables = &unit.pous[0].variable_blocks[0].variables; + assert_eq!(variables.len(), 1); + assert_eq!(variables[0].name, "x"); +} + +#[test] +fn interface_extends_missing_comma_recovers_at_next_interface_name() { + let src = r" + INTERFACE IBase + END_INTERFACE + + INTERFACE IOther + END_INTERFACE + + INTERFACE IFoo EXTENDS IBase IOther + END_INTERFACE + "; + + let (_, diagnostics) = parse_buffered(src); + + assert_snapshot!(diagnostics, @" + error[E006]: Missing expected Token `,` + ┌─ :8:38 + │ + 8 │ INTERFACE IFoo EXTENDS IBase IOther + │ ^^^^^^ Missing expected Token `,` + "); +} + +#[test] +fn implements_missing_comma_recovers_at_next_interface_name() { + let src = r" + INTERFACE IBase + END_INTERFACE + + INTERFACE IOther + END_INTERFACE + + FUNCTION_BLOCK FbA IMPLEMENTS IBase IOther + END_FUNCTION_BLOCK + "; + + let (_, diagnostics) = parse_buffered(src); + + assert_snapshot!(diagnostics, @" + error[E006]: Missing expected Token `,` + ┌─ :8:45 + │ + 8 │ FUNCTION_BLOCK FbA IMPLEMENTS IBase IOther + │ ^^^^^^ Missing expected Token `,` + "); +} + +#[test] +fn function_generics_missing_comma_recovers_at_next_generic_binding() { + let src = r" + FUNCTION test : R + END_FUNCTION + + FUNCTION other + END_FUNCTION + "; + + let (unit, diagnostics) = parse_buffered(src); + + assert_snapshot!(diagnostics, @" + error[E006]: Missing expected Token `,` + ┌─ :2:30 + │ + 2 │ FUNCTION test : R + │ ^ Missing expected Token `,` + "); + assert_eq!(unit.pous.len(), 2); + assert_eq!(unit.pous[0].generics.len(), 2); + assert_eq!(unit.pous[0].generics[0], GenericBinding { name: "T".into(), nature: TypeNature::Any }); + assert_eq!(unit.pous[0].generics[1], GenericBinding { name: "R".into(), nature: TypeNature::Num }); + assert_eq!(unit.pous[1].name, "other"); +} + +#[test] +fn method_generics_missing_comma_recovers_at_next_generic_binding() { + let src = r" + CLASS C + METHOD test : R + END_METHOD + END_CLASS + + FUNCTION other + END_FUNCTION + "; + + let (unit, diagnostics) = parse_buffered(src); + + assert_snapshot!(diagnostics, @" + error[E006]: Missing expected Token `,` + ┌─ :3:32 + │ + 3 │ METHOD test : R + │ ^ Missing expected Token `,` + "); + assert_eq!(unit.pous.len(), 3); + assert_eq!(unit.pous[1].name, "C.test"); + assert_eq!(unit.pous[1].generics.len(), 2); + assert_eq!(unit.pous[2].name, "other"); +} + +#[test] +fn function_generics_missing_close_recovers_at_return_type() { + let src = r" + FUNCTION test` + ┌─ :2:42 + │ + 2 │ FUNCTION test` + "); + assert_eq!(unit.pous.len(), 2); + assert_eq!(unit.pous[0].generics.len(), 2); + assert_eq!(unit.pous[1].name, "other"); +} + +#[test] +fn call_missing_close_paren_recovers_at_statement_semicolon() { + let src = r" + FUNCTION main + foo(1, 2; + END_FUNCTION + "; + + let (unit, diagnostics) = parse_buffered(src); + + assert_snapshot!(diagnostics, @" + error[E006]: Missing expected Token `)` + ┌─ :3:21 + │ + 3 │ foo(1, 2; + │ ^ Missing expected Token `)` + "); + assert_eq!(unit.pous.len(), 1); + assert_eq!(unit.implementations.len(), 1); +} + +#[test] +fn subscript_missing_close_bracket_recovers_at_statement_semicolon() { + let src = r" + FUNCTION main + x := arr[1; + END_FUNCTION + "; + + let (unit, diagnostics) = parse_buffered(src); + + assert_snapshot!(diagnostics, @" + error[E006]: Missing expected Token `]` + ┌─ :3:23 + │ + 3 │ x := arr[1; + │ ^ Missing expected Token `]` + "); + assert_eq!(unit.pous.len(), 1); + assert_eq!(unit.implementations.len(), 1); +} + +#[test] +fn struct_initializer_missing_close_paren_recovers_at_variable_semicolon() { + let src = r" + FUNCTION_BLOCK FbA + VAR + p: Position := (x := 1, y := 2; + z: DINT; + END_VAR + END_FUNCTION_BLOCK + "; + + let (unit, diagnostics) = parse_buffered(src); + + assert_snapshot!(diagnostics, @" + error[E006]: Missing expected Token `)` + ┌─ :4:47 + │ + 4 │ p: Position := (x := 1, y := 2; + │ ^ Missing expected Token `)` + "); + + let variables = &unit.pous[0].variable_blocks[0].variables; + assert_eq!(variables.len(), 2); + assert_eq!(variables[0].name, "p"); + assert_eq!(variables[1].name, "z"); +} + +#[test] +fn struct_initializer_missing_comma_recovers_at_next_field_assignment() { + let src = r" + FUNCTION_BLOCK FbA + VAR + p: Position := (x := 1 y := 2); + z: DINT; + END_VAR + END_FUNCTION_BLOCK + "; + + let (unit, diagnostics) = parse_buffered(src); + + assert_snapshot!(diagnostics, @" + error[E006]: Missing expected Token `,` + ┌─ :4:40 + │ + 4 │ p: Position := (x := 1 y := 2); + │ ^ Missing expected Token `,` + "); + + let variables = &unit.pous[0].variable_blocks[0].variables; + assert_eq!(variables.len(), 2); + assert_eq!(variables[0].name, "p"); + assert_eq!(variables[1].name, "z"); +} + +#[test] +fn parenthesized_initializer_missing_close_recovers_at_next_variable() { + let src = r" + FUNCTION_BLOCK FbA + VAR + p: Position := (x := 1 + z: DINT; + END_VAR + END_FUNCTION_BLOCK + "; + + let (unit, diagnostics) = parse_buffered(src); + + assert_snapshot!(diagnostics, @" + error[E006]: Missing expected Token `)` + ┌─ :5:17 + │ + 5 │ z: DINT; + │ ^ Missing expected Token `)` + + error[E006]: Missing expected Token `;` + ┌─ :5:17 + │ + 5 │ z: DINT; + │ ^ Missing expected Token `;` + "); + + let variables = &unit.pous[0].variable_blocks[0].variables; + assert_eq!(variables.len(), 2); + assert_eq!(variables[0].name, "p"); + assert_eq!(variables[1].name, "z"); +} + +#[test] +fn call_initializer_missing_close_recovers_at_next_variable() { + let src = r" + FUNCTION_BLOCK FbA + VAR + p: DINT := foo(1 + z: DINT; + END_VAR + END_FUNCTION_BLOCK + "; + + let (unit, diagnostics) = parse_buffered(src); + + assert_snapshot!(diagnostics, @" + error[E006]: Missing expected Token `)` + ┌─ :5:17 + │ + 5 │ z: DINT; + │ ^ Missing expected Token `)` + + error[E006]: Missing expected Token `;` + ┌─ :5:17 + │ + 5 │ z: DINT; + │ ^ Missing expected Token `;` + "); + + let variables = &unit.pous[0].variable_blocks[0].variables; + assert_eq!(variables.len(), 2); + assert_eq!(variables[0].name, "p"); + assert_eq!(variables[1].name, "z"); +} + +#[test] +fn index_initializer_missing_close_recovers_at_next_variable() { + let src = r" + FUNCTION_BLOCK FbA + VAR + p: DINT := arr[1 + z: DINT; + END_VAR + END_FUNCTION_BLOCK + "; + + let (unit, diagnostics) = parse_buffered(src); + + assert_snapshot!(diagnostics, @" + error[E006]: Missing expected Token `]` + ┌─ :5:17 + │ + 5 │ z: DINT; + │ ^ Missing expected Token `]` + + error[E006]: Missing expected Token `;` + ┌─ :5:17 + │ + 5 │ z: DINT; + │ ^ Missing expected Token `;` + "); + + let variables = &unit.pous[0].variable_blocks[0].variables; + assert_eq!(variables.len(), 2); + assert_eq!(variables[0].name, "p"); + assert_eq!(variables[1].name, "z"); +} + +#[test] +fn array_literal_missing_comma_recovers_at_next_element() { + let src = r" + FUNCTION_BLOCK FbA + VAR + xs: Arr := [1 2]; + y: DINT; + END_VAR + END_FUNCTION_BLOCK + "; + + let (unit, diagnostics) = parse_buffered(src); + + assert_snapshot!(diagnostics, @" + error[E006]: Missing expected Token `,` + ┌─ :4:31 + │ + 4 │ xs: Arr := [1 2]; + │ ^ Missing expected Token `,` + "); + + let variables = &unit.pous[0].variable_blocks[0].variables; + assert_eq!(variables.len(), 2); + assert_eq!(variables[0].name, "xs"); + assert_eq!(variables[1].name, "y"); +} + +#[test] +fn if_missing_then_recovers_at_statement_body() { + let src = r" + FUNCTION main + IF x + y := 1; + END_IF + z := 2; + END_FUNCTION + "; + + let (unit, diagnostics) = parse_buffered(src); + + assert_snapshot!(diagnostics, @" + error[E006]: Missing expected Token `THEN` + ┌─ :4:17 + │ + 4 │ y := 1; + │ ^ Missing expected Token `THEN` + "); + assert_eq!(unit.pous.len(), 1); + assert_eq!(unit.implementations.len(), 1); +} + +#[test] +fn for_missing_to_recovers_at_end_expression() { + let src = r" + FUNCTION main + FOR i := 0 10 DO + y := i; + END_FOR + z := 2; + END_FUNCTION + "; + + let (unit, diagnostics) = parse_buffered(src); + + assert_snapshot!(diagnostics, @" + error[E006]: Missing expected Token `TO` + ┌─ :3:24 + │ + 3 │ FOR i := 0 10 DO + │ ^^ Missing expected Token `TO` + "); + assert_eq!(unit.pous.len(), 1); + assert_eq!(unit.implementations.len(), 1); +} + +#[test] +fn case_missing_of_recovers_at_first_case_label() { + let src = r" + FUNCTION main + CASE x + 1: y := 1; + END_CASE + z := 2; + END_FUNCTION + "; + + let (unit, diagnostics) = parse_buffered(src); + + assert_snapshot!(diagnostics, @" + error[E006]: Missing expected Token `OF` + ┌─ :4:17 + │ + 4 │ 1: y := 1; + │ ^ Missing expected Token `OF` + "); + assert_eq!(unit.pous.len(), 1); + assert_eq!(unit.implementations.len(), 1); +} + +#[test] +fn case_missing_label_colon_recovers_at_statement_body() { + let src = r" + FUNCTION main + CASE x OF + 1 y := 1; + 2: y := 2; + END_CASE + z := 3; + END_FUNCTION + "; + + let (unit, diagnostics) = parse_buffered(src); + + assert_snapshot!(diagnostics, @" + error[E006]: Missing expected Token `:` + ┌─ :4:19 + │ + 4 │ 1 y := 1; + │ ^ Missing expected Token `:` + "); + + let AstStatement::ControlStatement(AstControlStatement::Case(case)) = + unit.implementations[0].statements[0].get_stmt() + else { + panic!("expected case statement"); + }; + assert_eq!(case.case_blocks.len(), 2); + assert_eq!(case.case_blocks[0].body.len(), 1); + assert_eq!(unit.implementations[0].statements.len(), 2); +} + +#[test] +fn case_missing_label_colon_recovers_at_next_case_label() { + let src = r" + FUNCTION main + CASE x OF + 1 + 2: y := 2; + END_CASE + z := 3; + END_FUNCTION + "; + + let (unit, diagnostics) = parse_buffered(src); + + assert_snapshot!(diagnostics, @" + error[E006]: Missing expected Token `:` + ┌─ :5:17 + │ + 5 │ 2: y := 2; + │ ^ Missing expected Token `:` + "); + + let AstStatement::ControlStatement(AstControlStatement::Case(case)) = + unit.implementations[0].statements[0].get_stmt() + else { + panic!("expected case statement"); + }; + assert_eq!(case.case_blocks.len(), 2); + assert!(case.case_blocks[0].body.is_empty()); + assert_eq!(unit.implementations[0].statements.len(), 2); +} + +#[test] +fn repeat_missing_end_repeat_recovers_at_next_statement() { + let src = r" + FUNCTION main + REPEAT + x := 1; + UNTIL x > 5 + z := 2; + END_FUNCTION + "; + + let (unit, diagnostics) = parse_buffered(src); + + assert_snapshot!(diagnostics, @" + error[E006]: Missing expected Token `END_REPEAT` + ┌─ :6:13 + │ + 6 │ z := 2; + │ ^ Missing expected Token `END_REPEAT` + "); + assert_eq!(unit.pous.len(), 1); + assert_eq!(unit.implementations.len(), 1); +} + +#[test] +fn pou_missing_end_recovers_at_next_top_level_declaration() { + let src = r" + FUNCTION_BLOCK FbA + VAR + x: DINT; + END_VAR + + FUNCTION main + END_FUNCTION + "; + + let (unit, diagnostics) = parse_buffered(src); + + assert_snapshot!(diagnostics, @" + error[E006]: Missing expected Token `END_FUNCTION_BLOCK` + ┌─ :7:9 + │ + 7 │ FUNCTION main + │ ^^^^^^^^ Missing expected Token `END_FUNCTION_BLOCK` + "); + assert_eq!(unit.pous.len(), 2); + assert_eq!(unit.pous[0].name, "FbA"); + assert_eq!(unit.pous[1].name, "main"); +} + +#[test] +fn var_block_missing_end_recovers_at_next_top_level_declaration() { + let src = r" + FUNCTION_BLOCK FbA + VAR + x: DINT; + + FUNCTION main + END_FUNCTION + "; + + let (unit, diagnostics) = parse_buffered(src); + + assert_snapshot!(diagnostics, @" + error[E006]: Missing expected Token `END_VAR` + ┌─ :6:9 + │ + 6 │ FUNCTION main + │ ^^^^^^^^ Missing expected Token `END_VAR` + + error[E006]: Missing expected Token `END_FUNCTION_BLOCK` + ┌─ :6:9 + │ + 6 │ FUNCTION main + │ ^^^^^^^^ Missing expected Token `END_FUNCTION_BLOCK` + "); + assert_eq!(unit.pous.len(), 2); + assert_eq!(unit.pous[0].name, "FbA"); + assert_eq!(unit.pous[1].name, "main"); +} + +#[test] +fn var_block_missing_end_recovers_at_next_member_declaration() { + let src = r" + FUNCTION_BLOCK FbA + VAR + x: DINT; + + METHOD second + END_METHOD + END_FUNCTION_BLOCK + "; + + let (unit, diagnostics) = parse_buffered(src); + + assert_snapshot!(diagnostics, @" + error[E006]: Missing expected Token `END_VAR` + ┌─ :6:13 + │ + 6 │ METHOD second + │ ^^^^^^ Missing expected Token `END_VAR` + "); + assert_eq!(unit.pous.len(), 2); + assert_eq!(unit.pous[0].name, "FbA"); + assert_eq!(unit.pous[1].name, "FbA.second"); +} + +#[test] +fn var_block_missing_variable_semicolon_recovers_at_next_variable() { + let src = r" + FUNCTION_BLOCK FbA + VAR + x: DINT + y: DINT; + END_VAR + END_FUNCTION_BLOCK + "; + + let (unit, diagnostics) = parse_buffered(src); + + assert_snapshot!(diagnostics, @" + error[E006]: Missing expected Token `;` + ┌─ :5:17 + │ + 5 │ y: DINT; + │ ^ Missing expected Token `;` + "); + let variables = &unit.pous[0].variable_blocks[0].variables; + assert_eq!(variables.len(), 2); + assert_eq!(variables[0].name, "x"); + assert_eq!(variables[1].name, "y"); +} + +#[test] +fn var_config_missing_end_recovers_at_next_top_level_declaration() { + let src = r" + VAR_CONFIG + main.x AT %QX0.0 : BOOL; + + FUNCTION main + END_FUNCTION + "; + + let (unit, diagnostics) = parse_buffered(src); + + assert_snapshot!(diagnostics, @" + error[E006]: Missing expected Token `END_VAR` + ┌─ :5:9 + │ + 5 │ FUNCTION main + │ ^^^^^^^^ Missing expected Token `END_VAR` + "); + assert_eq!(unit.var_config.len(), 1); + assert_eq!(unit.pous.len(), 1); + assert_eq!(unit.pous[0].name, "main"); +} + +#[test] +fn var_config_missing_semicolon_recovers_at_next_top_level_declaration() { + let src = r" + VAR_CONFIG + main.x AT %QX0.0 : BOOL + + FUNCTION main + END_FUNCTION + "; + + let (unit, diagnostics) = parse_buffered(src); + + assert_snapshot!(diagnostics, @" + error[E006]: Missing expected Token `;` + ┌─ :5:9 + │ + 5 │ FUNCTION main + │ ^^^^^^^^ Missing expected Token `;` + + error[E006]: Missing expected Token `END_VAR` + ┌─ :5:9 + │ + 5 │ FUNCTION main + │ ^^^^^^^^ Missing expected Token `END_VAR` + "); + assert_eq!(unit.var_config.len(), 1); + assert_eq!(unit.pous.len(), 1); + assert_eq!(unit.pous[0].name, "main"); +} + +#[test] +fn var_config_missing_semicolon_recovers_at_next_config_entry() { + let src = r" + VAR_CONFIG + main.x AT %QW0 : DINT + main.y AT %QW1 : INT; + END_VAR + + FUNCTION other + END_FUNCTION + "; + + let (unit, diagnostics) = parse_buffered(src); + + assert_snapshot!(diagnostics, @" + error[E006]: Missing expected Token `;` + ┌─ :4:13 + │ + 4 │ main.y AT %QW1 : INT; + │ ^^^^ Missing expected Token `;` + "); + assert_eq!(unit.var_config.len(), 2); + assert_eq!(unit.pous.len(), 1); + assert_eq!(unit.pous[0].name, "other"); +} + +#[test] +fn var_config_missing_hardware_access_skips_type_tail() { + let src = r" + VAR_CONFIG + main.x AT : BOOL; + main.y AT %QW1 : INT; + END_VAR + + FUNCTION other + END_FUNCTION + "; + + let (unit, diagnostics) = parse_buffered(src); + + assert_snapshot!(diagnostics, @r" + error[E006]: Missing expected Token hardware access + ┌─ :3:23 + │ + 3 │ main.x AT : BOOL; + │ ^ Missing expected Token hardware access + "); + assert_eq!(unit.var_config.len(), 1); + assert_eq!(unit.pous.len(), 1); + assert_eq!(unit.pous[0].name, "other"); +} + +#[test] +fn if_missing_end_recovers_at_next_top_level_declaration() { + let src = r" + FUNCTION main + IF TRUE THEN + x := 1; + + FUNCTION other + END_FUNCTION + "; + + let (unit, diagnostics) = parse_buffered(src); + + assert_snapshot!(diagnostics, @" + error[E006]: Missing expected Token `END_IF` + ┌─ :6:9 + │ + 6 │ FUNCTION other + │ ^^^^^^^^ Missing expected Token `END_IF` + + error[E006]: Missing expected Token `END_FUNCTION` + ┌─ :6:9 + │ + 6 │ FUNCTION other + │ ^^^^^^^^ Missing expected Token `END_FUNCTION` + "); + assert_eq!(unit.pous.len(), 2); + assert_eq!(unit.pous[0].name, "main"); + assert_eq!(unit.pous[1].name, "other"); +} + +#[test] +fn for_missing_end_recovers_at_next_top_level_declaration() { + let src = r" + FUNCTION main + FOR i := 0 TO 10 DO + x := i; + + FUNCTION other + END_FUNCTION + "; + + let (unit, diagnostics) = parse_buffered(src); + + assert_snapshot!(diagnostics, @" + error[E006]: Missing expected Token `END_FOR` + ┌─ :6:9 + │ + 6 │ FUNCTION other + │ ^^^^^^^^ Missing expected Token `END_FOR` + + error[E006]: Missing expected Token `END_FUNCTION` + ┌─ :6:9 + │ + 6 │ FUNCTION other + │ ^^^^^^^^ Missing expected Token `END_FUNCTION` + "); + assert_eq!(unit.pous.len(), 2); + assert_eq!(unit.pous[0].name, "main"); + assert_eq!(unit.pous[1].name, "other"); +} + +#[test] +fn while_missing_end_recovers_at_next_top_level_declaration() { + let src = r" + FUNCTION main + WHILE TRUE DO + x := 1; + + FUNCTION other + END_FUNCTION + "; + + let (unit, diagnostics) = parse_buffered(src); + + assert_snapshot!(diagnostics, @" + error[E006]: Missing expected Token `END_WHILE` + ┌─ :6:9 + │ + 6 │ FUNCTION other + │ ^^^^^^^^ Missing expected Token `END_WHILE` + + error[E006]: Missing expected Token `END_FUNCTION` + ┌─ :6:9 + │ + 6 │ FUNCTION other + │ ^^^^^^^^ Missing expected Token `END_FUNCTION` + "); + assert_eq!(unit.pous.len(), 2); + assert_eq!(unit.pous[0].name, "main"); + assert_eq!(unit.pous[1].name, "other"); +} + +#[test] +fn repeat_missing_until_recovers_at_next_top_level_declaration() { + let src = r" + FUNCTION main + REPEAT + x := 1; + + FUNCTION other + END_FUNCTION + "; + + let (unit, diagnostics) = parse_buffered(src); + + assert_snapshot!(diagnostics, @" + error[E006]: Missing expected Token `UNTIL` + ┌─ :6:9 + │ + 6 │ FUNCTION other + │ ^^^^^^^^ Missing expected Token `UNTIL` + + error[E006]: Missing expected Token `END_FUNCTION` + ┌─ :6:9 + │ + 6 │ FUNCTION other + │ ^^^^^^^^ Missing expected Token `END_FUNCTION` + "); + assert_eq!(unit.pous.len(), 2); + assert_eq!(unit.pous[0].name, "main"); + assert_eq!(unit.pous[1].name, "other"); +} + +#[test] +fn case_missing_end_recovers_at_next_top_level_declaration() { + let src = r" + FUNCTION main + CASE x OF + 1: y := 1; + + FUNCTION other + END_FUNCTION + "; + + let (unit, diagnostics) = parse_buffered(src); + + assert_snapshot!(diagnostics, @" + error[E006]: Missing expected Token `END_CASE` + ┌─ :6:9 + │ + 6 │ FUNCTION other + │ ^^^^^^^^ Missing expected Token `END_CASE` + + error[E006]: Missing expected Token `END_FUNCTION` + ┌─ :6:9 + │ + 6 │ FUNCTION other + │ ^^^^^^^^ Missing expected Token `END_FUNCTION` + "); + assert_eq!(unit.pous.len(), 2); + assert_eq!(unit.pous[0].name, "main"); + assert_eq!(unit.pous[1].name, "other"); +} + +#[test] +fn method_missing_end_recovers_at_next_member_declaration() { + let src = r" + FUNCTION_BLOCK FbA + METHOD first + + METHOD second + END_METHOD + END_FUNCTION_BLOCK + "; + + let (unit, diagnostics) = parse_buffered(src); + + assert_snapshot!(diagnostics, @" + error[E006]: Missing expected Token `END_METHOD` + ┌─ :5:13 + │ + 5 │ METHOD second + │ ^^^^^^ Missing expected Token `END_METHOD` + "); + assert_eq!(unit.pous.len(), 3); + assert_eq!(unit.pous[0].name, "FbA"); + assert_eq!(unit.pous[1].name, "FbA.first"); + assert_eq!(unit.pous[2].name, "FbA.second"); +} + +#[test] +fn property_missing_end_recovers_at_next_member_declaration() { + let src = r" + FUNCTION_BLOCK FbA + PROPERTY_GET first: DINT + first := 1; + + METHOD second + END_METHOD + END_FUNCTION_BLOCK + "; + + let (unit, diagnostics) = parse_buffered(src); + + assert_snapshot!(diagnostics, @" + error[E006]: Missing expected Token `END_PROPERTY` + ┌─ :6:13 + │ + 6 │ METHOD second + │ ^^^^^^ Missing expected Token `END_PROPERTY` + "); + assert_eq!(unit.pous.len(), 2); + assert_eq!(unit.pous[0].name, "FbA"); + assert_eq!(unit.pous[1].name, "FbA.second"); + assert_eq!(unit.pous[0].properties.len(), 1); + assert_eq!(unit.pous[0].properties[0].ident.name, "first"); +} + +#[test] +fn action_missing_end_recovers_at_next_action_declaration() { + let src = r" + PROGRAM Main + END_PROGRAM + + ACTIONS Main + ACTION first + x := 1; + + ACTION second + x := 2; + END_ACTION + END_ACTIONS + "; + + let (unit, diagnostics) = parse_buffered(src); + + assert_snapshot!(diagnostics, @" + error[E006]: Missing expected Token `END_ACTION` + ┌─ :9:13 + │ + 9 │ ACTION second + │ ^^^^^^ Missing expected Token `END_ACTION` + "); + assert_eq!(unit.implementations.len(), 3); + assert_eq!(unit.implementations[0].name, "Main"); + assert_eq!(unit.implementations[1].name, "Main.first"); + assert_eq!(unit.implementations[2].name, "Main.second"); +} + +#[test] +fn actions_block_missing_end_recovers_at_next_top_level_declaration() { + let src = r" + PROGRAM Main + END_PROGRAM + + ACTIONS Main + ACTION first + x := 1; + END_ACTION + + FUNCTION other + END_FUNCTION + "; + + let (unit, diagnostics) = parse_buffered(src); + + assert_snapshot!(diagnostics, @" + error[E006]: Missing expected Token `END_ACTIONS` + ┌─ :10:9 + │ + 10 │ FUNCTION other + │ ^^^^^^^^ Missing expected Token `END_ACTIONS` + "); + assert_eq!(unit.pous.len(), 2); + assert_eq!(unit.pous[0].name, "Main"); + assert_eq!(unit.pous[1].name, "other"); + assert_eq!(unit.implementations.len(), 3); + assert_eq!(unit.implementations[0].name, "Main"); + assert_eq!(unit.implementations[1].name, "Main.first"); + assert_eq!(unit.implementations[2].name, "other"); +} + +#[test] +fn interface_method_missing_end_recovers_at_interface_end() { + let src = r" + INTERFACE IFoo + METHOD first + + END_INTERFACE + + FUNCTION main + END_FUNCTION + "; + + let (unit, diagnostics) = parse_buffered(src); + + assert_snapshot!(diagnostics, @" + error[E006]: Missing expected Token `END_METHOD` + ┌─ :5:9 + │ + 5 │ END_INTERFACE + │ ^^^^^^^^^^^^^ Missing expected Token `END_METHOD` + "); + assert_eq!(unit.interfaces.len(), 1); + assert_eq!(unit.interfaces[0].ident.name, "IFoo"); + assert_eq!(unit.interfaces[0].methods.len(), 1); + assert_eq!(unit.pous.len(), 1); + assert_eq!(unit.pous[0].name, "main"); +} diff --git a/src/parser/tests/parse_errors/parse_error_statements_tests.rs b/src/parser/tests/parse_errors/parse_error_statements_tests.rs index b995a262f5b..332ba25cab3 100644 --- a/src/parser/tests/parse_errors/parse_error_statements_tests.rs +++ b/src/parser/tests/parse_errors/parse_error_statements_tests.rs @@ -41,8 +41,7 @@ fn missing_semicolon_after_call() { #[test] fn missing_comma_in_call_parameters() { /* - * the missing comma after b will end the expression-list so we expect a ')' - * c will not be parsed as an expression + * the missing comma after b is reported and c is recovered as the next argument. */ let src = r" PROGRAM foo @@ -61,7 +60,7 @@ fn missing_comma_in_call_parameters() { vec![AstFactory::create_call_statement( ref_to("buz"), Some(AstFactory::create_expression_list( - vec![ref_to("a"), ref_to("b")], + vec![ref_to("a"), ref_to("b"), ref_to("c")], SourceLocation::internal(), 0 )), diff --git a/src/parser/tests/parse_errors/snapshots/rusty__parser__tests__parse_errors__parse_error_classes_tests__method_with_invalid_return_type.snap b/src/parser/tests/parse_errors/snapshots/rusty__parser__tests__parse_errors__parse_error_classes_tests__method_with_invalid_return_type.snap index 78c03ad7e1b..7ab1fadf7e2 100644 --- a/src/parser/tests/parse_errors/snapshots/rusty__parser__tests__parse_errors__parse_error_classes_tests__method_with_invalid_return_type.snap +++ b/src/parser/tests/parse_errors/snapshots/rusty__parser__tests__parse_errors__parse_error_classes_tests__method_with_invalid_return_type.snap @@ -2,11 +2,11 @@ source: src/parser/tests/parse_errors/parse_error_classes_tests.rs expression: diagnostics --- -error[E007]: Unexpected token: expected DataTypeDefinition but found KeywordAbstract +error[E007]: Unexpected token: expected DataTypeDefinition but found `ABSTRACT` ┌─ :1:30 │ 1 │ CLASS TestClass METHOD foo : ABSTRACT END_METHOD END_CLASS - │ ^^^^^^^^ Unexpected token: expected DataTypeDefinition but found KeywordAbstract + │ ^^^^^^^^ Unexpected token: expected DataTypeDefinition but found `ABSTRACT` error[E007]: Unexpected token: expected Datatype but found ABSTRACT ┌─ :1:30 @@ -20,20 +20,20 @@ error[E007]: Unexpected token: expected Literal but found ABSTRACT 1 │ CLASS TestClass METHOD foo : ABSTRACT END_METHOD END_CLASS │ ^^^^^^^^ Unexpected token: expected Literal but found ABSTRACT -error[E007]: Unexpected token: expected KeywordSemicolon but found 'ABSTRACT' +error[E007]: Unexpected token: expected `;` but found 'ABSTRACT' ┌─ :1:30 │ 1 │ CLASS TestClass METHOD foo : ABSTRACT END_METHOD END_CLASS - │ ^^^^^^^^ Unexpected token: expected KeywordSemicolon but found 'ABSTRACT' + │ ^^^^^^^^ Unexpected token: expected `;` but found 'ABSTRACT' -error[E006]: Missing expected Token [KeywordSemicolon, KeywordColon] +error[E006]: Missing expected Token `;` or `:` ┌─ :1:39 │ 1 │ CLASS TestClass METHOD foo : ABSTRACT END_METHOD END_CLASS - │ ^^^^^^^^^^ Missing expected Token [KeywordSemicolon, KeywordColon] + │ ^^^^^^^^^^ Missing expected Token `;` or `:` -error[E007]: Unexpected token: expected KeywordSemicolon but found 'END_METHOD' +error[E007]: Unexpected token: expected `;` but found 'END_METHOD' ┌─ :1:39 │ 1 │ CLASS TestClass METHOD foo : ABSTRACT END_METHOD END_CLASS - │ ^^^^^^^^^^ Unexpected token: expected KeywordSemicolon but found 'END_METHOD' + │ ^^^^^^^^^^ Unexpected token: expected `;` but found 'END_METHOD' diff --git a/src/parser/tests/parse_errors/snapshots/rusty__parser__tests__parse_errors__parse_error_containers_tests__a_program_needs_to_end_with_end_program.snap b/src/parser/tests/parse_errors/snapshots/rusty__parser__tests__parse_errors__parse_error_containers_tests__a_program_needs_to_end_with_end_program.snap index 750771fcc63..9b20fa2e079 100644 --- a/src/parser/tests/parse_errors/snapshots/rusty__parser__tests__parse_errors__parse_error_containers_tests__a_program_needs_to_end_with_end_program.snap +++ b/src/parser/tests/parse_errors/snapshots/rusty__parser__tests__parse_errors__parse_error_containers_tests__a_program_needs_to_end_with_end_program.snap @@ -2,8 +2,8 @@ source: src/parser/tests/parse_errors/parse_error_containers_tests.rs expression: diagnostics --- -error[E007]: Unexpected token: expected KeywordEndProgram but found '' +error[E007]: Unexpected token: expected `END_PROGRAM` but found '' ┌─ :1:13 │ 1 │ PROGRAM buz - │ ^ Unexpected token: expected KeywordEndProgram but found '' + │ ^ Unexpected token: expected `END_PROGRAM` but found '' diff --git a/src/parser/tests/parse_errors/snapshots/rusty__parser__tests__parse_errors__parse_error_containers_tests__a_variable_declaration_block_needs_to_end_with_endvar.snap b/src/parser/tests/parse_errors/snapshots/rusty__parser__tests__parse_errors__parse_error_containers_tests__a_variable_declaration_block_needs_to_end_with_endvar.snap index 6cb9e6b6723..4a8e0eb7075 100644 --- a/src/parser/tests/parse_errors/snapshots/rusty__parser__tests__parse_errors__parse_error_containers_tests__a_variable_declaration_block_needs_to_end_with_endvar.snap +++ b/src/parser/tests/parse_errors/snapshots/rusty__parser__tests__parse_errors__parse_error_containers_tests__a_variable_declaration_block_needs_to_end_with_endvar.snap @@ -2,14 +2,8 @@ source: src/parser/tests/parse_errors/parse_error_containers_tests.rs expression: diagnostics --- -error[E006]: Missing expected Token [KeywordEndVar] +error[E006]: Missing expected Token `END_VAR` ┌─ :1:17 │ 1 │ PROGRAM buz VAR END_PROGRAM - │ ^^^^^^^^^^^ Missing expected Token [KeywordEndVar] - -error[E007]: Unexpected token: expected KeywordEndVar but found 'END_PROGRAM' - ┌─ :1:17 - │ -1 │ PROGRAM buz VAR END_PROGRAM - │ ^^^^^^^^^^^ Unexpected token: expected KeywordEndVar but found 'END_PROGRAM' + │ ^^^^^^^^^^^ Missing expected Token `END_VAR` diff --git a/src/parser/tests/parse_errors/snapshots/rusty__parser__tests__parse_errors__parse_error_containers_tests__function_with_illegal_return_variable_declaration.snap b/src/parser/tests/parse_errors/snapshots/rusty__parser__tests__parse_errors__parse_error_containers_tests__function_with_illegal_return_variable_declaration.snap index 1a9f5f64fa4..072bf1a8b15 100644 --- a/src/parser/tests/parse_errors/snapshots/rusty__parser__tests__parse_errors__parse_error_containers_tests__function_with_illegal_return_variable_declaration.snap +++ b/src/parser/tests/parse_errors/snapshots/rusty__parser__tests__parse_errors__parse_error_containers_tests__function_with_illegal_return_variable_declaration.snap @@ -2,11 +2,11 @@ source: src/parser/tests/parse_errors/parse_error_containers_tests.rs expression: diagnostics --- -error[E007]: Unexpected token: expected DataTypeDefinition but found KeywordVar +error[E007]: Unexpected token: expected DataTypeDefinition but found `VAR` ┌─ :3:13 │ 3 │ VAR END_VAR - │ ^^^ Unexpected token: expected DataTypeDefinition but found KeywordVar + │ ^^^ Unexpected token: expected DataTypeDefinition but found `VAR` error[E007]: Unexpected token: expected Datatype but found VAR ┌─ :3:13 diff --git a/src/parser/tests/parse_errors/snapshots/rusty__parser__tests__parse_errors__parse_error_containers_tests__illegal_end_pou_keyword.snap b/src/parser/tests/parse_errors/snapshots/rusty__parser__tests__parse_errors__parse_error_containers_tests__illegal_end_pou_keyword.snap index d526dd2f95e..2bb496c5395 100644 --- a/src/parser/tests/parse_errors/snapshots/rusty__parser__tests__parse_errors__parse_error_containers_tests__illegal_end_pou_keyword.snap +++ b/src/parser/tests/parse_errors/snapshots/rusty__parser__tests__parse_errors__parse_error_containers_tests__illegal_end_pou_keyword.snap @@ -2,8 +2,8 @@ source: src/parser/tests/parse_errors/parse_error_containers_tests.rs expression: diagnostics --- -error[E007]: Unexpected token: expected KeywordEndProgram but found END_FUNCTION +error[E007]: Unexpected token: expected `END_PROGRAM` but found END_FUNCTION ┌─ :4:13 │ 4 │ END_FUNCTION - │ ^^^^^^^^^^^^ Unexpected token: expected KeywordEndProgram but found END_FUNCTION + │ ^^^^^^^^^^^^ Unexpected token: expected `END_PROGRAM` but found END_FUNCTION diff --git a/src/parser/tests/parse_errors/snapshots/rusty__parser__tests__parse_errors__parse_error_containers_tests__missing_pou_name_2.snap b/src/parser/tests/parse_errors/snapshots/rusty__parser__tests__parse_errors__parse_error_containers_tests__missing_pou_name_2.snap index 8e6f7ad3206..ae40ad794a1 100644 --- a/src/parser/tests/parse_errors/snapshots/rusty__parser__tests__parse_errors__parse_error_containers_tests__missing_pou_name_2.snap +++ b/src/parser/tests/parse_errors/snapshots/rusty__parser__tests__parse_errors__parse_error_containers_tests__missing_pou_name_2.snap @@ -8,8 +8,8 @@ error[E007]: Unexpected token: expected Literal but found := 3 │ a := 2; │ ^^ Unexpected token: expected Literal but found := -error[E007]: Unexpected token: expected KeywordSemicolon but found ':= 2' +error[E007]: Unexpected token: expected `;` but found ':= 2' ┌─ :3:15 │ 3 │ a := 2; - │ ^^^^ Unexpected token: expected KeywordSemicolon but found ':= 2' + │ ^^^^ Unexpected token: expected `;` but found ':= 2' diff --git a/src/parser/tests/parse_errors/snapshots/rusty__parser__tests__parse_errors__parse_error_containers_tests__unclosed_var_container.snap b/src/parser/tests/parse_errors/snapshots/rusty__parser__tests__parse_errors__parse_error_containers_tests__unclosed_var_container.snap index ad6d08962f7..eba73081aac 100644 --- a/src/parser/tests/parse_errors/snapshots/rusty__parser__tests__parse_errors__parse_error_containers_tests__unclosed_var_container.snap +++ b/src/parser/tests/parse_errors/snapshots/rusty__parser__tests__parse_errors__parse_error_containers_tests__unclosed_var_container.snap @@ -2,8 +2,8 @@ source: src/parser/tests/parse_errors/parse_error_containers_tests.rs expression: diagnostics --- -error[E007]: Unexpected token: expected KeywordEndVar but found 'VAR b : INT;' +error[E007]: Unexpected token: expected `END_VAR` but found 'VAR b : INT;' ┌─ :4:21 │ 4 │ VAR b : INT; END_VAR - │ ^^^^^^^^^^^^ Unexpected token: expected KeywordEndVar but found 'VAR b : INT;' + │ ^^^^^^^^^^^^ Unexpected token: expected `END_VAR` but found 'VAR b : INT;' diff --git a/src/parser/tests/parse_errors/snapshots/rusty__parser__tests__parse_errors__parse_error_containers_tests__unexpected_type_declaration_error_message.snap b/src/parser/tests/parse_errors/snapshots/rusty__parser__tests__parse_errors__parse_error_containers_tests__unexpected_type_declaration_error_message.snap index 9d894cbfae7..c7a4461179b 100644 --- a/src/parser/tests/parse_errors/snapshots/rusty__parser__tests__parse_errors__parse_error_containers_tests__unexpected_type_declaration_error_message.snap +++ b/src/parser/tests/parse_errors/snapshots/rusty__parser__tests__parse_errors__parse_error_containers_tests__unexpected_type_declaration_error_message.snap @@ -2,29 +2,29 @@ source: src/parser/tests/parse_errors/parse_error_containers_tests.rs expression: diagnostics --- -error[E007]: Unexpected token: expected DataTypeDefinition but found KeywordProgram +error[E007]: Unexpected token: expected DataTypeDefinition but found `PROGRAM` ┌─ :2:17 │ 2 │ PROGRAM - │ ^^^^^^^ Unexpected token: expected DataTypeDefinition but found KeywordProgram + │ ^^^^^^^ Unexpected token: expected DataTypeDefinition but found `PROGRAM` -error[E007]: Unexpected token: expected KeywordSemicolon but found 'PROGRAM +error[E007]: Unexpected token: expected `;` but found 'PROGRAM END_PROGRAM' ┌─ :2:17 │ 2 │ ╭ PROGRAM 3 │ │ END_PROGRAM - │ ╰───────────────────────────^ Unexpected token: expected KeywordSemicolon but found 'PROGRAM + │ ╰───────────────────────────^ Unexpected token: expected `;` but found 'PROGRAM END_PROGRAM' -error[E006]: Missing expected Token [KeywordSemicolon] +error[E006]: Missing expected Token `;` ┌─ :4:13 │ 4 │ END_TYPE - │ ^^^^^^^^ Missing expected Token [KeywordSemicolon] + │ ^^^^^^^^ Missing expected Token `;` -error[E007]: Unexpected token: expected KeywordSemicolon but found 'END_TYPE' +error[E007]: Unexpected token: expected `;` but found 'END_TYPE' ┌─ :4:13 │ 4 │ END_TYPE - │ ^^^^^^^^ Unexpected token: expected KeywordSemicolon but found 'END_TYPE' + │ ^^^^^^^^ Unexpected token: expected `;` but found 'END_TYPE' diff --git a/src/parser/tests/parse_errors/snapshots/rusty__parser__tests__parse_errors__parse_error_literals_tests__literal_bin_number_with_double_underscores.snap b/src/parser/tests/parse_errors/snapshots/rusty__parser__tests__parse_errors__parse_error_literals_tests__literal_bin_number_with_double_underscores.snap index 3baf1f69211..69ec9070428 100644 --- a/src/parser/tests/parse_errors/snapshots/rusty__parser__tests__parse_errors__parse_error_literals_tests__literal_bin_number_with_double_underscores.snap +++ b/src/parser/tests/parse_errors/snapshots/rusty__parser__tests__parse_errors__parse_error_literals_tests__literal_bin_number_with_double_underscores.snap @@ -2,8 +2,8 @@ source: src/parser/tests/parse_errors/parse_error_literals_tests.rs expression: diagnostics --- -error[E007]: Unexpected token: expected KeywordSemicolon but found '__001_101_01' +error[E007]: Unexpected token: expected `;` but found '__001_101_01' ┌─ :1:17 │ 1 │ PROGRAM exp 2#01__001_101_01; END_PROGRAM - │ ^^^^^^^^^^^^ Unexpected token: expected KeywordSemicolon but found '__001_101_01' + │ ^^^^^^^^^^^^ Unexpected token: expected `;` but found '__001_101_01' diff --git a/src/parser/tests/parse_errors/snapshots/rusty__parser__tests__parse_errors__parse_error_literals_tests__literal_dec_number_with_double_underscores.snap b/src/parser/tests/parse_errors/snapshots/rusty__parser__tests__parse_errors__parse_error_literals_tests__literal_dec_number_with_double_underscores.snap index 91c92b3c9c4..d5cd173794d 100644 --- a/src/parser/tests/parse_errors/snapshots/rusty__parser__tests__parse_errors__parse_error_literals_tests__literal_dec_number_with_double_underscores.snap +++ b/src/parser/tests/parse_errors/snapshots/rusty__parser__tests__parse_errors__parse_error_literals_tests__literal_dec_number_with_double_underscores.snap @@ -2,8 +2,8 @@ source: src/parser/tests/parse_errors/parse_error_literals_tests.rs expression: diagnostics --- -error[E007]: Unexpected token: expected KeywordSemicolon but found '__000' +error[E007]: Unexpected token: expected `;` but found '__000' ┌─ :1:15 │ 1 │ PROGRAM exp 43__000; END_PROGRAM - │ ^^^^^ Unexpected token: expected KeywordSemicolon but found '__000' + │ ^^^^^ Unexpected token: expected `;` but found '__000' diff --git a/src/parser/tests/parse_errors/snapshots/rusty__parser__tests__parse_errors__parse_error_literals_tests__literal_hex_number_with_double_underscores.snap b/src/parser/tests/parse_errors/snapshots/rusty__parser__tests__parse_errors__parse_error_literals_tests__literal_hex_number_with_double_underscores.snap index b3ffc2ea2dd..2665739be16 100644 --- a/src/parser/tests/parse_errors/snapshots/rusty__parser__tests__parse_errors__parse_error_literals_tests__literal_hex_number_with_double_underscores.snap +++ b/src/parser/tests/parse_errors/snapshots/rusty__parser__tests__parse_errors__parse_error_literals_tests__literal_hex_number_with_double_underscores.snap @@ -2,8 +2,8 @@ source: src/parser/tests/parse_errors/parse_error_literals_tests.rs expression: diagnostics --- -error[E007]: Unexpected token: expected KeywordSemicolon but found '__beef' +error[E007]: Unexpected token: expected `;` but found '__beef' ┌─ :1:20 │ 1 │ PROGRAM exp 16#DEAD__beef; END_PROGRAM - │ ^^^^^^ Unexpected token: expected KeywordSemicolon but found '__beef' + │ ^^^^^^ Unexpected token: expected `;` but found '__beef' diff --git a/src/parser/tests/parse_errors/snapshots/rusty__parser__tests__parse_errors__parse_error_literals_tests__literal_oct_number_with_double_underscores.snap b/src/parser/tests/parse_errors/snapshots/rusty__parser__tests__parse_errors__parse_error_literals_tests__literal_oct_number_with_double_underscores.snap index a5e6dac9330..adeef4f902d 100644 --- a/src/parser/tests/parse_errors/snapshots/rusty__parser__tests__parse_errors__parse_error_literals_tests__literal_oct_number_with_double_underscores.snap +++ b/src/parser/tests/parse_errors/snapshots/rusty__parser__tests__parse_errors__parse_error_literals_tests__literal_oct_number_with_double_underscores.snap @@ -2,8 +2,8 @@ source: src/parser/tests/parse_errors/parse_error_literals_tests.rs expression: diagnostics --- -error[E007]: Unexpected token: expected KeywordSemicolon but found '__7' +error[E007]: Unexpected token: expected `;` but found '__7' ┌─ :1:16 │ 1 │ PROGRAM exp 8#7__7; END_PROGRAM - │ ^^^ Unexpected token: expected KeywordSemicolon but found '__7' + │ ^^^ Unexpected token: expected `;` but found '__7' diff --git a/src/parser/tests/parse_errors/snapshots/rusty__parser__tests__parse_errors__parse_error_messages_test__case_with_unexpected_token.snap b/src/parser/tests/parse_errors/snapshots/rusty__parser__tests__parse_errors__parse_error_messages_test__case_with_unexpected_token.snap index a842ec71410..7de99509744 100644 --- a/src/parser/tests/parse_errors/snapshots/rusty__parser__tests__parse_errors__parse_error_messages_test__case_with_unexpected_token.snap +++ b/src/parser/tests/parse_errors/snapshots/rusty__parser__tests__parse_errors__parse_error_messages_test__case_with_unexpected_token.snap @@ -2,47 +2,23 @@ source: src/parser/tests/parse_errors/parse_error_messages_test.rs expression: diagnostics --- -error[E007]: Unexpected token: expected KeywordOf but found DELTA +error[E006]: Missing expected Token `OF` ┌─ :3:27 │ 3 │ CASE StateMachine DELTA - │ ^^^^^ Unexpected token: expected KeywordOf but found DELTA + │ ^^^^^ Missing expected Token `OF` -error[E007]: Unexpected token: expected KeywordSemicolon but found '1' +error[E006]: Missing expected Token `:` ┌─ :4:9 │ 4 │ 1: x; - │ ^ Unexpected token: expected KeywordSemicolon but found '1' + │ ^ Missing expected Token `:` -error[E007]: Unexpected token: expected Literal but found END_CASE - ┌─ :5:9 - │ -5 │ END_CASE - │ ^^^^^^^^ Unexpected token: expected Literal but found END_CASE - -error[E007]: Unexpected token: expected KeywordSemicolon but found 'END_CASE' - ┌─ :5:9 - │ -5 │ END_CASE - │ ^^^^^^^^ Unexpected token: expected KeywordSemicolon but found 'END_CASE' - -error[E006]: Missing expected Token [KeywordSemicolon, KeywordColon] - ┌─ :6:9 - │ -6 │ END_PROGRAM - │ ^^^^^^^^^^^ Missing expected Token [KeywordSemicolon, KeywordColon] - -error[E007]: Unexpected token: expected KeywordSemicolon but found 'END_PROGRAM' - ┌─ :6:9 - │ -6 │ END_PROGRAM - │ ^^^^^^^^^^^ Unexpected token: expected KeywordSemicolon but found 'END_PROGRAM' - -error[E079]: Case condition used outside of case statement! Did you mean to use ';'? - ┌─ :3:27 +error[E048]: Could not resolve reference to StateMachine + ┌─ :3:14 │ 3 │ CASE StateMachine DELTA - │ ^^^^^ Case condition used outside of case statement! Did you mean to use ';'? + │ ^^^^^^^^^^^^ Could not resolve reference to StateMachine error[E048]: Could not resolve reference to DELTA ┌─ :3:27 diff --git a/src/parser/tests/parse_errors/snapshots/rusty__parser__tests__parse_errors__parse_error_messages_test__for_with_unexpected_token_1.snap b/src/parser/tests/parse_errors/snapshots/rusty__parser__tests__parse_errors__parse_error_messages_test__for_with_unexpected_token_1.snap index 03e765300cb..f9c81f4be45 100644 --- a/src/parser/tests/parse_errors/snapshots/rusty__parser__tests__parse_errors__parse_error_messages_test__for_with_unexpected_token_1.snap +++ b/src/parser/tests/parse_errors/snapshots/rusty__parser__tests__parse_errors__parse_error_messages_test__for_with_unexpected_token_1.snap @@ -2,20 +2,20 @@ source: src/parser/tests/parse_errors/parse_error_messages_test.rs expression: diagnostics --- -error[E007]: Unexpected token: expected KeywordAssignment but found ALPHA +error[E007]: Unexpected token: expected `:=` but found ALPHA ┌─ :3:15 │ 3 │ FOR z ALPHA x TO y DO - │ ^^^^^ Unexpected token: expected KeywordAssignment but found ALPHA + │ ^^^^^ Unexpected token: expected `:=` but found ALPHA -error[E007]: Unexpected token: expected KeywordSemicolon but found 'x TO y DO +error[E007]: Unexpected token: expected `;` but found 'x TO y DO x' ┌─ :3:21 │ 3 │ FOR z ALPHA x TO y DO │ ╭─────────────────────^ 4 │ │ x; - │ ╰─────────────^ Unexpected token: expected KeywordSemicolon but found 'x TO y DO + │ ╰─────────────^ Unexpected token: expected `;` but found 'x TO y DO x' error[E007]: Unexpected token: expected Literal but found END_FOR @@ -24,23 +24,23 @@ error[E007]: Unexpected token: expected Literal but found END_FOR 6 │ END_FOR │ ^^^^^^^ Unexpected token: expected Literal but found END_FOR -error[E007]: Unexpected token: expected KeywordSemicolon but found 'END_FOR' +error[E007]: Unexpected token: expected `;` but found 'END_FOR' ┌─ :6:9 │ 6 │ END_FOR - │ ^^^^^^^ Unexpected token: expected KeywordSemicolon but found 'END_FOR' + │ ^^^^^^^ Unexpected token: expected `;` but found 'END_FOR' -error[E006]: Missing expected Token [KeywordSemicolon, KeywordColon] +error[E006]: Missing expected Token `;` or `:` ┌─ :7:9 │ 7 │ END_PROGRAM - │ ^^^^^^^^^^^ Missing expected Token [KeywordSemicolon, KeywordColon] + │ ^^^^^^^^^^^ Missing expected Token `;` or `:` -error[E007]: Unexpected token: expected KeywordSemicolon but found 'END_PROGRAM' +error[E007]: Unexpected token: expected `;` but found 'END_PROGRAM' ┌─ :7:9 │ 7 │ END_PROGRAM - │ ^^^^^^^^^^^ Unexpected token: expected KeywordSemicolon but found 'END_PROGRAM' + │ ^^^^^^^^^^^ Unexpected token: expected `;` but found 'END_PROGRAM' error[E048]: Could not resolve reference to ALPHA ┌─ :3:15 diff --git a/src/parser/tests/parse_errors/snapshots/rusty__parser__tests__parse_errors__parse_error_messages_test__for_with_unexpected_token_2.snap b/src/parser/tests/parse_errors/snapshots/rusty__parser__tests__parse_errors__parse_error_messages_test__for_with_unexpected_token_2.snap index 03df0c1beda..57a192f4b95 100644 --- a/src/parser/tests/parse_errors/snapshots/rusty__parser__tests__parse_errors__parse_error_messages_test__for_with_unexpected_token_2.snap +++ b/src/parser/tests/parse_errors/snapshots/rusty__parser__tests__parse_errors__parse_error_messages_test__for_with_unexpected_token_2.snap @@ -2,20 +2,20 @@ source: src/parser/tests/parse_errors/parse_error_messages_test.rs expression: diagnostics --- -error[E007]: Unexpected token: expected KeywordTo but found BRAVO +error[E007]: Unexpected token: expected `TO` but found BRAVO ┌─ :3:20 │ 3 │ FOR z := x BRAVO y DO - │ ^^^^^ Unexpected token: expected KeywordTo but found BRAVO + │ ^^^^^ Unexpected token: expected `TO` but found BRAVO -error[E007]: Unexpected token: expected KeywordSemicolon but found 'y DO +error[E007]: Unexpected token: expected `;` but found 'y DO x' ┌─ :3:26 │ 3 │ FOR z := x BRAVO y DO │ ╭──────────────────────────^ 4 │ │ x; - │ ╰─────────────^ Unexpected token: expected KeywordSemicolon but found 'y DO + │ ╰─────────────^ Unexpected token: expected `;` but found 'y DO x' error[E007]: Unexpected token: expected Literal but found END_FOR @@ -24,23 +24,23 @@ error[E007]: Unexpected token: expected Literal but found END_FOR 6 │ END_FOR │ ^^^^^^^ Unexpected token: expected Literal but found END_FOR -error[E007]: Unexpected token: expected KeywordSemicolon but found 'END_FOR' +error[E007]: Unexpected token: expected `;` but found 'END_FOR' ┌─ :6:9 │ 6 │ END_FOR - │ ^^^^^^^ Unexpected token: expected KeywordSemicolon but found 'END_FOR' + │ ^^^^^^^ Unexpected token: expected `;` but found 'END_FOR' -error[E006]: Missing expected Token [KeywordSemicolon, KeywordColon] +error[E006]: Missing expected Token `;` or `:` ┌─ :7:9 │ 7 │ END_PROGRAM - │ ^^^^^^^^^^^ Missing expected Token [KeywordSemicolon, KeywordColon] + │ ^^^^^^^^^^^ Missing expected Token `;` or `:` -error[E007]: Unexpected token: expected KeywordSemicolon but found 'END_PROGRAM' +error[E007]: Unexpected token: expected `;` but found 'END_PROGRAM' ┌─ :7:9 │ 7 │ END_PROGRAM - │ ^^^^^^^^^^^ Unexpected token: expected KeywordSemicolon but found 'END_PROGRAM' + │ ^^^^^^^^^^^ Unexpected token: expected `;` but found 'END_PROGRAM' error[E048]: Could not resolve reference to BRAVO ┌─ :3:20 diff --git a/src/parser/tests/parse_errors/snapshots/rusty__parser__tests__parse_errors__parse_error_messages_test__if_then_with_unexpected_token.snap b/src/parser/tests/parse_errors/snapshots/rusty__parser__tests__parse_errors__parse_error_messages_test__if_then_with_unexpected_token.snap index f84939a06ee..5c4dd4806b2 100644 --- a/src/parser/tests/parse_errors/snapshots/rusty__parser__tests__parse_errors__parse_error_messages_test__if_then_with_unexpected_token.snap +++ b/src/parser/tests/parse_errors/snapshots/rusty__parser__tests__parse_errors__parse_error_messages_test__if_then_with_unexpected_token.snap @@ -2,59 +2,26 @@ source: src/parser/tests/parse_errors/parse_error_messages_test.rs expression: diagnostics --- -error[E007]: Unexpected token: expected KeywordThen but found CHARLIE +error[E006]: Missing expected Token `THEN` ┌─ :3:17 │ 3 │ IF TRUE CHARLIE - │ ^^^^^^^ Unexpected token: expected KeywordThen but found CHARLIE + │ ^^^^^^^ Missing expected Token `THEN` -error[E007]: Unexpected token: expected KeywordSemicolon but found 'x' +error[E007]: Unexpected token: expected `;` but found 'x' ┌─ :4:13 │ 4 │ x; - │ ^ Unexpected token: expected KeywordSemicolon but found 'x' - -error[E007]: Unexpected token: expected Literal but found ELSE - ┌─ :5:9 - │ -5 │ ELSE - │ ^^^^ Unexpected token: expected Literal but found ELSE - -error[E007]: Unexpected token: expected KeywordSemicolon but found 'ELSE - y' - ┌─ :5:9 - │ -5 │ ╭ ELSE -6 │ │ y; - │ ╰─────────────^ Unexpected token: expected KeywordSemicolon but found 'ELSE - y' - -error[E007]: Unexpected token: expected Literal but found END_IF - ┌─ :7:9 - │ -7 │ END_IF - │ ^^^^^^ Unexpected token: expected Literal but found END_IF - -error[E007]: Unexpected token: expected KeywordSemicolon but found 'END_IF' - ┌─ :7:9 - │ -7 │ END_IF - │ ^^^^^^ Unexpected token: expected KeywordSemicolon but found 'END_IF' - -error[E006]: Missing expected Token [KeywordSemicolon, KeywordColon] - ┌─ :8:9 - │ -8 │ END_PROGRAM - │ ^^^^^^^^^^^ Missing expected Token [KeywordSemicolon, KeywordColon] - -error[E007]: Unexpected token: expected KeywordSemicolon but found 'END_PROGRAM' - ┌─ :8:9 - │ -8 │ END_PROGRAM - │ ^^^^^^^^^^^ Unexpected token: expected KeywordSemicolon but found 'END_PROGRAM' + │ ^ Unexpected token: expected `;` but found 'x' error[E048]: Could not resolve reference to CHARLIE ┌─ :3:17 │ 3 │ IF TRUE CHARLIE │ ^^^^^^^ Could not resolve reference to CHARLIE + +error[E048]: Could not resolve reference to y + ┌─ :6:13 + │ +6 │ y; + │ ^ Could not resolve reference to y diff --git a/src/parser/tests/parse_errors/snapshots/rusty__parser__tests__parse_errors__parse_error_messages_test__unclosed_body_error_message.snap b/src/parser/tests/parse_errors/snapshots/rusty__parser__tests__parse_errors__parse_error_messages_test__unclosed_body_error_message.snap index 39520efe9e9..d34c9478fd7 100644 --- a/src/parser/tests/parse_errors/snapshots/rusty__parser__tests__parse_errors__parse_error_messages_test__unclosed_body_error_message.snap +++ b/src/parser/tests/parse_errors/snapshots/rusty__parser__tests__parse_errors__parse_error_messages_test__unclosed_body_error_message.snap @@ -2,8 +2,8 @@ source: src/parser/tests/parse_errors/parse_error_messages_test.rs expression: diagnostics --- -error[E007]: Unexpected token: expected KeywordEndProgram but found '' +error[E007]: Unexpected token: expected `END_PROGRAM` but found '' ┌─ :5:5 │ 5 │ - │ ^ Unexpected token: expected KeywordEndProgram but found '' + │ ^ Unexpected token: expected `END_PROGRAM` but found '' diff --git a/src/parser/tests/parse_errors/snapshots/rusty__parser__tests__parse_errors__parse_error_messages_test__unexpected_token_error_message.snap b/src/parser/tests/parse_errors/snapshots/rusty__parser__tests__parse_errors__parse_error_messages_test__unexpected_token_error_message.snap index e6bf30f18ef..58cbbfa50ee 100644 --- a/src/parser/tests/parse_errors/snapshots/rusty__parser__tests__parse_errors__parse_error_messages_test__unexpected_token_error_message.snap +++ b/src/parser/tests/parse_errors/snapshots/rusty__parser__tests__parse_errors__parse_error_messages_test__unexpected_token_error_message.snap @@ -2,8 +2,8 @@ source: src/parser/tests/parse_errors/parse_error_messages_test.rs expression: diagnostics --- -error[E007]: Unexpected token: expected KeywordEndVar but found ';' +error[E007]: Unexpected token: expected `END_VAR` but found ';' ┌─ :2:21 │ 2 │ VAR ; - │ ^ Unexpected token: expected KeywordEndVar but found ';' + │ ^ Unexpected token: expected `END_VAR` but found ';' diff --git a/src/parser/tests/parse_errors/snapshots/rusty__parser__tests__parse_errors__parse_error_messages_test__unexpected_token_error_message2.snap b/src/parser/tests/parse_errors/snapshots/rusty__parser__tests__parse_errors__parse_error_messages_test__unexpected_token_error_message2.snap index eab4d8fc1f7..f622b9fc471 100644 --- a/src/parser/tests/parse_errors/snapshots/rusty__parser__tests__parse_errors__parse_error_messages_test__unexpected_token_error_message2.snap +++ b/src/parser/tests/parse_errors/snapshots/rusty__parser__tests__parse_errors__parse_error_messages_test__unexpected_token_error_message2.snap @@ -8,8 +8,8 @@ error[E007]: Unexpected token: expected StartKeyword but found SOME 1 │ SOME PROGRAM prg │ ^^^^ Unexpected token: expected StartKeyword but found SOME -error[E007]: Unexpected token: expected KeywordEndVar but found ';' +error[E007]: Unexpected token: expected `END_VAR` but found ';' ┌─ :2:21 │ 2 │ VAR ; - │ ^ Unexpected token: expected KeywordEndVar but found ';' + │ ^ Unexpected token: expected `END_VAR` but found ';' diff --git a/src/parser/tests/parse_errors/snapshots/rusty__parser__tests__parse_errors__parse_error_statements_tests__bitwise_access_error_validation.snap b/src/parser/tests/parse_errors/snapshots/rusty__parser__tests__parse_errors__parse_error_statements_tests__bitwise_access_error_validation.snap index 14fcacb640d..a02472d1fad 100644 --- a/src/parser/tests/parse_errors/snapshots/rusty__parser__tests__parse_errors__parse_error_statements_tests__bitwise_access_error_validation.snap +++ b/src/parser/tests/parse_errors/snapshots/rusty__parser__tests__parse_errors__parse_error_statements_tests__bitwise_access_error_validation.snap @@ -14,8 +14,8 @@ error[E007]: Unexpected token: expected Literal but found % 3 │ b.%f6; // f is no valid direct access modifier │ ^ Unexpected token: expected Literal but found % -error[E007]: Unexpected token: expected KeywordSemicolon but found '%f6' +error[E007]: Unexpected token: expected `;` but found '%f6' ┌─ :3:7 │ 3 │ b.%f6; // f is no valid direct access modifier - │ ^^^ Unexpected token: expected KeywordSemicolon but found '%f6' + │ ^^^ Unexpected token: expected `;` but found '%f6' diff --git a/src/parser/tests/parse_errors/snapshots/rusty__parser__tests__parse_errors__parse_error_statements_tests__case_body_with_missing_semicolon.snap b/src/parser/tests/parse_errors/snapshots/rusty__parser__tests__parse_errors__parse_error_statements_tests__case_body_with_missing_semicolon.snap index b9d94a65fe0..1754cd0f7d9 100644 --- a/src/parser/tests/parse_errors/snapshots/rusty__parser__tests__parse_errors__parse_error_statements_tests__case_body_with_missing_semicolon.snap +++ b/src/parser/tests/parse_errors/snapshots/rusty__parser__tests__parse_errors__parse_error_statements_tests__case_body_with_missing_semicolon.snap @@ -2,14 +2,8 @@ source: src/parser/tests/parse_errors/parse_error_statements_tests.rs expression: diagnostics --- -error[E006]: Missing expected Token [KeywordSemicolon, KeywordColon] +error[E006]: Missing expected Token `;` ┌─ :4:12 │ 4 │ END_CASE - │ ^^^^^^^^ Missing expected Token [KeywordSemicolon, KeywordColon] - -error[E007]: Unexpected token: expected KeywordSemicolon but found 'END_CASE' - ┌─ :4:12 - │ -4 │ END_CASE - │ ^^^^^^^^ Unexpected token: expected KeywordSemicolon but found 'END_CASE' + │ ^^^^^^^^ Missing expected Token `;` diff --git a/src/parser/tests/parse_errors/snapshots/rusty__parser__tests__parse_errors__parse_error_statements_tests__case_without_condition.snap b/src/parser/tests/parse_errors/snapshots/rusty__parser__tests__parse_errors__parse_error_statements_tests__case_without_condition.snap index f5e8bbd4bcf..13d382919bf 100644 --- a/src/parser/tests/parse_errors/snapshots/rusty__parser__tests__parse_errors__parse_error_statements_tests__case_without_condition.snap +++ b/src/parser/tests/parse_errors/snapshots/rusty__parser__tests__parse_errors__parse_error_statements_tests__case_without_condition.snap @@ -2,8 +2,4 @@ source: src/parser/tests/parse_errors/parse_error_statements_tests.rs expression: diagnostics --- -error[E007]: Unexpected token: expected Literal but found : - ┌─ :4:21 - │ -4 │ : x := 3; - │ ^ Unexpected token: expected Literal but found : + diff --git a/src/parser/tests/parse_errors/snapshots/rusty__parser__tests__parse_errors__parse_error_statements_tests__for_with_missing_semicolon_in_body.snap b/src/parser/tests/parse_errors/snapshots/rusty__parser__tests__parse_errors__parse_error_statements_tests__for_with_missing_semicolon_in_body.snap index a258ddbbd5a..bfaf5eb3637 100644 --- a/src/parser/tests/parse_errors/snapshots/rusty__parser__tests__parse_errors__parse_error_statements_tests__for_with_missing_semicolon_in_body.snap +++ b/src/parser/tests/parse_errors/snapshots/rusty__parser__tests__parse_errors__parse_error_statements_tests__for_with_missing_semicolon_in_body.snap @@ -2,14 +2,14 @@ source: src/parser/tests/parse_errors/parse_error_statements_tests.rs expression: diagnostics --- -error[E006]: Missing expected Token [KeywordSemicolon, KeywordColon] +error[E006]: Missing expected Token `;` or `:` ┌─ :4:13 │ 4 │ END_FOR - │ ^^^^^^^ Missing expected Token [KeywordSemicolon, KeywordColon] + │ ^^^^^^^ Missing expected Token `;` or `:` -error[E007]: Unexpected token: expected KeywordSemicolon but found 'END_FOR' +error[E007]: Unexpected token: expected `;` but found 'END_FOR' ┌─ :4:13 │ 4 │ END_FOR - │ ^^^^^^^ Unexpected token: expected KeywordSemicolon but found 'END_FOR' + │ ^^^^^^^ Unexpected token: expected `;` but found 'END_FOR' diff --git a/src/parser/tests/parse_errors/snapshots/rusty__parser__tests__parse_errors__parse_error_statements_tests__if_with_missing_semicolon_in_body.snap b/src/parser/tests/parse_errors/snapshots/rusty__parser__tests__parse_errors__parse_error_statements_tests__if_with_missing_semicolon_in_body.snap index 33394cabb18..83ec7cfde30 100644 --- a/src/parser/tests/parse_errors/snapshots/rusty__parser__tests__parse_errors__parse_error_statements_tests__if_with_missing_semicolon_in_body.snap +++ b/src/parser/tests/parse_errors/snapshots/rusty__parser__tests__parse_errors__parse_error_statements_tests__if_with_missing_semicolon_in_body.snap @@ -2,14 +2,14 @@ source: src/parser/tests/parse_errors/parse_error_statements_tests.rs expression: diagnostics --- -error[E006]: Missing expected Token [KeywordSemicolon, KeywordColon] +error[E006]: Missing expected Token `;` or `:` ┌─ :4:13 │ 4 │ END_IF - │ ^^^^^^ Missing expected Token [KeywordSemicolon, KeywordColon] + │ ^^^^^^ Missing expected Token `;` or `:` -error[E007]: Unexpected token: expected KeywordSemicolon but found 'END_IF' +error[E007]: Unexpected token: expected `;` but found 'END_IF' ┌─ :4:13 │ 4 │ END_IF - │ ^^^^^^ Unexpected token: expected KeywordSemicolon but found 'END_IF' + │ ^^^^^^ Unexpected token: expected `;` but found 'END_IF' diff --git a/src/parser/tests/parse_errors/snapshots/rusty__parser__tests__parse_errors__parse_error_statements_tests__illegal_semicolon_in_call_parameters.snap b/src/parser/tests/parse_errors/snapshots/rusty__parser__tests__parse_errors__parse_error_statements_tests__illegal_semicolon_in_call_parameters.snap index 647e6931fc6..9a581a31e5b 100644 --- a/src/parser/tests/parse_errors/snapshots/rusty__parser__tests__parse_errors__parse_error_statements_tests__illegal_semicolon_in_call_parameters.snap +++ b/src/parser/tests/parse_errors/snapshots/rusty__parser__tests__parse_errors__parse_error_statements_tests__illegal_semicolon_in_call_parameters.snap @@ -2,20 +2,14 @@ source: src/parser/tests/parse_errors/parse_error_statements_tests.rs expression: diagnostics --- -error[E006]: Missing expected Token [KeywordParensClose] +error[E006]: Missing expected Token `)` ┌─ :3:28 │ 3 │ buz(a,b; c); - │ ^ Missing expected Token [KeywordParensClose] + │ ^ Missing expected Token `)` -error[E007]: Unexpected token: expected KeywordParensClose but found ';' - ┌─ :3:28 - │ -3 │ buz(a,b; c); - │ ^ Unexpected token: expected KeywordParensClose but found ';' - -error[E007]: Unexpected token: expected KeywordSemicolon but found ')' +error[E007]: Unexpected token: expected `;` but found ')' ┌─ :3:31 │ 3 │ buz(a,b; c); - │ ^ Unexpected token: expected KeywordSemicolon but found ')' + │ ^ Unexpected token: expected `;` but found ')' diff --git a/src/parser/tests/parse_errors/snapshots/rusty__parser__tests__parse_errors__parse_error_statements_tests__invalid_variable_data_type_error_recovery.snap b/src/parser/tests/parse_errors/snapshots/rusty__parser__tests__parse_errors__parse_error_statements_tests__invalid_variable_data_type_error_recovery.snap index 91d7b723cbe..377b7a7fd93 100644 --- a/src/parser/tests/parse_errors/snapshots/rusty__parser__tests__parse_errors__parse_error_statements_tests__invalid_variable_data_type_error_recovery.snap +++ b/src/parser/tests/parse_errors/snapshots/rusty__parser__tests__parse_errors__parse_error_statements_tests__invalid_variable_data_type_error_recovery.snap @@ -2,38 +2,38 @@ source: src/parser/tests/parse_errors/parse_error_statements_tests.rs expression: diagnostics --- -error[E006]: Missing expected Token KeywordColon or KeywordComma +error[E006]: Missing expected Token `:` or `,` ┌─ :4:18 │ 4 │ a DINT : ; - │ ^ Missing expected Token KeywordColon or KeywordComma + │ ^ Missing expected Token `:` or `,` -error[E007]: Unexpected token: expected DataTypeDefinition but found KeywordSemicolon +error[E007]: Unexpected token: expected DataTypeDefinition but found `;` ┌─ :4:26 │ 4 │ a DINT : ; - │ ^ Unexpected token: expected DataTypeDefinition but found KeywordSemicolon + │ ^ Unexpected token: expected DataTypeDefinition but found `;` -error[E006]: Missing expected Token KeywordColon +error[E006]: Missing expected Token `:` ┌─ :6:21 │ 6 │ h , , : INT; - │ ^ Missing expected Token KeywordColon + │ ^ Missing expected Token `:` -error[E007]: Unexpected token: expected DataTypeDefinition but found KeywordComma +error[E007]: Unexpected token: expected DataTypeDefinition but found `,` ┌─ :6:21 │ 6 │ h , , : INT; - │ ^ Unexpected token: expected DataTypeDefinition but found KeywordComma + │ ^ Unexpected token: expected DataTypeDefinition but found `,` -error[E007]: Unexpected token: expected KeywordSemicolon but found ', : INT' +error[E007]: Unexpected token: expected `;` but found ', : INT' ┌─ :6:21 │ 6 │ h , , : INT; - │ ^^^^^^^ Unexpected token: expected KeywordSemicolon but found ', : INT' + │ ^^^^^^^ Unexpected token: expected `;` but found ', : INT' -error[E007]: Unexpected token: expected DataTypeDefinition but found KeywordSemicolon +error[E007]: Unexpected token: expected DataTypeDefinition but found `;` ┌─ :7:27 │ 7 │ f , INT : ; - │ ^ Unexpected token: expected DataTypeDefinition but found KeywordSemicolon + │ ^ Unexpected token: expected DataTypeDefinition but found `;` diff --git a/src/parser/tests/parse_errors/snapshots/rusty__parser__tests__parse_errors__parse_error_statements_tests__invalid_variable_name_error_recovery.snap b/src/parser/tests/parse_errors/snapshots/rusty__parser__tests__parse_errors__parse_error_statements_tests__invalid_variable_name_error_recovery.snap index 9a0830b5ba0..c974c2c6976 100644 --- a/src/parser/tests/parse_errors/snapshots/rusty__parser__tests__parse_errors__parse_error_statements_tests__invalid_variable_name_error_recovery.snap +++ b/src/parser/tests/parse_errors/snapshots/rusty__parser__tests__parse_errors__parse_error_statements_tests__invalid_variable_name_error_recovery.snap @@ -2,8 +2,8 @@ source: src/parser/tests/parse_errors/parse_error_statements_tests.rs expression: diagnostics --- -error[E007]: Unexpected token: expected KeywordEndVar but found '4 : INT;' +error[E007]: Unexpected token: expected `END_VAR` but found '4 : INT;' ┌─ :5:17 │ 5 │ 4 : INT; - │ ^^^^^^^^ Unexpected token: expected KeywordEndVar but found '4 : INT;' + │ ^^^^^^^^ Unexpected token: expected `END_VAR` but found '4 : INT;' diff --git a/src/parser/tests/parse_errors/snapshots/rusty__parser__tests__parse_errors__parse_error_statements_tests__mismatched_parantheses_recovery_test.snap b/src/parser/tests/parse_errors/snapshots/rusty__parser__tests__parse_errors__parse_error_statements_tests__mismatched_parantheses_recovery_test.snap index 7d38e15bf91..bbec38b3e0d 100644 --- a/src/parser/tests/parse_errors/snapshots/rusty__parser__tests__parse_errors__parse_error_statements_tests__mismatched_parantheses_recovery_test.snap +++ b/src/parser/tests/parse_errors/snapshots/rusty__parser__tests__parse_errors__parse_error_statements_tests__mismatched_parantheses_recovery_test.snap @@ -2,14 +2,8 @@ source: src/parser/tests/parse_errors/parse_error_statements_tests.rs expression: diagnostics --- -error[E006]: Missing expected Token [KeywordParensClose] +error[E006]: Missing expected Token `)` ┌─ :3:19 │ 3 │ (1 + 2; - │ ^ Missing expected Token [KeywordParensClose] - -error[E007]: Unexpected token: expected KeywordParensClose but found ';' - ┌─ :3:19 - │ -3 │ (1 + 2; - │ ^ Unexpected token: expected KeywordParensClose but found ';' + │ ^ Missing expected Token `)` diff --git a/src/parser/tests/parse_errors/snapshots/rusty__parser__tests__parse_errors__parse_error_statements_tests__missing_comma_in_call_parameters.snap b/src/parser/tests/parse_errors/snapshots/rusty__parser__tests__parse_errors__parse_error_statements_tests__missing_comma_in_call_parameters.snap index 8b241b67882..0ec33c33051 100644 --- a/src/parser/tests/parse_errors/snapshots/rusty__parser__tests__parse_errors__parse_error_statements_tests__missing_comma_in_call_parameters.snap +++ b/src/parser/tests/parse_errors/snapshots/rusty__parser__tests__parse_errors__parse_error_statements_tests__missing_comma_in_call_parameters.snap @@ -2,8 +2,8 @@ source: src/parser/tests/parse_errors/parse_error_statements_tests.rs expression: diagnostics --- -error[E007]: Unexpected token: expected KeywordParensClose but found 'c' +error[E006]: Missing expected Token `,` ┌─ :3:29 │ 3 │ buz(a,b c); - │ ^ Unexpected token: expected KeywordParensClose but found 'c' + │ ^ Missing expected Token `,` diff --git a/src/parser/tests/parse_errors/snapshots/rusty__parser__tests__parse_errors__parse_error_statements_tests__missing_semicolon_after_call.snap b/src/parser/tests/parse_errors/snapshots/rusty__parser__tests__parse_errors__parse_error_statements_tests__missing_semicolon_after_call.snap index ce1f5e11202..c27e1e8e6c3 100644 --- a/src/parser/tests/parse_errors/snapshots/rusty__parser__tests__parse_errors__parse_error_statements_tests__missing_semicolon_after_call.snap +++ b/src/parser/tests/parse_errors/snapshots/rusty__parser__tests__parse_errors__parse_error_statements_tests__missing_semicolon_after_call.snap @@ -2,8 +2,8 @@ source: src/parser/tests/parse_errors/parse_error_statements_tests.rs expression: diagnostics --- -error[E007]: Unexpected token: expected KeywordSemicolon but found 'foo()' +error[E007]: Unexpected token: expected `;` but found 'foo()' ┌─ :4:21 │ 4 │ foo(); - │ ^^^^^ Unexpected token: expected KeywordSemicolon but found 'foo()' + │ ^^^^^ Unexpected token: expected `;` but found 'foo()' diff --git a/src/parser/tests/parse_errors/snapshots/rusty__parser__tests__parse_errors__parse_error_statements_tests__nested_for_with_missing_end_for.snap b/src/parser/tests/parse_errors/snapshots/rusty__parser__tests__parse_errors__parse_error_statements_tests__nested_for_with_missing_end_for.snap index a240f9d43cc..f4dcaf95ded 100644 --- a/src/parser/tests/parse_errors/snapshots/rusty__parser__tests__parse_errors__parse_error_statements_tests__nested_for_with_missing_end_for.snap +++ b/src/parser/tests/parse_errors/snapshots/rusty__parser__tests__parse_errors__parse_error_statements_tests__nested_for_with_missing_end_for.snap @@ -2,14 +2,8 @@ source: src/parser/tests/parse_errors/parse_error_statements_tests.rs expression: diagnostics --- -error[E006]: Missing expected Token [KeywordEndFor] +error[E006]: Missing expected Token `END_FOR` ┌─ :7:9 │ 7 │ END_PROGRAM - │ ^^^^^^^^^^^ Missing expected Token [KeywordEndFor] - -error[E007]: Unexpected token: expected KeywordEndFor but found 'END_PROGRAM' - ┌─ :7:9 - │ -7 │ END_PROGRAM - │ ^^^^^^^^^^^ Unexpected token: expected KeywordEndFor but found 'END_PROGRAM' + │ ^^^^^^^^^^^ Missing expected Token `END_FOR` diff --git a/src/parser/tests/parse_errors/snapshots/rusty__parser__tests__parse_errors__parse_error_statements_tests__nested_if_with_missing_end_if.snap b/src/parser/tests/parse_errors/snapshots/rusty__parser__tests__parse_errors__parse_error_statements_tests__nested_if_with_missing_end_if.snap index 1357bf836fc..edd5291aaee 100644 --- a/src/parser/tests/parse_errors/snapshots/rusty__parser__tests__parse_errors__parse_error_statements_tests__nested_if_with_missing_end_if.snap +++ b/src/parser/tests/parse_errors/snapshots/rusty__parser__tests__parse_errors__parse_error_statements_tests__nested_if_with_missing_end_if.snap @@ -2,14 +2,8 @@ source: src/parser/tests/parse_errors/parse_error_statements_tests.rs expression: diagnostics --- -error[E006]: Missing expected Token [KeywordEndIf, KeywordElseIf, KeywordElse] +error[E006]: Missing expected Token `END_IF` ┌─ :7:9 │ 7 │ END_PROGRAM - │ ^^^^^^^^^^^ Missing expected Token [KeywordEndIf, KeywordElseIf, KeywordElse] - -error[E007]: Unexpected token: expected KeywordEndIf but found 'END_PROGRAM' - ┌─ :7:9 - │ -7 │ END_PROGRAM - │ ^^^^^^^^^^^ Unexpected token: expected KeywordEndIf but found 'END_PROGRAM' + │ ^^^^^^^^^^^ Missing expected Token `END_IF` diff --git a/src/parser/tests/parse_errors/snapshots/rusty__parser__tests__parse_errors__parse_error_statements_tests__nested_repeat_with_missing_condition_and_end_repeat.snap b/src/parser/tests/parse_errors/snapshots/rusty__parser__tests__parse_errors__parse_error_statements_tests__nested_repeat_with_missing_condition_and_end_repeat.snap index a73bcfd5dda..5a6e770eb12 100644 --- a/src/parser/tests/parse_errors/snapshots/rusty__parser__tests__parse_errors__parse_error_statements_tests__nested_repeat_with_missing_condition_and_end_repeat.snap +++ b/src/parser/tests/parse_errors/snapshots/rusty__parser__tests__parse_errors__parse_error_statements_tests__nested_repeat_with_missing_condition_and_end_repeat.snap @@ -8,14 +8,8 @@ error[E007]: Unexpected token: expected Literal but found END_PROGRAM 8 │ END_PROGRAM │ ^^^^^^^^^^^ Unexpected token: expected Literal but found END_PROGRAM -error[E006]: Missing expected Token [KeywordEndRepeat] +error[E006]: Missing expected Token `END_REPEAT` ┌─ :8:12 │ 8 │ END_PROGRAM - │ ^^^^^^^^^^^ Missing expected Token [KeywordEndRepeat] - -error[E007]: Unexpected token: expected KeywordEndRepeat but found 'END_PROGRAM' - ┌─ :8:12 - │ -8 │ END_PROGRAM - │ ^^^^^^^^^^^ Unexpected token: expected KeywordEndRepeat but found 'END_PROGRAM' + │ ^^^^^^^^^^^ Missing expected Token `END_REPEAT` diff --git a/src/parser/tests/parse_errors/snapshots/rusty__parser__tests__parse_errors__parse_error_statements_tests__nested_repeat_with_missing_end_repeat.snap b/src/parser/tests/parse_errors/snapshots/rusty__parser__tests__parse_errors__parse_error_statements_tests__nested_repeat_with_missing_end_repeat.snap index f09270d6645..ca278a318e2 100644 --- a/src/parser/tests/parse_errors/snapshots/rusty__parser__tests__parse_errors__parse_error_statements_tests__nested_repeat_with_missing_end_repeat.snap +++ b/src/parser/tests/parse_errors/snapshots/rusty__parser__tests__parse_errors__parse_error_statements_tests__nested_repeat_with_missing_end_repeat.snap @@ -2,14 +2,8 @@ source: src/parser/tests/parse_errors/parse_error_statements_tests.rs expression: diagnostics --- -error[E006]: Missing expected Token [KeywordEndRepeat] +error[E006]: Missing expected Token `END_REPEAT` ┌─ :8:12 │ 8 │ END_PROGRAM - │ ^^^^^^^^^^^ Missing expected Token [KeywordEndRepeat] - -error[E007]: Unexpected token: expected KeywordEndRepeat but found 'END_PROGRAM' - ┌─ :8:12 - │ -8 │ END_PROGRAM - │ ^^^^^^^^^^^ Unexpected token: expected KeywordEndRepeat but found 'END_PROGRAM' + │ ^^^^^^^^^^^ Missing expected Token `END_REPEAT` diff --git a/src/parser/tests/parse_errors/snapshots/rusty__parser__tests__parse_errors__parse_error_statements_tests__nested_repeat_with_missing_until_end_repeat.snap b/src/parser/tests/parse_errors/snapshots/rusty__parser__tests__parse_errors__parse_error_statements_tests__nested_repeat_with_missing_until_end_repeat.snap index 770593caca8..9928d7bef12 100644 --- a/src/parser/tests/parse_errors/snapshots/rusty__parser__tests__parse_errors__parse_error_statements_tests__nested_repeat_with_missing_until_end_repeat.snap +++ b/src/parser/tests/parse_errors/snapshots/rusty__parser__tests__parse_errors__parse_error_statements_tests__nested_repeat_with_missing_until_end_repeat.snap @@ -2,14 +2,8 @@ source: src/parser/tests/parse_errors/parse_error_statements_tests.rs expression: diagnostics --- -error[E006]: Missing expected Token [KeywordUntil, KeywordEndRepeat] +error[E006]: Missing expected Token `UNTIL` ┌─ :7:12 │ 7 │ END_PROGRAM - │ ^^^^^^^^^^^ Missing expected Token [KeywordUntil, KeywordEndRepeat] - -error[E007]: Unexpected token: expected KeywordUntil but found 'END_PROGRAM' - ┌─ :7:12 - │ -7 │ END_PROGRAM - │ ^^^^^^^^^^^ Unexpected token: expected KeywordUntil but found 'END_PROGRAM' + │ ^^^^^^^^^^^ Missing expected Token `UNTIL` diff --git a/src/parser/tests/parse_errors/snapshots/rusty__parser__tests__parse_errors__parse_error_statements_tests__nested_while_with_missing_end_while.snap b/src/parser/tests/parse_errors/snapshots/rusty__parser__tests__parse_errors__parse_error_statements_tests__nested_while_with_missing_end_while.snap index af0a9f8211e..31c8facdcc0 100644 --- a/src/parser/tests/parse_errors/snapshots/rusty__parser__tests__parse_errors__parse_error_statements_tests__nested_while_with_missing_end_while.snap +++ b/src/parser/tests/parse_errors/snapshots/rusty__parser__tests__parse_errors__parse_error_statements_tests__nested_while_with_missing_end_while.snap @@ -2,14 +2,8 @@ source: src/parser/tests/parse_errors/parse_error_statements_tests.rs expression: diagnostics --- -error[E006]: Missing expected Token [KeywordEndWhile] +error[E006]: Missing expected Token `END_WHILE` ┌─ :7:12 │ 7 │ END_PROGRAM - │ ^^^^^^^^^^^ Missing expected Token [KeywordEndWhile] - -error[E007]: Unexpected token: expected KeywordEndWhile but found 'END_PROGRAM' - ┌─ :7:12 - │ -7 │ END_PROGRAM - │ ^^^^^^^^^^^ Unexpected token: expected KeywordEndWhile but found 'END_PROGRAM' + │ ^^^^^^^^^^^ Missing expected Token `END_WHILE` diff --git a/src/parser/tests/parse_errors/snapshots/rusty__parser__tests__parse_errors__parse_error_statements_tests__pointer_type_with_wrong_keyword_to_test.snap b/src/parser/tests/parse_errors/snapshots/rusty__parser__tests__parse_errors__parse_error_statements_tests__pointer_type_with_wrong_keyword_to_test.snap index bb839c130d6..11ff86d5edb 100644 --- a/src/parser/tests/parse_errors/snapshots/rusty__parser__tests__parse_errors__parse_error_statements_tests__pointer_type_with_wrong_keyword_to_test.snap +++ b/src/parser/tests/parse_errors/snapshots/rusty__parser__tests__parse_errors__parse_error_statements_tests__pointer_type_with_wrong_keyword_to_test.snap @@ -2,14 +2,14 @@ source: src/parser/tests/parse_errors/parse_error_statements_tests.rs expression: diagnostics --- -error[E007]: Unexpected token: expected KeywordTo but found tu +error[E007]: Unexpected token: expected `TO` but found tu ┌─ :3:21 │ 3 │ POINTER tu INT; - │ ^^ Unexpected token: expected KeywordTo but found tu + │ ^^ Unexpected token: expected `TO` but found tu -error[E007]: Unexpected token: expected KeywordSemicolon but found 'INT' +error[E007]: Unexpected token: expected `;` but found 'INT' ┌─ :3:24 │ 3 │ POINTER tu INT; - │ ^^^ Unexpected token: expected KeywordSemicolon but found 'INT' + │ ^^^ Unexpected token: expected `;` but found 'INT' diff --git a/src/parser/tests/parse_errors/snapshots/rusty__parser__tests__parse_errors__parse_error_statements_tests__pointer_type_without_to_test.snap b/src/parser/tests/parse_errors/snapshots/rusty__parser__tests__parse_errors__parse_error_statements_tests__pointer_type_without_to_test.snap index 55f0dbae8cc..7a61b615429 100644 --- a/src/parser/tests/parse_errors/snapshots/rusty__parser__tests__parse_errors__parse_error_statements_tests__pointer_type_without_to_test.snap +++ b/src/parser/tests/parse_errors/snapshots/rusty__parser__tests__parse_errors__parse_error_statements_tests__pointer_type_without_to_test.snap @@ -2,8 +2,8 @@ source: src/parser/tests/parse_errors/parse_error_statements_tests.rs expression: diagnostics --- -error[E007]: Unexpected token: expected KeywordTo but found INT +error[E007]: Unexpected token: expected `TO` but found INT ┌─ :3:21 │ 3 │ POINTER INT; - │ ^^^ Unexpected token: expected KeywordTo but found INT + │ ^^^ Unexpected token: expected `TO` but found INT diff --git a/src/parser/tests/parse_errors/snapshots/rusty__parser__tests__parse_errors__parse_error_statements_tests__repeat_with_missing_semicolon_in_body.snap b/src/parser/tests/parse_errors/snapshots/rusty__parser__tests__parse_errors__parse_error_statements_tests__repeat_with_missing_semicolon_in_body.snap index 460792c7096..aae3d44656f 100644 --- a/src/parser/tests/parse_errors/snapshots/rusty__parser__tests__parse_errors__parse_error_statements_tests__repeat_with_missing_semicolon_in_body.snap +++ b/src/parser/tests/parse_errors/snapshots/rusty__parser__tests__parse_errors__parse_error_statements_tests__repeat_with_missing_semicolon_in_body.snap @@ -2,14 +2,14 @@ source: src/parser/tests/parse_errors/parse_error_statements_tests.rs expression: diagnostics --- -error[E006]: Missing expected Token [KeywordSemicolon, KeywordColon] +error[E006]: Missing expected Token `;` or `:` ┌─ :4:13 │ 4 │ UNTIL x = y END_REPEAT - │ ^^^^^ Missing expected Token [KeywordSemicolon, KeywordColon] + │ ^^^^^ Missing expected Token `;` or `:` -error[E007]: Unexpected token: expected KeywordSemicolon but found 'UNTIL' +error[E007]: Unexpected token: expected `;` but found 'UNTIL' ┌─ :4:13 │ 4 │ UNTIL x = y END_REPEAT - │ ^^^^^ Unexpected token: expected KeywordSemicolon but found 'UNTIL' + │ ^^^^^ Unexpected token: expected `;` but found 'UNTIL' diff --git a/src/parser/tests/parse_errors/snapshots/rusty__parser__tests__parse_errors__parse_error_statements_tests__while_with_missing_do.snap b/src/parser/tests/parse_errors/snapshots/rusty__parser__tests__parse_errors__parse_error_statements_tests__while_with_missing_do.snap index 13a9f15a66b..c740569fd5b 100644 --- a/src/parser/tests/parse_errors/snapshots/rusty__parser__tests__parse_errors__parse_error_statements_tests__while_with_missing_do.snap +++ b/src/parser/tests/parse_errors/snapshots/rusty__parser__tests__parse_errors__parse_error_statements_tests__while_with_missing_do.snap @@ -2,8 +2,8 @@ source: src/parser/tests/parse_errors/parse_error_statements_tests.rs expression: diagnostics --- -error[E006]: Missing expected Token KeywordDo +error[E006]: Missing expected Token `DO` ┌─ :3:17 │ 3 │ y := x; - │ ^ Missing expected Token KeywordDo + │ ^ Missing expected Token `DO` diff --git a/src/parser/tests/parse_errors/snapshots/rusty__parser__tests__parse_errors__parse_error_statements_tests__while_with_missing_semicolon_in_body.snap b/src/parser/tests/parse_errors/snapshots/rusty__parser__tests__parse_errors__parse_error_statements_tests__while_with_missing_semicolon_in_body.snap index 031e18ef427..55a42da9003 100644 --- a/src/parser/tests/parse_errors/snapshots/rusty__parser__tests__parse_errors__parse_error_statements_tests__while_with_missing_semicolon_in_body.snap +++ b/src/parser/tests/parse_errors/snapshots/rusty__parser__tests__parse_errors__parse_error_statements_tests__while_with_missing_semicolon_in_body.snap @@ -2,14 +2,14 @@ source: src/parser/tests/parse_errors/parse_error_statements_tests.rs expression: diagnostics --- -error[E006]: Missing expected Token [KeywordSemicolon, KeywordColon] +error[E006]: Missing expected Token `;` or `:` ┌─ :4:13 │ 4 │ END_WHILE - │ ^^^^^^^^^ Missing expected Token [KeywordSemicolon, KeywordColon] + │ ^^^^^^^^^ Missing expected Token `;` or `:` -error[E007]: Unexpected token: expected KeywordSemicolon but found 'END_WHILE' +error[E007]: Unexpected token: expected `;` but found 'END_WHILE' ┌─ :4:13 │ 4 │ END_WHILE - │ ^^^^^^^^^ Unexpected token: expected KeywordSemicolon but found 'END_WHILE' + │ ^^^^^^^^^ Unexpected token: expected `;` but found 'END_WHILE' diff --git a/src/parser/tests/snapshots/rusty__parser__tests__container_parser_tests__actions_with_invalid_token.snap b/src/parser/tests/snapshots/rusty__parser__tests__container_parser_tests__actions_with_invalid_token.snap index 38be18c02ed..992551c68ba 100644 --- a/src/parser/tests/snapshots/rusty__parser__tests__container_parser_tests__actions_with_invalid_token.snap +++ b/src/parser/tests/snapshots/rusty__parser__tests__container_parser_tests__actions_with_invalid_token.snap @@ -4,7 +4,7 @@ expression: errors.first().unwrap() --- Diagnostic { inner: DiagnosticsInner { - message: "Unexpected token: expected KeywordAction but found BRAVO", + message: "Unexpected token: expected `ACTION` but found BRAVO", primary_location: SourceLocation { span: Range(0:13 - 0:18), }, diff --git a/src/parser/tests/snapshots/rusty__parser__tests__function_parser_tests__function_inline_struct_return_unsupported.snap b/src/parser/tests/snapshots/rusty__parser__tests__function_parser_tests__function_inline_struct_return_unsupported.snap index 189c0738de1..5bbee4f7c08 100644 --- a/src/parser/tests/snapshots/rusty__parser__tests__function_parser_tests__function_inline_struct_return_unsupported.snap +++ b/src/parser/tests/snapshots/rusty__parser__tests__function_parser_tests__function_inline_struct_return_unsupported.snap @@ -14,20 +14,20 @@ error[E007]: Unexpected token: expected Literal but found END_STRUCT 1 │ FUNCTION foo : STRUCT x : INT; y : INT; END_STRUCT VAR_INPUT END_VAR END_FUNCTION │ ^^^^^^^^^^ Unexpected token: expected Literal but found END_STRUCT -error[E007]: Unexpected token: expected KeywordSemicolon but found 'END_STRUCT VAR_INPUT END_VAR' +error[E007]: Unexpected token: expected `;` but found 'END_STRUCT VAR_INPUT END_VAR' ┌─ :1:41 │ 1 │ FUNCTION foo : STRUCT x : INT; y : INT; END_STRUCT VAR_INPUT END_VAR END_FUNCTION - │ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Unexpected token: expected KeywordSemicolon but found 'END_STRUCT VAR_INPUT END_VAR' + │ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Unexpected token: expected `;` but found 'END_STRUCT VAR_INPUT END_VAR' -error[E006]: Missing expected Token [KeywordSemicolon, KeywordColon] +error[E006]: Missing expected Token `;` or `:` ┌─ :1:70 │ 1 │ FUNCTION foo : STRUCT x : INT; y : INT; END_STRUCT VAR_INPUT END_VAR END_FUNCTION - │ ^^^^^^^^^^^^ Missing expected Token [KeywordSemicolon, KeywordColon] + │ ^^^^^^^^^^^^ Missing expected Token `;` or `:` -error[E007]: Unexpected token: expected KeywordSemicolon but found 'END_FUNCTION' +error[E007]: Unexpected token: expected `;` but found 'END_FUNCTION' ┌─ :1:70 │ 1 │ FUNCTION foo : STRUCT x : INT; y : INT; END_STRUCT VAR_INPUT END_VAR END_FUNCTION - │ ^^^^^^^^^^^^ Unexpected token: expected KeywordSemicolon but found 'END_FUNCTION' + │ ^^^^^^^^^^^^ Unexpected token: expected `;` but found 'END_FUNCTION' diff --git a/src/parser/tests/snapshots/rusty__parser__tests__type_parser_tests__enum_with_call_statement_as_variant_should_error.snap b/src/parser/tests/snapshots/rusty__parser__tests__type_parser_tests__enum_with_call_statement_as_variant_should_error.snap index 8a056cde9a5..0bdeeac49f9 100644 --- a/src/parser/tests/snapshots/rusty__parser__tests__type_parser_tests__enum_with_call_statement_as_variant_should_error.snap +++ b/src/parser/tests/snapshots/rusty__parser__tests__type_parser_tests__enum_with_call_statement_as_variant_should_error.snap @@ -2,14 +2,14 @@ source: src/parser/tests/type_parser_tests.rs expression: diagnostics --- -error[E007]: Unexpected token: expected KeywordParensClose but found '(' +error[E007]: Unexpected token: expected `)` but found '(' ┌─ :2:37 │ 2 │ TYPE State : (Idle := 0, foo()); - │ ^ Unexpected token: expected KeywordParensClose but found '(' + │ ^ Unexpected token: expected `)` but found '(' -error[E007]: Unexpected token: expected KeywordSemicolon but found ')' +error[E007]: Unexpected token: expected `;` but found ')' ┌─ :2:39 │ 2 │ TYPE State : (Idle := 0, foo()); - │ ^ Unexpected token: expected KeywordSemicolon but found ')' + │ ^ Unexpected token: expected `;` but found ')' diff --git a/src/parser/tests/snapshots/rusty__parser__tests__type_parser_tests__enum_with_equality_operator_instead_of_assignment_should_error.snap b/src/parser/tests/snapshots/rusty__parser__tests__type_parser_tests__enum_with_equality_operator_instead_of_assignment_should_error.snap index cf290c3d11e..fe99296a806 100644 --- a/src/parser/tests/snapshots/rusty__parser__tests__type_parser_tests__enum_with_equality_operator_instead_of_assignment_should_error.snap +++ b/src/parser/tests/snapshots/rusty__parser__tests__type_parser_tests__enum_with_equality_operator_instead_of_assignment_should_error.snap @@ -2,8 +2,8 @@ source: src/parser/tests/type_parser_tests.rs expression: diagnostics --- -error[E007]: Unexpected token: expected KeywordParensClose but found '= 1' +error[E007]: Unexpected token: expected `)` but found '= 1' ┌─ :2:42 │ 2 │ TYPE State : (Idle := 0, Running = 1); - │ ^^^ Unexpected token: expected KeywordParensClose but found '= 1' + │ ^^^ Unexpected token: expected `)` but found '= 1' diff --git a/src/parser/tests/snapshots/rusty__parser__tests__type_parser_tests__enum_with_qualified_member_variant_should_error.snap b/src/parser/tests/snapshots/rusty__parser__tests__type_parser_tests__enum_with_qualified_member_variant_should_error.snap index 490ea5e165b..445a5c497b6 100644 --- a/src/parser/tests/snapshots/rusty__parser__tests__type_parser_tests__enum_with_qualified_member_variant_should_error.snap +++ b/src/parser/tests/snapshots/rusty__parser__tests__type_parser_tests__enum_with_qualified_member_variant_should_error.snap @@ -2,8 +2,8 @@ source: src/parser/tests/type_parser_tests.rs expression: diagnostics --- -error[E007]: Unexpected token: expected KeywordParensClose but found '.Fast, Running.Slow' +error[E007]: Unexpected token: expected `)` but found '.Fast, Running.Slow' ┌─ :2:41 │ 2 │ TYPE State : (Idle := 0, Running.Fast, Running.Slow); - │ ^^^^^^^^^^^^^^^^^^^ Unexpected token: expected KeywordParensClose but found '.Fast, Running.Slow' + │ ^^^^^^^^^^^^^^^^^^^ Unexpected token: expected `)` but found '.Fast, Running.Slow' diff --git a/src/parser/tests/type_parser_tests.rs b/src/parser/tests/type_parser_tests.rs index ed1eb8f7167..96b030266a0 100644 --- a/src/parser/tests/type_parser_tests.rs +++ b/src/parser/tests/type_parser_tests.rs @@ -558,12 +558,12 @@ fn enum_mixed_style_type_before_and_after_list() { ); // This should produce a diagnostic since types are specified twice assert!(!diagnostics.is_empty(), "Expected diagnostic for mixed enum type syntax"); - assert_snapshot!(diagnostics, @r" - error[E007]: Unexpected token: expected KeywordSemicolon but found 'DWORD' + assert_snapshot!(diagnostics, @" + error[E007]: Unexpected token: expected `;` but found 'DWORD' ┌─ :2:44 │ 2 │ TYPE MyEnum : INT (a := 1, b := 2) DWORD; - │ ^^^^^ Unexpected token: expected KeywordSemicolon but found 'DWORD' + │ ^^^^^ Unexpected token: expected `;` but found 'DWORD' "); } @@ -912,18 +912,18 @@ fn enum_with_no_elements_produces_syntax_error() { "#, ); assert!(!diagnostics.is_empty()); - assert_snapshot!(diagnostics, @r" + assert_snapshot!(diagnostics, @" error[E007]: Unexpected token: expected Literal but found ) ┌─ :2:32 │ 2 │ TYPE EMPTY_ENUM : INT (); │ ^ Unexpected token: expected Literal but found ) - error[E007]: Unexpected token: expected KeywordEndType but found '' + error[E007]: Unexpected token: expected `END_TYPE` but found '' ┌─ :6:9 │ 6 │ - │ ^ Unexpected token: expected KeywordEndType but found '' + │ ^ Unexpected token: expected `END_TYPE` but found '' "); // User type should still be created despite the error (error recovery) assert_debug_snapshot!(result.user_types[0], @r#" diff --git a/src/validation/tests/snapshots/rusty__validation__tests__array_validation_test__assignment_1d.snap b/src/validation/tests/snapshots/rusty__validation__tests__array_validation_test__assignment_1d.snap index 851c3062f04..1a53001e11f 100644 --- a/src/validation/tests/snapshots/rusty__validation__tests__array_validation_test__assignment_1d.snap +++ b/src/validation/tests/snapshots/rusty__validation__tests__array_validation_test__assignment_1d.snap @@ -2,35 +2,35 @@ source: src/validation/tests/array_validation_test.rs expression: diagnostics --- -error[E007]: Unexpected token: expected KeywordSquareParensClose but found ) +error[E007]: Unexpected token: expected `]` but found ) ┌─ :15:34 │ 15 │ arr := [1, 2, 3, 4, 5); - │ ^ Unexpected token: expected KeywordSquareParensClose but found ) + │ ^ Unexpected token: expected `]` but found ) -error[E007]: Unexpected token: expected KeywordSemicolon but found ')' +error[E007]: Unexpected token: expected `;` but found ')' ┌─ :15:34 │ 15 │ arr := [1, 2, 3, 4, 5); - │ ^ Unexpected token: expected KeywordSemicolon but found ')' + │ ^ Unexpected token: expected `;` but found ')' -error[E007]: Unexpected token: expected KeywordParensClose but found ']' +error[E007]: Unexpected token: expected `)` but found ']' ┌─ :16:34 │ 16 │ arr := (1, 2, 3, 4, 5]; - │ ^ Unexpected token: expected KeywordParensClose but found ']' + │ ^ Unexpected token: expected `)` but found ']' -error[E006]: Missing expected Token [KeywordParensClose] +error[E006]: Missing expected Token `)` ┌─ :16:35 │ 16 │ arr := (1, 2, 3, 4, 5]; - │ ^ Missing expected Token [KeywordParensClose] + │ ^ Missing expected Token `)` -error[E007]: Unexpected token: expected KeywordParensClose but found ';' +error[E007]: Unexpected token: expected `)` but found ';' ┌─ :16:35 │ 16 │ arr := (1, 2, 3, 4, 5]; - │ ^ Unexpected token: expected KeywordParensClose but found ';' + │ ^ Unexpected token: expected `)` but found ';' error[E043]: Too many initial values for array `arr`. Expected 5, found 6. ┌─ :4:54