scalameta / metals

Scala language server with rich IDE features šŸš€
https://scalameta.org/metals/
Apache License 2.0
2.07k stars 323 forks source link

Metals crashed during string interpolation editing #6699

Closed soronpo closed 2 days ago

soronpo commented 3 weeks ago

Describe the bug

The crash occured during string interpolation editing. Not sure how to replicate. Hope this log helps:

2024.08.21 20:11:30 ERROR Failed to tokenize input for semantic tokens for C:\Users\oronpo\IdeaProjects\dfhdl\lib\src\main\scala\dfhdl\DFApp.scala
org.scalameta.UnreachableError: this code path should've been unreachable
where curr = {token = 1007, position = [8662,8830), strVal = ):
          |verilog - Verilog or SystemVerilog dialects
          |vhdl    - VHDL dialects
          |
          |Available Verilog/SystemVerilog dialects (default = , base = 0}

    at org.scalameta.UnreachableError$.raise(UnreachableError.scala:20)
    at org.scalameta.UnreachableError$.raise(UnreachableError.scala:8)
    at scala.meta.internal.tokenizers.ScalametaTokenizer.getToken(ScalametaTokenizer.scala:299)
    at scala.meta.internal.tokenizers.ScalametaTokenizer.emitContents$1(ScalametaTokenizer.scala:79)
    at scala.meta.internal.tokenizers.ScalametaTokenizer.emitTokenInterpolation$1(ScalametaTokenizer.scala:94)
    at scala.meta.internal.tokenizers.ScalametaTokenizer.emitToken$1(ScalametaTokenizer.scala:144)
    at scala.meta.internal.tokenizers.ScalametaTokenizer.loop$1(ScalametaTokenizer.scala:152)
    at scala.meta.internal.tokenizers.ScalametaTokenizer.uncachedTokenize(ScalametaTokenizer.scala:162)
    at scala.meta.internal.tokenizers.ScalametaTokenizer.$anonfun$tokenize$1(ScalametaTokenizer.scala:16)
    at scala.collection.concurrent.TrieMap.getOrElseUpdate(TrieMap.scala:960)
    at scala.meta.internal.tokenizers.ScalametaTokenizer.tokenize(ScalametaTokenizer.scala:16)
    at scala.meta.internal.tokenizers.ScalametaTokenizer$$anon$1.apply(ScalametaTokenizer.scala:313)
    at scala.meta.tokenizers.Api$XtensionTokenizeDialectInput.tokenize(Api.scala:22)
    at scala.meta.tokenizers.Api$XtensionTokenizeInputLike.tokenize(Api.scala:13)
    at scala.meta.internal.metals.SemanticTokensProvider$.getTokens(SemanticTokensProvider.scala:29)
    at scala.meta.internal.metals.SemanticTokensProvider$.provide(SemanticTokensProvider.scala:90)
    at scala.meta.internal.metals.Compilers.$anonfun$semanticTokens$3(Compilers.scala:561)
    at scala.concurrent.impl.Promise$Transformation.run(Promise.scala:467)
    at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1136)
    at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:635)
    at java.lang.Thread.run(Thread.java:840)

[Error - 8:11:30 PM] Request textDocument/foldingRange failed.
  Message: Internal error.
  Code: -32603 
java.util.concurrent.CompletionException: java.util.concurrent.ExecutionException: Boxed Exception
    at java.base/java.util.concurrent.CompletableFuture.encodeThrowable(CompletableFuture.java:332)
    at java.base/java.util.concurrent.CompletableFuture.completeThrowable(CompletableFuture.java:347)
    at java.base/java.util.concurrent.CompletableFuture$UniAccept.tryFire(CompletableFuture.java:708)
    at java.base/java.util.concurrent.CompletableFuture.postComplete(CompletableFuture.java:510)
    at java.base/java.util.concurrent.CompletableFuture.completeExceptionally(CompletableFuture.java:2162)
    at scala.meta.internal.metals.CancelTokens$.$anonfun$future$1(CancelTokens.scala:40)
    at scala.meta.internal.metals.CancelTokens$.$anonfun$future$1$adapted(CancelTokens.scala:38)
    at scala.concurrent.impl.Promise$Transformation.run(Promise.scala:484)
    at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1136)
    at java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:635)
    at java.base/java.lang.Thread.run(Thread.java:840)
Caused by: java.util.concurrent.ExecutionException: Boxed Exception
    at scala.concurrent.impl.Promise$.scala$concurrent$impl$Promise$$resolve(Promise.scala:99)
    at scala.concurrent.impl.Promise$Transformation.handleFailure(Promise.scala:444)
    at scala.concurrent.impl.Promise$Transformation.run(Promise.scala:506)
    ... 3 more
