Home | History | Annotate | Download | only in runtime
      1 /*
      2  [The "BSD license"]
      3  Copyright (c) 2005-2009 Terence Parr
      4  All rights reserved.
      5 
      6  Redistribution and use in source and binary forms, with or without
      7  modification, are permitted provided that the following conditions
      8  are met:
      9  1. Redistributions of source code must retain the above copyright
     10      notice, this list of conditions and the following disclaimer.
     11  2. Redistributions in binary form must reproduce the above copyright
     12      notice, this list of conditions and the following disclaimer in the
     13      documentation and/or other materials provided with the distribution.
     14  3. The name of the author may not be used to endorse or promote products
     15      derived from this software without specific prior written permission.
     16 
     17  THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR
     18  IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
     19  OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
     20  IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT,
     21  INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
     22  NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
     23  DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
     24  THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
     25  (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
     26  THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
     27  */
     28 package org.antlr.runtime;
     29 
     30 import java.util.*;
     31 
     32 /** Useful for dumping out the input stream after doing some
     33  *  augmentation or other manipulations.
     34  *
     35  *  You can insert stuff, replace, and delete chunks.  Note that the
     36  *  operations are done lazily--only if you convert the buffer to a
     37  *  String.  This is very efficient because you are not moving data around
     38  *  all the time.  As the buffer of tokens is converted to strings, the
     39  *  toString() method(s) check to see if there is an operation at the
     40  *  current index.  If so, the operation is done and then normal String
     41  *  rendering continues on the buffer.  This is like having multiple Turing
     42  *  machine instruction streams (programs) operating on a single input tape. :)
     43  *
     44  *  Since the operations are done lazily at toString-time, operations do not
     45  *  screw up the token index values.  That is, an insert operation at token
     46  *  index i does not change the index values for tokens i+1..n-1.
     47  *
     48  *  Because operations never actually alter the buffer, you may always get
     49  *  the original token stream back without undoing anything.  Since
     50  *  the instructions are queued up, you can easily simulate transactions and
     51  *  roll back any changes if there is an error just by removing instructions.
     52  *  For example,
     53  *
     54  *   CharStream input = new ANTLRFileStream("input");
     55  *   TLexer lex = new TLexer(input);
     56  *   TokenRewriteStream tokens = new TokenRewriteStream(lex);
     57  *   T parser = new T(tokens);
     58  *   parser.startRule();
     59  *
     60  * 	 Then in the rules, you can execute
     61  *      Token t,u;
     62  *      ...
     63  *      input.insertAfter(t, "text to put after t");}
     64  * 		input.insertAfter(u, "text after u");}
     65  * 		System.out.println(tokens.toString());
     66  *
     67  *  Actually, you have to cast the 'input' to a TokenRewriteStream. :(
     68  *
     69  *  You can also have multiple "instruction streams" and get multiple
     70  *  rewrites from a single pass over the input.  Just name the instruction
     71  *  streams and use that name again when printing the buffer.  This could be
     72  *  useful for generating a C file and also its header file--all from the
     73  *  same buffer:
     74  *
     75  *      tokens.insertAfter("pass1", t, "text to put after t");}
     76  * 		tokens.insertAfter("pass2", u, "text after u");}
     77  * 		System.out.println(tokens.toString("pass1"));
     78  * 		System.out.println(tokens.toString("pass2"));
     79  *
     80  *  If you don't use named rewrite streams, a "default" stream is used as
     81  *  the first example shows.
     82  */
     83 public class TokenRewriteStream extends CommonTokenStream {
     84 	public static final String DEFAULT_PROGRAM_NAME = "default";
     85     public static final int PROGRAM_INIT_SIZE = 100;
     86 	public static final int MIN_TOKEN_INDEX = 0;
     87 
     88 	// Define the rewrite operation hierarchy
     89 
     90 	public class RewriteOperation {
     91         /** What index into rewrites List are we? */
     92         protected int instructionIndex;
     93         /** Token buffer index. */
     94         protected int index;
     95 		protected Object text;
     96 
     97 		protected RewriteOperation(int index) {
     98 			this.index = index;
     99 		}
    100 
    101 		protected RewriteOperation(int index, Object text) {
    102 			this.index = index;
    103 			this.text = text;
    104 		}
    105 		/** Execute the rewrite operation by possibly adding to the buffer.
    106 		 *  Return the index of the next token to operate on.
    107 		 */
    108 		public int execute(StringBuffer buf) {
    109 			return index;
    110 		}
    111 		@Override
    112 		public String toString() {
    113 			String opName = getClass().getName();
    114 			int $index = opName.indexOf('$');
    115 			opName = opName.substring($index+1, opName.length());
    116 			return "<"+opName+"@"+tokens.get(index)+
    117 				   ":\""+text+"\">";
    118 		}
    119 	}
    120 
    121 	class InsertBeforeOp extends RewriteOperation {
    122 		public InsertBeforeOp(int index, Object text) {
    123 			super(index,text);
    124 		}
    125 		@Override
    126 		public int execute(StringBuffer buf) {
    127 			buf.append(text);
    128 			if ( tokens.get(index).getType()!=Token.EOF ) {
    129 				buf.append(tokens.get(index).getText());
    130 			}
    131 			return index+1;
    132 		}
    133 	}
    134 
    135 	/** I'm going to try replacing range from x..y with (y-x)+1 ReplaceOp
    136 	 *  instructions.
    137 	 */
    138 	class ReplaceOp extends RewriteOperation {
    139 		protected int lastIndex;
    140 		public ReplaceOp(int from, int to, Object text) {
    141 			super(from,text);
    142 			lastIndex = to;
    143 		}
    144 		@Override
    145 		public int execute(StringBuffer buf) {
    146 			if ( text!=null ) {
    147 				buf.append(text);
    148 			}
    149 			return lastIndex+1;
    150 		}
    151 		@Override
    152 		public String toString() {
    153 			if ( text==null ) {
    154 				return "<DeleteOp@"+tokens.get(index)+
    155 					   ".."+tokens.get(lastIndex)+">";
    156 			}
    157 			return "<ReplaceOp@"+tokens.get(index)+
    158 				   ".."+tokens.get(lastIndex)+":\""+text+"\">";
    159 		}
    160 	}
    161 
    162 	/** You may have multiple, named streams of rewrite operations.
    163 	 *  I'm calling these things "programs."
    164 	 *  Maps String (name) &rarr; rewrite (List)
    165 	 */
    166 	protected Map<String, List<RewriteOperation>> programs = null;
    167 
    168 	/** Map String (program name) &rarr; Integer index */
    169 	protected Map<String, Integer> lastRewriteTokenIndexes = null;
    170 
    171 	public TokenRewriteStream() {
    172 		init();
    173 	}
    174 
    175 	protected void init() {
    176 		programs = new HashMap<String, List<RewriteOperation>>();
    177 		programs.put(DEFAULT_PROGRAM_NAME, new ArrayList<RewriteOperation>(PROGRAM_INIT_SIZE));
    178 		lastRewriteTokenIndexes = new HashMap<String, Integer>();
    179 	}
    180 
    181 	public TokenRewriteStream(TokenSource tokenSource) {
    182 	    super(tokenSource);
    183 		init();
    184 	}
    185 
    186 	public TokenRewriteStream(TokenSource tokenSource, int channel) {
    187 		super(tokenSource, channel);
    188 		init();
    189 	}
    190 
    191 	public void rollback(int instructionIndex) {
    192 		rollback(DEFAULT_PROGRAM_NAME, instructionIndex);
    193 	}
    194 
    195 	/** Rollback the instruction stream for a program so that
    196 	 *  the indicated instruction (via instructionIndex) is no
    197 	 *  longer in the stream.  UNTESTED!
    198 	 */
    199 	public void rollback(String programName, int instructionIndex) {
    200 		List<RewriteOperation> is = programs.get(programName);
    201 		if ( is!=null ) {
    202 			programs.put(programName, is.subList(MIN_TOKEN_INDEX,instructionIndex));
    203 		}
    204 	}
    205 
    206 	public void deleteProgram() {
    207 		deleteProgram(DEFAULT_PROGRAM_NAME);
    208 	}
    209 
    210 	/** Reset the program so that no instructions exist */
    211 	public void deleteProgram(String programName) {
    212 		rollback(programName, MIN_TOKEN_INDEX);
    213 	}
    214 
    215 	public void insertAfter(Token t, Object text) {
    216 		insertAfter(DEFAULT_PROGRAM_NAME, t, text);
    217 	}
    218 
    219 	public void insertAfter(int index, Object text) {
    220 		insertAfter(DEFAULT_PROGRAM_NAME, index, text);
    221 	}
    222 
    223 	public void insertAfter(String programName, Token t, Object text) {
    224 		insertAfter(programName,t.getTokenIndex(), text);
    225 	}
    226 
    227 	public void insertAfter(String programName, int index, Object text) {
    228 		// to insert after, just insert before next index (even if past end)
    229 		insertBefore(programName,index+1, text);
    230 	}
    231 
    232 	public void insertBefore(Token t, Object text) {
    233 		insertBefore(DEFAULT_PROGRAM_NAME, t, text);
    234 	}
    235 
    236 	public void insertBefore(int index, Object text) {
    237 		insertBefore(DEFAULT_PROGRAM_NAME, index, text);
    238 	}
    239 
    240 	public void insertBefore(String programName, Token t, Object text) {
    241 		insertBefore(programName, t.getTokenIndex(), text);
    242 	}
    243 
    244 	public void insertBefore(String programName, int index, Object text) {
    245 		RewriteOperation op = new InsertBeforeOp(index,text);
    246 		List<? super RewriteOperation> rewrites = getProgram(programName);
    247         op.instructionIndex = rewrites.size();
    248         rewrites.add(op);
    249 	}
    250 
    251 	public void replace(int index, Object text) {
    252 		replace(DEFAULT_PROGRAM_NAME, index, index, text);
    253 	}
    254 
    255 	public void replace(int from, int to, Object text) {
    256 		replace(DEFAULT_PROGRAM_NAME, from, to, text);
    257 	}
    258 
    259 	public void replace(Token indexT, Object text) {
    260 		replace(DEFAULT_PROGRAM_NAME, indexT, indexT, text);
    261 	}
    262 
    263 	public void replace(Token from, Token to, Object text) {
    264 		replace(DEFAULT_PROGRAM_NAME, from, to, text);
    265 	}
    266 
    267 	public void replace(String programName, int from, int to, Object text) {
    268 		if ( from > to || from<0 || to<0 || to >= tokens.size() ) {
    269 			throw new IllegalArgumentException("replace: range invalid: "+from+".."+to+"(size="+tokens.size()+")");
    270 		}
    271 		RewriteOperation op = new ReplaceOp(from, to, text);
    272 		List<? super RewriteOperation> rewrites = getProgram(programName);
    273         op.instructionIndex = rewrites.size();
    274         rewrites.add(op);
    275 	}
    276 
    277 	public void replace(String programName, Token from, Token to, Object text) {
    278 		replace(programName,
    279 				from.getTokenIndex(),
    280 				to.getTokenIndex(),
    281 				text);
    282 	}
    283 
    284 	public void delete(int index) {
    285 		delete(DEFAULT_PROGRAM_NAME, index, index);
    286 	}
    287 
    288 	public void delete(int from, int to) {
    289 		delete(DEFAULT_PROGRAM_NAME, from, to);
    290 	}
    291 
    292 	public void delete(Token indexT) {
    293 		delete(DEFAULT_PROGRAM_NAME, indexT, indexT);
    294 	}
    295 
    296 	public void delete(Token from, Token to) {
    297 		delete(DEFAULT_PROGRAM_NAME, from, to);
    298 	}
    299 
    300 	public void delete(String programName, int from, int to) {
    301 		replace(programName,from,to,null);
    302 	}
    303 
    304 	public void delete(String programName, Token from, Token to) {
    305 		replace(programName,from,to,null);
    306 	}
    307 
    308 	public int getLastRewriteTokenIndex() {
    309 		return getLastRewriteTokenIndex(DEFAULT_PROGRAM_NAME);
    310 	}
    311 
    312 	protected int getLastRewriteTokenIndex(String programName) {
    313 		Integer I = lastRewriteTokenIndexes.get(programName);
    314 		if ( I==null ) {
    315 			return -1;
    316 		}
    317 		return I;
    318 	}
    319 
    320 	protected void setLastRewriteTokenIndex(String programName, int i) {
    321 		lastRewriteTokenIndexes.put(programName, i);
    322 	}
    323 
    324 	protected List<RewriteOperation> getProgram(String name) {
    325 		List<RewriteOperation> is = programs.get(name);
    326 		if ( is==null ) {
    327 			is = initializeProgram(name);
    328 		}
    329 		return is;
    330 	}
    331 
    332 	private List<RewriteOperation> initializeProgram(String name) {
    333 		List<RewriteOperation> is = new ArrayList<RewriteOperation>(PROGRAM_INIT_SIZE);
    334 		programs.put(name, is);
    335 		return is;
    336 	}
    337 
    338 	public String toOriginalString() {
    339         fill();
    340 		return toOriginalString(MIN_TOKEN_INDEX, size()-1);
    341 	}
    342 
    343 	public String toOriginalString(int start, int end) {
    344 		StringBuilder buf = new StringBuilder();
    345 		for (int i=start; i>=MIN_TOKEN_INDEX && i<=end && i<tokens.size(); i++) {
    346 			if ( get(i).getType()!=Token.EOF ) buf.append(get(i).getText());
    347 		}
    348 		return buf.toString();
    349 	}
    350 
    351 	@Override
    352 	public String toString() {
    353         fill();
    354 		return toString(MIN_TOKEN_INDEX, size()-1);
    355 	}
    356 
    357 	public String toString(String programName) {
    358         fill();
    359 		return toString(programName, MIN_TOKEN_INDEX, size()-1);
    360 	}
    361 
    362 	@Override
    363 	public String toString(int start, int end) {
    364 		return toString(DEFAULT_PROGRAM_NAME, start, end);
    365 	}
    366 
    367 	public String toString(String programName, int start, int end) {
    368 		List<RewriteOperation> rewrites = programs.get(programName);
    369 
    370         // ensure start/end are in range
    371         if ( end>tokens.size()-1 ) end = tokens.size()-1;
    372         if ( start<0 ) start = 0;
    373 
    374         if ( rewrites==null || rewrites.isEmpty() ) {
    375 			return toOriginalString(start,end); // no instructions to execute
    376 		}
    377 		StringBuffer buf = new StringBuffer();
    378 
    379 		// First, optimize instruction stream
    380 		Map<Integer, ? extends RewriteOperation> indexToOp = reduceToSingleOperationPerIndex(rewrites);
    381 
    382         // Walk buffer, executing instructions and emitting tokens
    383         int i = start;
    384         while ( i <= end && i < tokens.size() ) {
    385 			RewriteOperation op = indexToOp.get(i);
    386 			indexToOp.remove(i); // remove so any left have index size-1
    387 			Token t = tokens.get(i);
    388 			if ( op==null ) {
    389 				// no operation at that index, just dump token
    390 				if ( t.getType()!=Token.EOF ) buf.append(t.getText());
    391 				i++; // move to next token
    392 			}
    393 			else {
    394 				i = op.execute(buf); // execute operation and skip
    395 			}
    396 		}
    397 
    398         // include stuff after end if it's last index in buffer
    399         // So, if they did an insertAfter(lastValidIndex, "foo"), include
    400         // foo if end==lastValidIndex.
    401         if ( end==tokens.size()-1 ) {
    402             // Scan any remaining operations after last token
    403             // should be included (they will be inserts).
    404             for (RewriteOperation op : indexToOp.values()) {
    405                 if ( op.index >= tokens.size()-1 ) buf.append(op.text);
    406             }
    407         }
    408         return buf.toString();
    409 	}
    410 
    411 	/** We need to combine operations and report invalid operations (like
    412 	 *  overlapping replaces that are not completed nested).  Inserts to
    413 	 *  same index need to be combined etc...   Here are the cases:
    414 	 *
    415 	 *  I.i.u I.j.v								leave alone, nonoverlapping
    416 	 *  I.i.u I.i.v								combine: Iivu
    417 	 *
    418 	 *  R.i-j.u R.x-y.v	| i-j in x-y			delete first R
    419 	 *  R.i-j.u R.i-j.v							delete first R
    420 	 *  R.i-j.u R.x-y.v	| x-y in i-j			ERROR
    421 	 *  R.i-j.u R.x-y.v	| boundaries overlap	ERROR
    422 	 *
    423 	 *  Delete special case of replace (text==null):
    424 	 *  D.i-j.u D.x-y.v	| boundaries overlap	combine to max(min)..max(right)
    425 	 *
    426 	 *  I.i.u R.x-y.v | i in (x+1)-y			delete I (since insert before
    427 	 *											we're not deleting i)
    428 	 *  I.i.u R.x-y.v | i not in (x+1)-y		leave alone, nonoverlapping
    429 	 *  R.x-y.v I.i.u | i in x-y				ERROR
    430 	 *  R.x-y.v I.x.u 							R.x-y.uv (combine, delete I)
    431 	 *  R.x-y.v I.i.u | i not in x-y			leave alone, nonoverlapping
    432 	 *
    433 	 *  I.i.u = insert u before op @ index i
    434 	 *  R.x-y.u = replace x-y indexed tokens with u
    435 	 *
    436 	 *  First we need to examine replaces.  For any replace op:
    437 	 *
    438 	 * 		1. wipe out any insertions before op within that range.
    439 	 *		2. Drop any replace op before that is contained completely within
    440 	 *         that range.
    441 	 *		3. Throw exception upon boundary overlap with any previous replace.
    442 	 *
    443 	 *  Then we can deal with inserts:
    444 	 *
    445 	 * 		1. for any inserts to same index, combine even if not adjacent.
    446 	 * 		2. for any prior replace with same left boundary, combine this
    447 	 *         insert with replace and delete this replace.
    448 	 * 		3. throw exception if index in same range as previous replace
    449 	 *
    450 	 *  Don't actually delete; make op null in list. Easier to walk list.
    451 	 *  Later we can throw as we add to index &rarr; op map.
    452 	 *
    453 	 *  Note that I.2 R.2-2 will wipe out I.2 even though, technically, the
    454 	 *  inserted stuff would be before the replace range.  But, if you
    455 	 *  add tokens in front of a method body '{' and then delete the method
    456 	 *  body, I think the stuff before the '{' you added should disappear too.
    457 	 *
    458 	 *  Return a map from token index to operation.
    459 	 */
    460 	protected Map<Integer, ? extends RewriteOperation> reduceToSingleOperationPerIndex(List<? extends RewriteOperation> rewrites) {
    461 //		System.out.println("rewrites="+rewrites);
    462 
    463 		// WALK REPLACES
    464 		for (int i = 0; i < rewrites.size(); i++) {
    465 			RewriteOperation op = rewrites.get(i);
    466 			if ( op==null ) continue;
    467 			if ( !(op instanceof ReplaceOp) ) continue;
    468 			ReplaceOp rop = (ReplaceOp)rewrites.get(i);
    469 			// Wipe prior inserts within range
    470 			List<? extends InsertBeforeOp> inserts = getKindOfOps(rewrites, InsertBeforeOp.class, i);
    471 			for (int j = 0; j < inserts.size(); j++) {
    472 				InsertBeforeOp iop = inserts.get(j);
    473 				if ( iop.index == rop.index ) {
    474 					// E.g., insert before 2, delete 2..2; update replace
    475 					// text to include insert before, kill insert
    476 					rewrites.set(iop.instructionIndex, null);
    477 					rop.text = iop.text.toString() + (rop.text!=null?rop.text.toString():"");
    478 				}
    479 				else if ( iop.index > rop.index && iop.index <= rop.lastIndex ) {
    480                     // delete insert as it's a no-op.
    481                     rewrites.set(iop.instructionIndex, null);
    482 				}
    483 			}
    484 			// Drop any prior replaces contained within
    485 			List<? extends ReplaceOp> prevReplaces = getKindOfOps(rewrites, ReplaceOp.class, i);
    486 			for (int j = 0; j < prevReplaces.size(); j++) {
    487 				ReplaceOp prevRop = prevReplaces.get(j);
    488 				if ( prevRop.index>=rop.index && prevRop.lastIndex <= rop.lastIndex ) {
    489                     // delete replace as it's a no-op.
    490                     rewrites.set(prevRop.instructionIndex, null);
    491 					continue;
    492 				}
    493 				// throw exception unless disjoint or identical
    494 				boolean disjoint =
    495 					prevRop.lastIndex<rop.index || prevRop.index > rop.lastIndex;
    496 				boolean same =
    497 					prevRop.index==rop.index && prevRop.lastIndex==rop.lastIndex;
    498 				// Delete special case of replace (text==null):
    499 				// D.i-j.u D.x-y.v	| boundaries overlap	combine to max(min)..max(right)
    500 				if ( prevRop.text==null && rop.text==null && !disjoint ) {
    501 					//System.out.println("overlapping deletes: "+prevRop+", "+rop);
    502 					rewrites.set(prevRop.instructionIndex, null); // kill first delete
    503 					rop.index = Math.min(prevRop.index, rop.index);
    504 					rop.lastIndex = Math.max(prevRop.lastIndex, rop.lastIndex);
    505 					System.out.println("new rop "+rop);
    506 				}
    507 				else if ( !disjoint && !same ) {
    508 					throw new IllegalArgumentException("replace op boundaries of "+rop+
    509 													   " overlap with previous "+prevRop);
    510 				}
    511 			}
    512 		}
    513 
    514 		// WALK INSERTS
    515 		for (int i = 0; i < rewrites.size(); i++) {
    516 			RewriteOperation op = rewrites.get(i);
    517 			if ( op==null ) continue;
    518 			if ( !(op instanceof InsertBeforeOp) ) continue;
    519 			InsertBeforeOp iop = (InsertBeforeOp)rewrites.get(i);
    520 			// combine current insert with prior if any at same index
    521 			List<? extends InsertBeforeOp> prevInserts = getKindOfOps(rewrites, InsertBeforeOp.class, i);
    522 			for (int j = 0; j < prevInserts.size(); j++) {
    523 				InsertBeforeOp prevIop = prevInserts.get(j);
    524 				if ( prevIop.index == iop.index ) { // combine objects
    525 					// convert to strings...we're in process of toString'ing
    526 					// whole token buffer so no lazy eval issue with any templates
    527 					iop.text = catOpText(iop.text,prevIop.text);
    528                     // delete redundant prior insert
    529                     rewrites.set(prevIop.instructionIndex, null);
    530 				}
    531 			}
    532 			// look for replaces where iop.index is in range; error
    533 			List<? extends ReplaceOp> prevReplaces = getKindOfOps(rewrites, ReplaceOp.class, i);
    534 			for (int j = 0; j < prevReplaces.size(); j++) {
    535 				ReplaceOp rop = prevReplaces.get(j);
    536 				if ( iop.index == rop.index ) {
    537 					rop.text = catOpText(iop.text,rop.text);
    538 					rewrites.set(i, null);  // delete current insert
    539 					continue;
    540 				}
    541 				if ( iop.index >= rop.index && iop.index <= rop.lastIndex ) {
    542 					throw new IllegalArgumentException("insert op "+iop+
    543 													   " within boundaries of previous "+rop);
    544 				}
    545 			}
    546 		}
    547 		// System.out.println("rewrites after="+rewrites);
    548 		Map<Integer, RewriteOperation> m = new HashMap<Integer, RewriteOperation>();
    549 		for (int i = 0; i < rewrites.size(); i++) {
    550 			RewriteOperation op = rewrites.get(i);
    551 			if ( op==null ) continue; // ignore deleted ops
    552 			if ( m.get(op.index)!=null ) {
    553 				throw new Error("should only be one op per index");
    554 			}
    555 			m.put(op.index, op);
    556 		}
    557 		//System.out.println("index to op: "+m);
    558 		return m;
    559 	}
    560 
    561 	protected String catOpText(Object a, Object b) {
    562 		String x = "";
    563 		String y = "";
    564 		if ( a!=null ) x = a.toString();
    565 		if ( b!=null ) y = b.toString();
    566 		return x+y;
    567 	}
    568 	protected <T extends RewriteOperation> List<? extends T> getKindOfOps(List<? extends RewriteOperation> rewrites, Class<T> kind) {
    569 		return getKindOfOps(rewrites, kind, rewrites.size());
    570 	}
    571 
    572     /** Get all operations before an index of a particular kind */
    573     protected <T extends RewriteOperation> List<? extends T> getKindOfOps(List<? extends RewriteOperation> rewrites, Class<T> kind, int before) {
    574 		List<T> ops = new ArrayList<T>();
    575 		for (int i=0; i<before && i<rewrites.size(); i++) {
    576 			RewriteOperation op = rewrites.get(i);
    577 			if ( op==null ) continue; // ignore deleted
    578 			if ( kind.isInstance(op) ) ops.add(kind.cast(op));
    579 		}
    580 		return ops;
    581 	}
    582 
    583 	public String toDebugString() {
    584 		return toDebugString(MIN_TOKEN_INDEX, size()-1);
    585 	}
    586 
    587 	public String toDebugString(int start, int end) {
    588 		StringBuilder buf = new StringBuilder();
    589 		for (int i=start; i>=MIN_TOKEN_INDEX && i<=end && i<tokens.size(); i++) {
    590 			buf.append(get(i));
    591 		}
    592 		return buf.toString();
    593 	}
    594 }
    595