antlr/antlr4 · error · IllegalArgumentException

insert op {iop} within boundaries of previous {rop}

Error message

insert op {iop} within boundaries of previous {rop}

What it means

TokenStreamRewriter rejects an InsertBeforeOp whose token index falls strictly inside a previously queued ReplaceOp's range (index > rop.index && index <= rop.lastIndex). Inserting text into the middle of a region that will be wholly replaced has no well-defined meaning, so the rewriter fails fast. Note insert-at-index == replace-index is legal and is folded into the replace text.

Source

Thrown at runtime/Java/src/org/antlr/v4/runtime/TokenStreamRewriter.java:559

					else if ( InsertBeforeOp.class.isInstance(prevIop) ) { // combine objects
						// convert to strings...we're in process of toString'ing
						// whole token buffer so no lazy eval issue with any templates
						iop.text = catOpText(iop.text, prevIop.text);
						// delete redundant prior insert
						rewrites.set(prevIop.instructionIndex, null);
					}
				}
			}
			// look for replaces where iop.index is in range; error
			List<? extends ReplaceOp> prevReplaces = getKindOfOps(rewrites, ReplaceOp.class, i);
			for (ReplaceOp rop : prevReplaces) {
				if ( iop.index == rop.index ) {
					rop.text = catOpText(iop.text,rop.text);
					rewrites.set(i, null);	// delete current insert
					continue;
				}
				if ( iop.index >= rop.index && iop.index <= rop.lastIndex ) {
					throw new IllegalArgumentException("insert op "+iop+" within boundaries of previous "+rop);
				}
			}
		}
		// System.out.println("rewrites after="+rewrites);
		Map<Integer, RewriteOperation> m = new HashMap<Integer, RewriteOperation>();
		for (int i = 0; i < rewrites.size(); i++) {
			RewriteOperation op = rewrites.get(i);
			if ( op==null ) continue; // ignore deleted ops
			if ( m.get(op.index)!=null ) {
				throw new Error("should only be one op per index");
			}
			m.put(op.index, op);
		}
		//System.out.println("index to op: "+m);
		return m;
	}

	protected String catOpText(Object a, Object b) {

View on GitHub (pinned to 7d5770395b)

Solutions

  1. Move the inserted text outside the replaced range, or prepend/append it to the replace text yourself
  2. Check queued replaces first: only insert when the index is not inside any (from, to] replace range
  3. Restructure the pass so one visitor owns a token range: either it replaces it or it inserts into it, never both

Example fix

// before
rewriter.replace(5, 10, "newExpr");
rewriter.insertAfter(tokens.get(7), ";"); // index 8 inside (5,10] -> throws

// after
rewriter.replace(5, 10, "newExpr;"); // fold the insertion into the replacement
Defensive patterns

Strategy: validation

Validate before calling

static boolean insertSafe(int idx, List<int[]> replaces) {
    for (int[] r : replaces)
        if (idx > r[0] && idx <= r[1]) return false; // strictly inside a replaced range
    return true;
}
if (insertSafe(t.getTokenIndex(), claimedReplaces)) rewriter.insertBefore(t, text);

Try / catch

try { rewriter.getText(); }
catch (IllegalArgumentException e) { /* fold the insert text into the covering replace op and re-run */ }

Prevention

When it happens

Trigger: rewriter.insertBefore(t, text) or insertAfter(t, text) where t's index is within (from, to] of an earlier replace(from, to, ...); insertAfter a replaced token (index from+..to) since insertAfter maps to insertBefore of index+1.

Common situations: Transformation pipelines that replace an expression but also try to insert a token inside it; visitors adding separators or annotations into ranges another rule already rewrote.

Related errors


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