Caused by: org.scalameta.UnreachableError: this code path should've been unreachable
where curr = {token = 1007, position = [8662,8830), strVal = ):
          |verilog - Verilog or SystemVerilog dialects
          |vhdl    - VHDL dialects
          |
          |Available Verilog/SystemVerilog dialects (default = , base = 0}

    at org.scalameta.UnreachableError$.raise(UnreachableError.scala:20)
    at org.scalameta.UnreachableError$.raise(UnreachableError.scala:8)
    at scala.meta.internal.tokenizers.ScalametaTokenizer.getToken(ScalametaTokenizer.scala:299)
    at scala.meta.internal.tokenizers.ScalametaTokenizer.emitContents$1(ScalametaTokenizer.scala:79)
    at scala.meta.internal.tokenizers.ScalametaTokenizer.emitTokenInterpolation$1(ScalametaTokenizer.scala:94)
    at scala.meta.internal.tokenizers.ScalametaTokenizer.emitToken$1(ScalametaTokenizer.scala:144)
    at scala.meta.internal.tokenizers.ScalametaTokenizer.loop$1(ScalametaTokenizer.scala:152)
    at scala.meta.internal.tokenizers.ScalametaTokenizer.uncachedTokenize(ScalametaTokenizer.scala:162)
    at scala.meta.internal.tokenizers.ScalametaTokenizer.$anonfun$tokenize$1(ScalametaTokenizer.scala:16)
    at scala.collection.concurrent.TrieMap.getOrElseUpdate(TrieMap.scala:960)
    at scala.meta.internal.tokenizers.ScalametaTokenizer.tokenize(ScalametaTokenizer.scala:16)
    at scala.meta.internal.tokenizers.ScalametaTokenizer$$anon$1.apply(ScalametaTokenizer.scala:313)
    at scala.meta.tokenizers.Api$XtensionTokenizeDialectInput.tokenize(Api.scala:22)
    at scala.meta.tokenizers.Api$XtensionTokenizeInputLike.tokenize(Api.scala:13)
    at scala.meta.internal.parsers.ScannerTokens$.apply(ScannerTokens.scala:924)
    at scala.meta.internal.parsers.ScalametaParser.<init>(ScalametaParser.scala:34)
    at scala.meta.parsers.Parse$$anon$1.apply(Parse.scala:36)
    at scala.meta.internal.mtags.ScalametaCommonEnrichments$XtensionInputOffset.safeParse(ScalametaCommonEnrichments.scala:565)
    at scala.meta.internal.parsing.Trees.$anonfun$parse$2(Trees.scala:150)
    at scala.Option.map(Option.scala:242)
    at scala.meta.internal.parsing.Trees.parse(Trees.scala:141)
    at scala.meta.internal.parsing.Trees.didChange(Trees.scala:101)
    at scala.meta.internal.metals.MetalsLspService.$anonfun$parseTreesAndPublishDiags$2(MetalsLspService.scala:631)
    at scala.runtime.java8.JFunction0$mcV$sp.apply(JFunction0$mcV$sp.scala:18)
    at scala.concurrent.Future$.$anonfun$apply$1(Future.scala:687)
    at scala.concurrent.impl.Promise$Transformation.run(Promise.scala:467)
    ... 3 more

[Error - 8:11:30 PM] Request textDocument/codeAction failed.
  Message: Internal error.
  Code: -32603 
