antlr/antlr4 · error · IllegalStateException

release() called with an invalid marker.

Error message

release() called with an invalid marker.

What it means

UnbufferedCharStream.release(marker) requires markers to be released in strict last-in-first-out order: the only valid argument is the most recent mark() result. Releasing an older marker, releasing the same marker twice, or releasing out of order corrupts the buffer-window bookkeeping, so it throws IllegalStateException.

Source

Thrown at runtime/Java/src/org/antlr/v4/runtime/UnbufferedCharStream.java:255

    @Override
    public int mark() {
		if (numMarkers == 0) {
			lastCharBufferStart = lastChar;
		}

		int mark = -numMarkers - 1;
		numMarkers++;
		return mark;
    }

	/** Decrement number of markers, resetting buffer if we hit 0.
	 * @param marker
	 */
    @Override
    public void release(int marker) {
		int expectedMark = -numMarkers;
		if ( marker!=expectedMark ) {
			throw new IllegalStateException("release() called with an invalid marker.");
		}

		numMarkers--;
		if ( numMarkers==0 && p > 0 ) { // release buffer when we can, but don't do unnecessary work
			// Copy data[p]..data[n-1] to data[0]..data[(n-1)-p], reset ptrs
			// p is last valid char; move nothing if p==n as we have no valid char
			System.arraycopy(data, p, data, 0, n - p); // shift n-p char from p to 0
			n = n - p;
			p = 0;
			lastCharBufferStart = lastChar;
		}
    }

    @Override
    public int index() {
		return currentCharIndex;
    }

View on GitHub (pinned to 7d5770395b)

Solutions

  1. Use a Deque<Integer> of marks and always release from the top (LIFO)
  2. Wrap mark/release in try/finally so early exits cannot skip a release and break the ordering
  3. Give each consumer its own stream instance instead of sharing one UnbufferedCharStream

Example fix

// before
int m1 = stream.mark();
int m2 = stream.mark();
stream.release(m1); // throws: expected m2

// after
Deque<Integer> marks = new ArrayDeque<>();
marks.push(stream.mark());
marks.push(stream.mark());
stream.release(marks.pop()); // m2
stream.release(marks.pop()); // m1
Defensive patterns

Strategy: validation

Validate before calling

Deque<Integer> marks = new ArrayDeque<>();
marks.push(stream.mark());
// ... work ...
if (!marks.isEmpty() && marker == marks.peek()) {
    stream.release(marks.pop()); // only the top marker is ever valid
} else {
    throw new IllegalStateException("marker released out of order");
}

Prevention

When it happens

Trigger: release(m1) called before release(m2) when m2 was marked after m1; calling release twice on the same marker; storing marks in a collection and releasing them in insertion order instead of reverse.

Common situations: Nested components each marking/unmarking the stream with exception paths that skip a release; asynchronous or interleaved parsing attempts sharing one stream; refactoring that changed mark/unmark nesting.

Related errors


AI-assisted analysis of antlr/antlr4@7d5770395b (2026-08-14). Data as JSON: /api/errors/1e303e39d0fd58d3. Report an issue: GitHub.