apple/pkl · error

Unexpected character

Error message

Unexpected character `{0}`. Did you mean `{1}`, `{2}` or `{3}`?

What it means

The lexer sees a `.` starting a dot-sequence that does not form a valid token. Valid forms are `.` (member access), `..` (range), `...` (spread), `...?` (null-safe spread), and `..` followed by digits (number like `1.5` handled elsewhere). Anything else, e.g. `.?` or `....`, triggers unexpectedChar with the four valid suggestions.

Solutions

  1. If optional access was intended, note Pkl uses `?.` on method/reference syntax differently — check docs and use the supported form.
  2. Fix the spread syntax: use `...` or `...?` exactly.
  3. Use a single `.` for member access or `..` for ranges.
  4. Remove the extra/stray dot characters.

Example fix

// before (TypeScript habit)
foo?.bar

// after
foo?.bar // only if supported in your Pkl version; otherwise:
foo.bar
Defensive patterns

Strategy: validation

Validate before calling

function assertValidDotSequence(src) {
  const bad = src.match(/\.{4,}|\.\?|\.\.\?\?|\.\./g);
  // allow '..' ranges but flag '.?' and 4+ dots
  const illegal = src.match(/\.\?|\.{4,}/);
  if (illegal) throw new Error(`Invalid dot sequence '${illegal[0]}' at index ${illegal.index}`);
}

Prevention

When it happens

Trigger: Writing malformed dot sequences such as `.?`, `. …` mixing characters, `..*`, or an accidental extra dot in spread/range syntax — e.g. `....` or `..?`.

Common situations: Typos in null-safe spread (`...?` written as `..?`); optional chaining habits from TypeScript (`?.`) typed into Pkl; hand-written range expressions missing a bound.

Related errors


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

Appendix: source

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

          nextChar();
          yield Token.INT_DIV;
        } else {
          throw unexpectedChar(ch, "~/");
        }
      }
      case '.' -> {
        if (lookahead == '.') {
          nextChar();
          if (lookahead == '.') {
            nextChar();
            if (lookahead == '?') {
              nextChar();
              yield Token.QSPREAD;
            } else {
              yield Token.SPREAD;
            }
          } else {
            throw unexpectedChar("..", ".", "...", "...?");
          }
        } else if (lookahead >= '0' && lookahead <= '9') {
          yield lexNumber(ch);
        } else {
          yield Token.DOT;
        }
      }
      case '`' -> {
        lexQuotedIdentifier();
        yield Token.IDENTIFIER;
      }
      case '/' -> lexSlash();
      case '"' -> lexStringStart(0);
      case '#' -> {
        if (lookahead == '!') {
          yield lexShebang();
        } else {
          yield lexStringStartPounds();

View on GitHub (pinned to f3efcbfc9b)