org.scalameta.UnreachableError: this code path should've been unreachable
where curr = {token = 1007, position = [8662,8830), strVal = ):
          |verilog - Verilog or SystemVerilog dialects
          |vhdl    - VHDL dialects
          |
          |Available Verilog/SystemVerilog dialects (default = , base = 0}

    at org.scalameta.UnreachableError$.raise(UnreachableError.scala:20)
    at org.scalameta.UnreachableError$.raise(UnreachableError.scala:8)
    at scala.meta.internal.tokenizers.ScalametaTokenizer.getToken(ScalametaTokenizer.scala:299)
    at scala.meta.internal.tokenizers.ScalametaTokenizer.emitContents$1(ScalametaTokenizer.scala:79)
    at scala.meta.internal.tokenizers.ScalametaTokenizer.emitTokenInterpolation$1(ScalametaTokenizer.scala:94)
    at scala.meta.internal.tokenizers.ScalametaTokenizer.emitToken$1(ScalametaTokenizer.scala:144)
    at scala.meta.internal.tokenizers.ScalametaTokenizer.loop$1(ScalametaTokenizer.scala:152)
    at scala.meta.internal.tokenizers.ScalametaTokenizer.uncachedTokenize(ScalametaTokenizer.scala:162)
    at scala.meta.internal.tokenizers.ScalametaTokenizer.$anonfun$tokenize$1(ScalametaTokenizer.scala:16)
    at scala.collection.concurrent.TrieMap.getOrElseUpdate(TrieMap.scala:960)
    at scala.meta.internal.tokenizers.ScalametaTokenizer.tokenize(ScalametaTokenizer.scala:16)
    at scala.meta.internal.tokenizers.ScalametaTokenizer$$anon$1.apply(ScalametaTokenizer.scala:313)
    at scala.meta.tokenizers.Api$XtensionTokenizeDialectInput.tokenize(Api.scala:22)
    at scala.meta.internal.metals.codeactions.StringActions.$anonfun$contribute$1(StringActions.scala:36)
    at scala.Option.flatMap(Option.scala:283)
    at scala.meta.internal.metals.codeactions.StringActions.contribute(StringActions.scala:35)
    at scala.meta.internal.metals.codeactions.CodeActionProvider$$anonfun$1.applyOrElse(CodeActionProvider.scala:79)
    at scala.meta.internal.metals.codeactions.CodeActionProvider$$anonfun$1.applyOrElse(CodeActionProvider.scala:77)
    at scala.collection.immutable.List.collect(List.scala:276)
    at scala.meta.internal.metals.codeactions.CodeActionProvider.codeActions(CodeActionProvider.scala:77)
    at scala.meta.internal.metals.MetalsLspService.$anonfun$codeAction$1(MetalsLspService.scala:1213)
    at scala.meta.internal.metals.CancelTokens$.future(CancelTokens.scala:38)
    at scala.meta.internal.metals.MetalsLspService.codeAction(MetalsLspService.scala:1212)
    at scala.meta.internal.metals.WorkspaceLspService.codeAction(WorkspaceLspService.scala:525)
    at scala.meta.metals.lsp.DelegatingScalaService.codeAction(DelegatingScalaService.scala:146)
    at jdk.internal.reflect.GeneratedMethodAccessor5.invoke(Unknown Source)
    at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
    at java.base/java.lang.reflect.Method.invoke(Method.java:569)
    at org.eclipse.lsp4j.jsonrpc.services.GenericEndpoint.lambda$recursiveFindRpcMethods$0(GenericEndpoint.java:65)
    at org.eclipse.lsp4j.jsonrpc.services.GenericEndpoint.request(GenericEndpoint.java:128)
    at org.eclipse.lsp4j.jsonrpc.RemoteEndpoint.handleRequest(RemoteEndpoint.java:271)
    at org.eclipse.lsp4j.jsonrpc.RemoteEndpoint.consume(RemoteEndpoint.java:201)
    at org.eclipse.lsp4j.jsonrpc.json.StreamMessageProducer.handleMessage(StreamMessageProducer.java:185)
    at org.eclipse.lsp4j.jsonrpc.json.StreamMessageProducer.listen(StreamMessageProducer.java:97)
    at org.eclipse.lsp4j.jsonrpc.json.ConcurrentMessageProcessor.run(ConcurrentMessageProcessor.java:114)
    at java.base/java.util.concurrent.Executors$RunnableAdapter.call(Executors.java:539)
    at java.base/java.util.concurrent.FutureTask.run(FutureTask.java:264)
    at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1136)
    at java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:635)
    at java.base/java.lang.Thread.run(Thread.java:840)

[Error - 8:11:30 PM] Client Metals: connection to server is erroring. Shutting down server.
[Error - 8:11:31 PM] Connection to server got closed. Server will not be restarted.
[Error - 8:11:31 PM] Stopping server failed
  Message: Pending response rejected since connection got disposed
  Code: -32097 
[Error - 8:11:31 PM] Stopping server failed
  Message: Pending response rejected since connection got disposed
  Code: -32097 

Expected behavior

No crash

Operating system

Windows

Editor/Extension

VS Code

Version of Metals

v1.3.5

Extra context or search terms

No response

soronpo commented 3 weeks ago

OK, the following file causes it to crash (have not attempted to minimize it further). Note the missing interpolation in line 199:

