Repository: apple/pkl (2025-08-25T21:02:31Z)

Issue:
Unmatched backtick in `pkl repl` causes `ArrayIndexOutOfBoundsExpression` in the lexer
Reproduced in Pkl 0.30.0-dev+3f2f0c3a.

```
» pkl repl
Welcome to Pkl 0.30.0-dev+3f2f0c3a.
Type an expression to have it evaluated.
Type :help or :examples for more information.

pkl0> `
An unexpected error has occurred. Would you mind filing a bug report?

java.lang.ArrayIndexOutOfBoundsException: Index -2147483648 out of bounds for length 1
        at org.pkl.parser.Lexer.nextChar(Lexer.java:679)
        at org.pkl.parser.Lexer.lexQuotedIdentifier(Lexer.java:493)
        at org.pkl.parser.Lexer.nextDefault(Lexer.java:220)
        at org.pkl.parser.Lexer.next(Lexer.java:70)
        at org.pkl.parser.Parser.forceNext(Parser.java:1790)
        at org.pkl.parser.Parser.init(Parser.java:106)
        at org.pkl.parser.Parser.parseReplInput(Parser.java:165)
        at org.pkl.core.repl.ReplServer.evaluate(ReplServer.java:184)
        at org.pkl.core.repl.ReplServer.handleEval(ReplServer.java:161)
        at org.pkl.core.repl.ReplServer.handleRequest(ReplServer.java:126)
        at org.pkl.cli.repl.Repl.handleEvalRequest(Repl.kt:176)
        at org.pkl.cli.repl.Repl.evaluate(Repl.kt:160)
        at org.pkl.cli.repl.Repl.run(Repl.kt:95)
        at org.pkl.cli.CliRepl.doRun(CliRepl.kt:72)
        at org.pkl.commons.cli.CliCommand.run(CliCommand.kt:46)
        at org.pkl.cli.commands.ReplCommand.run(ReplCommand.kt:31)
        at com.github.ajalt.clikt.core.CoreCliktCommandKt.parse(CoreCliktCommand.kt:107)
        at com.github.ajalt.clikt.core.CoreCliktCommandKt.main(CoreCliktCommand.kt:78)
        at com.github.ajalt.clikt.core.CoreCliktCommandKt.main(CoreCliktCommand.kt:90)
        at org.pkl.cli.Main.main$lambda$0(Main.kt:26)
        at org.pkl.commons.cli.CliMainKt.cliMain(CliMain.kt:33)
        at org.pkl.cli.Main.main(Main.kt:26)
```

This curiously does not happen with regular `pkl eval`, only on the REPL.


Discussion:
Can reproduce, thanks for the bug report!



Patch:
diff --git a/pkl-parser/src/main/java/org/pkl/parser/Lexer.java b/pkl-parser/src/main/java/org/pkl/parser/Lexer.java
index 5b7ee93af..296ecd198 100644
--- a/pkl-parser/src/main/java/org/pkl/parser/Lexer.java
+++ b/pkl-parser/src/main/java/org/pkl/parser/Lexer.java
@@ -489,7 +489,7 @@ private Token lexIdentifier() {
   }
 
   private void lexQuotedIdentifier() {
-    while (lookahead != '`' && lookahead != '\n' && lookahead != '\r') {
+    while (lookahead != '`' && lookahead != '\n' && lookahead != '\r' && lookahead != EOF) {
       nextChar();
     }
     if (lookahead == '`') {
@@ -705,6 +705,13 @@ private ParserError lexError(String msg, Span span) {
   }
 
   private ParserError unexpectedChar(char got, String didYouMean) {
+    if (got == EOF) {
+      return unexpectedChar("EOF", didYouMean);
+    }
+    return lexError("unexpectedCharacter", got, didYouMean);
+  }
+
+  private ParserError unexpectedChar(String got, String didYouMean) {
     return lexError("unexpectedCharacter", got, didYouMean);
   }
 


Test patch:
diff --git a/pkl-cli/src/test/kotlin/org/pkl/cli/repl/ReplMessagesTest.kt b/pkl-cli/src/test/kotlin/org/pkl/cli/repl/ReplMessagesTest.kt
index 145707e76..0ea92910c 100644
--- a/pkl-cli/src/test/kotlin/org/pkl/cli/repl/ReplMessagesTest.kt
+++ b/pkl-cli/src/test/kotlin/org/pkl/cli/repl/ReplMessagesTest.kt
@@ -1,5 +1,5 @@
 /*
- * Copyright © 2024 Apple Inc. and the Pkl project authors. All rights reserved.
+ * Copyright © 2024-2025 Apple Inc. and the Pkl project authors. All rights reserved.
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
@@ -64,4 +64,11 @@ class ReplMessagesTest {
       startIndex = examples.indexOf("```", endIndex + 3)
     }
   }
+
+  @Test
+  fun `handle single backtick`() {
+    val responses = server.handleRequest(ReplRequest.Eval("1", "`", true, true))
+    assertThat(responses.size).isEqualTo(1)
+    assertThat(responses).hasOnlyElementsOfType(ReplResponse.EvalError::class.java)
+  }
 }
diff --git a/pkl-core/src/test/files/LanguageSnippetTests/input/errors/singleBacktick.pkl b/pkl-core/src/test/files/LanguageSnippetTests/input/errors/singleBacktick.pkl
new file mode 100644
index 000000000..b37663f19
--- /dev/null
+++ b/pkl-core/src/test/files/LanguageSnippetTests/input/errors/singleBacktick.pkl
@@ -0,0 +1,1 @@
+`
diff --git a/pkl-core/src/test/files/LanguageSnippetTests/output/errors/singleBacktick.err b/pkl-core/src/test/files/LanguageSnippetTests/output/errors/singleBacktick.err
new file mode 100644
index 000000000..7448f433d
--- /dev/null
+++ b/pkl-core/src/test/files/LanguageSnippetTests/output/errors/singleBacktick.err
@@ -0,0 +1,7 @@
+–– Pkl Error ––
+Unexpected character `
+`. Did you mean `backquote`?
+
+x | `
+     ^
+at singleBacktick (file:///$snippetsDir/input/errors/singleBacktick.pkl)
diff --git a/pkl-parser/src/test/kotlin/org/pkl/parser/LexerTest.kt b/pkl-parser/src/test/kotlin/org/pkl/parser/LexerTest.kt
index 6cd9fbfd7..e76eebabb 100644
--- a/pkl-parser/src/test/kotlin/org/pkl/parser/LexerTest.kt
+++ b/pkl-parser/src/test/kotlin/org/pkl/parser/LexerTest.kt
@@ -17,6 +17,7 @@ package org.pkl.parser
 
 import org.assertj.core.api.Assertions.assertThat
 import org.junit.jupiter.api.Test
+import org.junit.jupiter.api.assertThrows
 
 class LexerTest {
 
@@ -46,4 +47,10 @@ class LexerTest {
     assertThat(Lexer.maybeQuoteIdentifier("this")).isEqualTo("`this`")
     assertThat(Lexer.maybeQuoteIdentifier("😀")).isEqualTo("`😀`")
   }
+
+  @Test
+  fun lexSingleBacktick() {
+    val thrown = assertThrows<ParserError> { Lexer("`").next() }
+    assertThat(thrown).hasMessageContaining("Unexpected character `EOF`")
+  }
 }