apple/pkl · error

charIndexOutOfRange

charIndexOutOfRange

Error message

charIndexOutOfRange ${index} 0 ${max}

What it means

Indexing a String with `[i]` in Pkl indexes code points; when the index is negative, beyond the last code point, or falls inside a surrogate pair boundary, Pkl throws `charIndexOutOfRange` showing the valid range 0..max. It protects against out-of-bounds character access.

Source

Thrown at pkl-core/src/main/java/org/pkl/core/ast/expression/binary/SubscriptNode.java:43

import com.oracle.truffle.api.source.SourceSection;
import org.jspecify.annotations.Nullable;
import org.pkl.core.runtime.*;
import org.pkl.core.runtime.VmReference.VmReferenceAccessError;
import org.pkl.core.runtime.VmReference.VmReferenceAccessErrorType;
import org.pkl.core.util.ErrorMessages;

@NodeInfo(shortName = "[]")
public abstract class SubscriptNode extends BinaryExpressionNode {
  protected SubscriptNode(SourceSection sourceSection) {
    super(sourceSection);
  }

  @Specialization
  @TruffleBoundary
  protected String eval(String receiver, long index) {
    var charIndex = VmUtils.codePointOffsetToCharOffset(receiver, index);
    if (charIndex == -1 || charIndex == receiver.length()) {
      throw exceptionBuilder()
          .evalError(
              "charIndexOutOfRange", index, 0, receiver.codePointCount(0, receiver.length()) - 1)
          .withSourceSection(getRightNode().getSourceSection())
          .withProgramValue("String", receiver)
          .build();
    }

    if (Character.isHighSurrogate(receiver.charAt(charIndex))
        && charIndex < receiver.length() - 1
        && Character.isLowSurrogate(receiver.charAt(charIndex + 1))) {
      return receiver.substring(charIndex, charIndex + 2);
    }
    return receiver.substring(charIndex, charIndex + 1);
  }

  @Specialization
  protected Object eval(VmList receiver, long index) {
    if (index < 0 || index >= receiver.getLength()) {

View on GitHub (pinned to f3efcbfc9b)

Solutions

  1. Bounds-check the index against `s.length - 1` (actually codePointCount - 1) before indexing
  2. Handle the empty-string case separately
  3. Use `s.substring(...)` or iterate instead of direct indexing when bounds are uncertain

Example fix

// before (Pkl)
val c = name[name.length - 1]
// after
val c = name.isEmpty() ? null : name[name.length - 1]
Defensive patterns

Strategy: validation

Validate before calling

function safeCharAt(s, i) {
  if (typeof s !== 'string' || !Number.isInteger(i)) return null;
  return (i >= 0 && i < s.length) ? s[i] : null;
}

Prevention

When it happens

Trigger: `s[index]` where index < 0 or index >= number of code points, e.g. `"abc"[5]` or `"abc"[-1]`.

Common situations: Off-by-one errors using `length` instead of `length - 1`, parsing possibly-empty strings, or indexing user-supplied offsets.

Related errors


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