apple/pkl · error

invalidCharacter

invalidCharacter

Error message

Invalid identifier `{0}`.

What it means

The lexer's default branch cannot classify the character: it is not a digit and not a valid identifier start, so it reports "Invalid identifier" with the offending character via lexError keyed as invalidCharacter. This covers stray punctuation, unicode look-alikes, and control characters in Pkl source.

Solutions

  1. Replace the offending character with its ASCII equivalent (straight quotes, regular spaces).
  2. Quote the text as a string literal if it is data, not code.
  3. Re-save the file as clean UTF-8 without BOM; strip invisible characters.
  4. Check for language-specific syntax (e.g. decorators `@`) that Pkl does not support in that position.

Example fix

// before (smart quotes)
name = “pkl”

// after
name = "pkl"
Defensive patterns

Strategy: validation

Validate before calling

function findNonAsciiCodeChars(src) {
  const issues = [];
  const lines = src.split("\n");
  lines.forEach((line, i) => {
    const cleaned = line.replace(/"[^"]*"/g, ""); // ignore string contents
    const bad = cleaned.match(/[^\x20-\x7E\t]/g);
    if (bad) issues.push({ line: i + 1, chars: bad.map(c => c.codePointAt(0).toString(16)) });
  });
  return issues;
}

Prevention

When it happens

Trigger: A character that can't start a token appears in code — e.g. `@` outside annotations, `#` outside custom string delimiters, smart quotes from a word processor (`“` instead of `"`), non-breaking spaces, or emoji in unquoted positions.

Common situations: Copying code from docs/slack that converted quotes to smart quotes; invisible BOM or non-breaking space characters; writing DSL-like syntax from another language; stray comment markers.

Related errors


AI-assisted analysis of apple/pkl@f3efcbfc9b (2026-09-08). Data as JSON: /api/errors/e1d655d418dbd34c. Report an issue: GitHub.

Appendix: source

Thrown at pkl-parser/src/main/java/org/pkl/parser/Lexer.java:263

      case '`' -> {
        lexQuotedIdentifier();
        yield Token.IDENTIFIER;
      }
      case '/' -> lexSlash();
      case '"' -> lexStringStart(0);
      case '#' -> {
        if (lookahead == '!') {
          yield lexShebang();
        } else {
          yield lexStringStartPounds();
        }
      }
      default -> {
        if (Character.isDigit(ch)) {
          yield lexNumber(ch);
        } else if (isIdentifierStart(ch)) {
          yield lexIdentifier();
        } else throw lexError(ErrorMessages.create("invalidCharacter", (char) ch), cursor - 1, 1);
      }
    };
  }

  private Token nextString() {
    var scope = interpolationStack.getFirst();
    if (stringEnded) {
      lexStringEnd(scope);
      stringEnded = false;
      interpolationStack.pop();
      state = State.DEFAULT;
      return Token.STRING_END;
    }
    if (lookahead == EOF) return Token.EOF;
    if (isEscape) {
      isEscape = false;
      // consume the `\#*`
      for (var i = 0; i < scope.pounds + 1; i++) {

View on GitHub (pinned to f3efcbfc9b)