antlr/antlr4 · error · ValueError

should be only one op per index

Error message

should be only one op per index

What it means

Final invariant check of the reduction pass: after merging, at most one rewrite operation may remain per token index. If two operations of different kinds (e.g. an insert and a delete at the same index, or two inserts at the same index of different op types that were not merged) survive, the output order would be ambiguous, so ANTLR raises this ValueError.

Source

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

                    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
                    rewrites[i] = None
                    continue
                if all((iop.index >= rop.index, iop.index <= rop.last_index)):
                    raise ValueError("insert op {} within boundaries of previous {}".format(iop, rop))

        reduced = {}
        for i, op in enumerate(rewrites):
            if op is None: continue
            if reduced.get(op.index): raise ValueError('should be only one op per index')
            reduced[op.index] = op

        return reduced

    class RewriteOperation(object):
        __slots__ = ('tokens', 'index', 'text', 'instructionIndex')

        def __init__(self, tokens, index, text=""):
            """
            :type tokens: CommonTokenStream
            :param tokens:
            :param index:
            :param text:
            :return:
            """
            self.tokens = tokens
            self.index = index
            self.text = text

View on GitHub (pinned to 7d5770395b)

Solutions

  1. Deduplicate your edit list by index before calling the rewriter (keep one op per token index).
  2. Use one program per transformation pass and call getText(programName) after each pass so the program is rolled.
  3. Combine conflicting ops manually (insert text into the replacement string) instead of issuing both.

Example fix

# before
rewriter.insertBefore(4, 'a')
rewriter.deleteIndex(4)  # two ops at index 4

# after
rewriter.replace(4, 4, 'a')  # single op: delete token 4 and emit 'a'
Defensive patterns

Strategy: validation

Validate before calling

seen = {}
for op in planned_ops:  # (index, kind, text)
    if op[0] in seen:
        raise ValueError('duplicate rewrite at token index %d: %s vs %s' % (op[0], seen[op[0]], op))
    seen[op[0]] = op

Try / catch

try:
    rewriter.getText()
except ValueError as e:
    if 'only one op per index' in str(e):
        # rebuild rewrites deduplicated by index
        pass
    else:
        raise

Prevention

When it happens

Trigger: Multiple operations that reduce to the same op.index without hitting any merge rule — for example a delete (replace with None) and an insertBefore at the same index, or overlapping op kinds at one index that the pairwise merges above do not cover.

Common situations: Accumulating edits from several listeners/visitors at the same token (common in formatter/transformer pipelines); replaying a recorded edit log twice on the same program.

Related errors


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