package dfhdl
import scala.collection.immutable.ListMap
import dfhdl.core
import core.{DFValAny, asValAny}
import dfhdl.compiler.ir
import wvlet.log.{Logger, LogFormatter}
import scala.collection.mutable
import scala.annotation.static
trait DFApp:
  private val logger = Logger("DFHDL App")
  logger.setFormatter(LogFormatter.BareFormatter)
  logger.info(s"Welcome to DFiant HDL (DFHDL) v$dfhdlVersion !!!")
  private var designName: String = ""
  private var topScalaPath: String = ""
  // this context is just for enabling `getConstData` to work.
  // the internal global context inside `value` will be actually at play here.
  val dfc: DFC = DFC.emptyNoEO
  case class Arg(name: String, typeName: String, value: Any, desc: String):
    def valueStr: String = value match
      case dfConst: DFValAny =>
        import dfc.getSet
        dfConst.asIR.getConstData.asInstanceOf[Option[Option[Any]]].get.get.toString()
      case _ => value.toString()
    def updateWithValueStr(updatedValueStr: String): Arg =
      val scalaTypeName = typeName.replaceFirst("DFHDL ", "")
      val updatedValueScala = scalaTypeName match
        case "Int"    => BigInt(updatedValueStr)
        case "Double" => updatedValueStr.toDouble
        case "Boolean" | "Bit" =>
          updatedValueStr match
            case "1" => true
            case "0" => false
            case _   => updatedValueStr.toBoolean
        case _ => updatedValueStr

      val updatedValue = value match
        case dfConst: DFValAny =>
          core.DFVal.Const.forced(dfConst.dfType, Some(updatedValueScala))
        case _ => updatedValueScala
      copy(value = updatedValue)
    end updateWithValueStr
  end Arg

  private var designArgs: ListMap[String, Arg] = ListMap.empty
  private var elaborationOptions: options.ElaborationOptions = null
  private var compilerOptions: options.CompilerOptions = null
  private var printerOptions: options.PrinterOptions = null
  private var linterOptions: options.LinterOptions = null
  given options.CompilerOptions = compilerOptions
  given options.PrinterOptions = printerOptions
  private var dsn: () => core.Design = null
  private var mode: String = "commit"
  private var helpMode: String = "usage"
  // used by the plugin to get the updated design arguments that could be changed by the
  // command-line options
  final protected def getDsnArg(name: String): Any =
    designArgs(name).value
  // used by the plugin to get the updated elaboration options that could be changed by the
  // command-line options
  final protected def getElaborationOptions: options.ElaborationOptions = elaborationOptions
  final protected def setInitials(
      _designName: String,
      _topScalaPath: String,
      top: dfhdl.top,
      argNames: List[String],
      argTypes: List[String],
      argValues: List[Any],
      argDescs: List[String]
  ): Unit =
    designName = _designName
    topScalaPath = _topScalaPath
    elaborationOptions = top.elaborationOptions
    compilerOptions = top.compilerOptions
    printerOptions = top.printerOptions
    designArgs = ListMap.from(
      argNames.lazyZip(argTypes).lazyZip(argValues).lazyZip(argDescs).map(
        (name, typeName, value, desc) => name -> Arg(name, typeName, value, desc)
      )
    )
  end setInitials
  final protected def setDsn(d: => core.Design): Unit = dsn = () => d
  private def elaborate: core.Design =
    logger.info("Elaborating design...")
    // the elaboration options are set in the compiler plugin using getElaborationOptions
    val elaborated = dsn()
    if (elaborationOptions.printDesignCodeAfter)
      println(
        """|~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
           |The design code after elaboration:
           |~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~""".stripMargin
      )
      elaborated.printCodeString
    elaborated

  private def programName: String =
    import dfhdl.internals.{sbtIsRunning, scala_cliIsRunning, sbtShellIsRunning}
    if (scala_cliIsRunning) s"scala-cli run . -M $topScalaPath --"
    else if (sbtIsRunning)
      if (sbtShellIsRunning) s"runMain $topScalaPath"
      else s"""sbt "runMain $topScalaPath [options]""""
    else "<your program>"
  private lazy val parser = new scopt.OptionParser[Unit](programName):
    override def terminate(exitState: Either[String, Unit]): Unit = ()
    def optDesignArg = opt[Map[String, String]]("design-args")
      .abbr("da")
      .valueName("a1=v1,a2=v2...")
      .validate(x =>
        val invalidKeys = x.keySet -- designArgs.keySet
        if (invalidKeys.nonEmpty)
          failure(s"Unrecognized design arguments: ${invalidKeys.mkString(", ")}")
        else
          val typeMismatches = x.flatMap((argName, updatedValueStr) =>
            val isValid =
              designArgs(argName).typeName match
                case "String" | "DFHDL String" => true
                case "Int" | "DFHDL Int"       => updatedValueStr.toIntOption.nonEmpty
                case "Double" | "DFHDL Double" => updatedValueStr.toDoubleOption.nonEmpty
                case "Boolean" | "Bit" | "DFHDL Boolean" | "DFHDL Bit" =>
                  updatedValueStr.toBooleanOption
                    .nonEmpty || updatedValueStr == "1" || updatedValueStr == "0"
                case _ => false
            if (isValid) None
            else Some(argName)
          )
          if (typeMismatches.nonEmpty)
            failure(s"Type mismatch in design arguments: ${typeMismatches.mkString(", ")}")
          else success
        end if
      )
      .foreach(_.foreach { (argName, updatedValueStr) =>
        designArgs =
          designArgs.updatedWith(argName)(ao => Some(ao.get.updateWithValueStr(updatedValueStr)))
      })
      .text("design arguments (run list-design-args command to get a full list)")
    def optLinter =
      val accepted = Set("verilator", "ghdl")
      opt[String]("linter")
        .validate(x =>
          if (accepted.contains(x)) success
          else failure(s"Acceptable values are: ${accepted.mkString(", ")}")
        )
        .text(s"linter tool: ${accepted.mkString(", ")}")
    head(s"Design Name: $designName")
    optDesignArg
    opt[String]("backend")
      .abbr("b")
      .foreach(_ => compilerOptions = compilerOptions.copy(printDesignCodeAfter = true))
      .valueName("<lang>[:<dialect>]")
      .text("backend selection (run `help backend` to get full list of languages and dialects)")
    opt[Unit]("print-elaborate")
      .abbr("pe")
      .foreach(_ => elaborationOptions = elaborationOptions.copy(printDesignCodeAfter = true))
      .text("print the DFHDL design after elaboration")
    opt[Unit]("print-compile")
      .abbr("pc")
      .foreach(_ => compilerOptions = compilerOptions.copy(printDesignCodeAfter = true))
      .text("print the DFHDL design after compilation")
    opt[Unit]("print-backend")
      .abbr("pb")
      .foreach(_ => compilerOptions = compilerOptions.copy(printGenFiles = true))
      .text("print the backend design after compilation")
    cmd("help")
      .foreach(_ => mode = "help")
      .text("Display usage text")
      .children(
        cmd("backend")
          .foreach(_ => helpMode = "backend")
          .text("List all backend languages and dialects"),
        cmd("design-args")
          .foreach(_ => helpMode = "design-args")
          .text("List all design arguments")
      )
    cmd("elaborate")
      .foreach(_ => mode = "elaborate")
      .text("Mode: Elaboration only (no compilation)")
    cmd("compile")
      .foreach(_ => mode = "compile")
      .text("Mode: Compilation (after elaboration, and WITHOUT committing files to disk)")
    cmd("commit")
      .foreach(_ => mode = "commit")
      .text("Mode: Committing to disk (after elaboration and compilation) [default]")
    cmd("lint")
      .foreach(_ => mode = "lint")
      .text("Mode: Linting (after elaboration, compilation, and committing to disk)")
      .children(optLinter)
  private def listDesignArgs: Unit =
    println("Design arguments:")
    val titles = f"${"Name"}%-20s${"Type"}%-20s${"Default"}%-20sDescription"
    println(titles)
    println("-" * titles.length)
    designArgs.values.foreach(a =>
      println(f"${a.name}%-20s${a.typeName}%-20s${a.valueStr}%-20s${a.desc}")
    )
  private def listBackends: Unit =
    val (defaultLang, defaultDialect) = compilerOptions.backend match
      case backend: dfhdl.backends.verilog => ("verilog", backend.dialect.toString())
      case backend: dfhdl.backends.vhdl    => ("vhdl", backend.dialect.toString())
    println(
      s"""|Available languages (default = $):
          |verilog - Verilog or SystemVerilog dialects
          |vhdl    - VHDL dialects
          |
          |Available Verilog/SystemVerilog dialects (default = $defaultDialect):
          |v2001   - Verilog 2001
          |sv2005  - 
          |""".stripMargin
    )
  end listBackends

  def main(args: Array[String]): Unit =
    if (parser.parse(args, ()).isDefined)
      mode match
        case "help" =>
          helpMode match
            case "backend"     => listBackends
            case "design-args" => listDesignArgs
            case _             => println(parser.usage)
        case "elaborate" => elaborate
        case "compile"   => elaborate.compile
        case "commit"    => elaborate.compile.commit
        case "lint"      => elaborate.compile.commit.lint
        case _           =>
  end main
end DFApp
kasiaMarek commented 3 weeks ago

Thanks for the report. Looks like an issue with scalameta tokenizer. Also in Metals instead of throwing we should probably create an error report and return an empty list of semantic tokens.