antlr/antlr4 · error · ValueError

replace op boundaries of {} overlap with previous {}

Error message

replace op boundaries of {} overlap with previous {}

What it means

During reduceProgramToSingleCommandPerIndex(), the rewriter folds overlapping rewrite operations into one command per token index. Two ReplaceOps whose ranges intersect (and are not strictly nested, and not both pure deletions) cannot be merged unambiguously, so ANTLR raises this ValueError instead of guessing the resulting text.

Source

Thrown at runtime/Python3/src/antlr4/TokenStreamRewriter.py:167

                    rewrites[iop.instructionIndex] = None
                    rop.text = '{}{}'.format(iop.text, rop.text)
                elif all((iop.index > rop.index, iop.index <= rop.last_index)):
                    rewrites[iop.instructionIndex] = None

            # Drop any prior replaces contained within
            prevReplaces = [op for op in rewrites[:i] if isinstance(op, TokenStreamRewriter.ReplaceOp)]
            for prevRop in prevReplaces:
                if all((prevRop.index >= rop.index, prevRop.last_index <= rop.last_index)):
                    rewrites[prevRop.instructionIndex] = None
                    continue
                isDisjoint = any((prevRop.last_index<rop.index, prevRop.index>rop.last_index))
                if all((prevRop.text is None, rop.text is None, not isDisjoint)):
                    rewrites[prevRop.instructionIndex] = None
                    rop.index = min(prevRop.index, rop.index)
                    rop.last_index = min(prevRop.last_index, rop.last_index)
                    print('New rop {}'.format(rop))
                elif (not(isDisjoint)):
                    raise ValueError("replace op boundaries of {} overlap with previous {}".format(rop, prevRop))

        # Walk inserts
        for i, iop in enumerate(rewrites):
            if any((iop is None, not isinstance(iop, TokenStreamRewriter.InsertBeforeOp))):
                continue
            prevInserts = [op for op in rewrites[:i] if isinstance(op, TokenStreamRewriter.InsertBeforeOp)]
            for prev_index, prevIop in enumerate(prevInserts):
                if prevIop.index == iop.index and type(prevIop) is TokenStreamRewriter.InsertBeforeOp:
                    iop.text += prevIop.text
                    rewrites[prev_index] = None
                elif prevIop.index == iop.index and type(prevIop) is TokenStreamRewriter.InsertAfterOp:
                    iop.text = prevIop.text + iop.text
                    rewrites[prev_index] = None
            # look for replaces where iop.index is in range; error
            prevReplaces = [op for op in rewrites[:i] if isinstance(op, TokenStreamRewriter.ReplaceOp)]
            for rop in prevReplaces:
                if iop.index == rop.index:
                    rop.text = iop.text + rop.text

View on GitHub (pinned to 7d5770395b)

Solutions

  1. Use a separate program per independent edit batch: rewriter.replace('pass1', ...) / rewriter.replace('pass2', ...), or call rewriter.getText() between batches.
  2. Merge overlapping replaces yourself before submitting: coalesce ranges and concatenate/decide text deterministically.
  3. Sort edits by start index and rewrite them so each range is disjoint (extend ranges over gaps to eliminate partial overlaps).

Example fix

// before
rewriter.replace(2, 5, 'a')
rewriter.replace(4, 7, 'b')  // partial overlap -> ValueError on getText()

// after
rewriter.replace(2, 7, 'ab')  # single merged, deterministic edit
Defensive patterns

Strategy: validation

Validate before calling

def assert_disjoint_replaces(edits):
    # edits: list of (start, end, text) sorted by start
    edits = sorted(edits, key=lambda e: e[0])
    for (s1, e1, _), (s2, e2, _) in zip(edits, edits[1:]):
        if s2 <= e1:
            raise ValueError('edits %d..%d and %d..%d overlap; merge them first' % (s1, e1, s2, e2))
    return edits

for start, stop, text in assert_disjoint_replaces(my_edits):
    rewriter.replace(start, stop, text)

Try / catch

try:
    out = rewriter.getText()
except ValueError as e:
    if 'overlap' in str(e):
        # fall back to per-pass programs so edits never co-reside
        rewriter = TokenStreamRewriter(stream)
    else:
        raise

Prevention

When it happens

Trigger: Calling rewriter.replace() (or replaceRange/replaceRangeTokens) twice on the same program with ranges that partially overlap, e.g. replace(2,5,'a') then replace(4,7,'b'); nested ranges (prev fully inside new) and disjoint ranges are fine, partial overlap with non-None text is not.

Common situations: Applying many edits generated from different AST visits without sorting/merging; mixing inserts at the edge of a replaced range; using the default program name for edits intended for separate output passes.

Related errors


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