{"record":{"id":"1e303e39d0fd58d3","repo":"antlr/antlr4","slug":"release-called-with-an-invalid-marker","errorCode":null,"errorMessage":"release() called with an invalid marker.","messagePattern":"release\\(\\) called with an invalid marker\\.","errorType":"exception","errorClass":"IllegalStateException","httpStatus":null,"severity":"error","filePath":"runtime/Java/src/org/antlr/v4/runtime/UnbufferedCharStream.java","lineNumber":255,"sourceCode":"    @Override\n    public int mark() {\n\t\tif (numMarkers == 0) {\n\t\t\tlastCharBufferStart = lastChar;\n\t\t}\n\n\t\tint mark = -numMarkers - 1;\n\t\tnumMarkers++;\n\t\treturn mark;\n    }\n\n\t/** Decrement number of markers, resetting buffer if we hit 0.\n\t * @param marker\n\t */\n    @Override\n    public void release(int marker) {\n\t\tint expectedMark = -numMarkers;\n\t\tif ( marker!=expectedMark ) {\n\t\t\tthrow new IllegalStateException(\"release() called with an invalid marker.\");\n\t\t}\n\n\t\tnumMarkers--;\n\t\tif ( numMarkers==0 && p > 0 ) { // release buffer when we can, but don't do unnecessary work\n\t\t\t// Copy data[p]..data[n-1] to data[0]..data[(n-1)-p], reset ptrs\n\t\t\t// p is last valid char; move nothing if p==n as we have no valid char\n\t\t\tSystem.arraycopy(data, p, data, 0, n - p); // shift n-p char from p to 0\n\t\t\tn = n - p;\n\t\t\tp = 0;\n\t\t\tlastCharBufferStart = lastChar;\n\t\t}\n    }\n\n    @Override\n    public int index() {\n\t\treturn currentCharIndex;\n    }\n","sourceCodeStart":237,"sourceCodeEnd":273,"githubUrl":"https://github.com/antlr/antlr4/blob/7d5770395bb7b02eb56e7c62662cb1d7c08f42a3/runtime/Java/src/org/antlr/v4/runtime/UnbufferedCharStream.java#L237-L273","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","solutions":["Use a Deque<Integer> of marks and always release from the top (LIFO)","Wrap mark/release in try/finally so early exits cannot skip a release and break the ordering","Give each consumer its own stream instance instead of sharing one UnbufferedCharStream"],"exampleFix":"// before\nint m1 = stream.mark();\nint m2 = stream.mark();\nstream.release(m1); // throws: expected m2\n\n// after\nDeque<Integer> marks = new ArrayDeque<>();\nmarks.push(stream.mark());\nmarks.push(stream.mark());\nstream.release(marks.pop()); // m2\nstream.release(marks.pop()); // m1","handlingStrategy":"validation","validationCode":"Deque<Integer> marks = new ArrayDeque<>();\nmarks.push(stream.mark());\n// ... work ...\nif (!marks.isEmpty() && marker == marks.peek()) {\n    stream.release(marks.pop()); // only the top marker is ever valid\n} else {\n    throw new IllegalStateException(\"marker released out of order\");\n}","typeGuard":null,"tryCatchPattern":null,"preventionTips":["Always release marks in reverse order of acquisition","Pair every mark() with release() in try/finally","One stream per consumer; never share an unbuffered stream across threads"],"tags":["antlr","char-stream","mark-release","lifo","buffer"],"backgroundTag":null,"analyzedSha":"7d5770395bb7b02eb56e7c62662cb1d7c08f42a3","analyzedAt":"2026-08-14T14:47:56.354Z","schemaVersion":2},"datasetVersion":"2026-08-15T17:31:12.345Z"}