Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix loop in parser #15421

Merged
merged 2 commits into from
Jun 13, 2022
Merged

Fix loop in parser #15421

merged 2 commits into from
Jun 13, 2022

Conversation

odersky
Copy link
Contributor

@odersky odersky commented Jun 11, 2022

I noted a case in Metals where the compiler would keep running at 100+ % until the process was killed.

Using jstack I tracked it down to an infinite skip caused by a syntaxError in pattern3. In fact,
the syntaxError should not skip at this point since the offending expression was already fully parsed.
I fixed this in this commit.

The parser was invoked from the signatureHelp method. It seems it parsed something that was not
syntactically correct (specifically, a postfix * appeared in a pattern where none was allowed).

`

I noted a case in Metals where the compiler would keep running at 100+ % until the process was killed.

Using jstack I tracked it down to an infinite `skip` caused by a `syntaxError` in `pattern3`. In fact,
the syntaxError should not skip at this point since the offending expression was already fully parsed.
I fixed this in this commit.

The parser was invoked from the `signatureHelp` method. It seems it parsed something that was not
syntactically correct (specifically, a postfix `*` appeared in a pattern where none was allowed).

`
@odersky
Copy link
Contributor Author

odersky commented Jun 11, 2022

The fix is actually not sufficient. I just found another infinite loop in the Parser, this time here:

"pool-4-thread-1" #82 prio=5 os_prio=31 tid=0x00007f7eb4308800 nid=0x12d53 runnable [0x0000700012a20000]
   java.lang.Thread.State: RUNNABLE
	at dotty.tools.dotc.parsing.Scanners$TokenData.copyFrom(Scanners.scala:61)
	at dotty.tools.dotc.parsing.Scanners$TokenData.copyFrom$(Scanners.scala:35)
	at dotty.tools.dotc.parsing.Scanners$ScannerCommon.copyFrom(Scanners.scala:97)
	at dotty.tools.dotc.parsing.Scanners$Scanner.getNextToken(Scanners.scala:382)
	at dotty.tools.dotc.parsing.Scanners$Scanner.nextToken(Scanners.scala:390)
	at dotty.tools.dotc.parsing.Scanners$Scanner.skip(Scanners.scala:309)
	at dotty.tools.dotc.parsing.Parsers$Parser.skip(Parsers.scala:256)
	at dotty.tools.dotc.parsing.Parsers$Parser.syntaxErrorOrIncomplete(Parsers.scala:281)
	at dotty.tools.dotc.parsing.Parsers$Parser.simpleExpr(Parsers.scala:2306)
	at dotty.tools.dotc.parsing.Parsers$Parser.$init$$$anonfun$9(Parsers.scala:2237)

It's again called from signatureHelp. It seems it always stays in the skip method of Scanner, in the while loop at the bottom of this snippet:

      def atStop =
        token == EOF
        || (currentRegion eq lastRegion)
            && (isStatSep
                || closingParens.contains(token) && lastRegion.toList.exists(_.closedBy == token)
                || token == COMMA && lastRegion.toList.exists(_.commasExpected)
                || token == OUTDENT && indentWidth(offset) < lastKnownIndentWidth)
          // stop at OUTDENT if the new indentwidth is smaller than the indent width of
          // currentRegion. This corrects for the problem that sometimes we don't see an INDENT
          // when skipping and therefore might erroneously end up syncing on a nested OUTDENT.
      if debugTokenStream then
        println(s"\nSTART SKIP AT ${sourcePos().line + 1}, $this in $currentRegion")
      while !atStop do
        nextToken()

It looks like something is wrong with the way the virtual file passed to the parser is set up that makes atStop be false forever. Does it end in EOF?

@odersky
Copy link
Contributor Author

odersky commented Jun 11, 2022

Another thing to try would be to make indent syntax be false for signature helps. (I assume it's not needed). That would prevent spurious insertions of indent and outdent. You can achieve this by passing a context with -noindent setting to the parser.

Copy link
Contributor

@tgodzik tgodzik left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for looking into this! This might have caused the issue with the stuck compiler

@odersky odersky merged commit 5332a12 into scala:main Jun 13, 2022
@odersky odersky deleted the fix-parser-loop branch June 13, 2022 07:54
@griggt
Copy link
Contributor

griggt commented Jun 14, 2022

@tgodzik I've hit this issue several times in the past week using Metals 0.11.6 with projects that are on Scala 3.1. Is there a corresponding workaround on the Metals side for those codebases not on 3.2? (since this looping bug in parser seems to be longstanding)

@alishangtian
Copy link

@tgodzik I've hit this issue several times in the past week using Metals 0.11.6 with projects that are on Scala 3.1. Is there a corresponding workaround on the Metals side for those codebases not on 3.2? (since this looping bug in parser seems to be longstanding)

The fix is actually not sufficient. I just found another infinite loop in the Parser, this time here:

"pool-4-thread-1" #82 prio=5 os_prio=31 tid=0x00007f7eb4308800 nid=0x12d53 runnable [0x0000700012a20000]
   java.lang.Thread.State: RUNNABLE
	at dotty.tools.dotc.parsing.Scanners$TokenData.copyFrom(Scanners.scala:61)
	at dotty.tools.dotc.parsing.Scanners$TokenData.copyFrom$(Scanners.scala:35)
	at dotty.tools.dotc.parsing.Scanners$ScannerCommon.copyFrom(Scanners.scala:97)
	at dotty.tools.dotc.parsing.Scanners$Scanner.getNextToken(Scanners.scala:382)
	at dotty.tools.dotc.parsing.Scanners$Scanner.nextToken(Scanners.scala:390)
	at dotty.tools.dotc.parsing.Scanners$Scanner.skip(Scanners.scala:309)
	at dotty.tools.dotc.parsing.Parsers$Parser.skip(Parsers.scala:256)
	at dotty.tools.dotc.parsing.Parsers$Parser.syntaxErrorOrIncomplete(Parsers.scala:281)
	at dotty.tools.dotc.parsing.Parsers$Parser.simpleExpr(Parsers.scala:2306)
	at dotty.tools.dotc.parsing.Parsers$Parser.$init$$$anonfun$9(Parsers.scala:2237)

It's again called from signatureHelp. It seems it always stays in the skip method of Scanner, in the while loop at the bottom of this snippet:

      def atStop =
        token == EOF
        || (currentRegion eq lastRegion)
            && (isStatSep
                || closingParens.contains(token) && lastRegion.toList.exists(_.closedBy == token)
                || token == COMMA && lastRegion.toList.exists(_.commasExpected)
                || token == OUTDENT && indentWidth(offset) < lastKnownIndentWidth)
          // stop at OUTDENT if the new indentwidth is smaller than the indent width of
          // currentRegion. This corrects for the problem that sometimes we don't see an INDENT
          // when skipping and therefore might erroneously end up syncing on a nested OUTDENT.
      if debugTokenStream then
        println(s"\nSTART SKIP AT ${sourcePos().line + 1}, $this in $currentRegion")
      while !atStop do
        nextToken()

It looks like something is wrong with the way the virtual file passed to the parser is set up that makes atStop be false forever. Does it end in EOF?

oh this looks so bad

@tgodzik
Copy link
Contributor

tgodzik commented Jun 28, 2022

I am trying to fix it for older versions in https://github.com/scalameta/metals/pull/4074/files and it seems to work, but I am not sure if it should. Does anyone knows how to get an infinitly compiling code? Would be cool to test out cancelation in that case.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants