org.codehaus.janino.CodeContext Maven / Gradle / Ivy
/*
* Janino - An embedded Java[TM] compiler
*
* Copyright (c) 2001-2010 Arno Unkrig. All rights reserved.
* Copyright (c) 2015-2016 TIBCO Software Inc. All rights reserved. // CHECKSTYLE:OFF CHECKSTYLE:ON
*
* Redistribution and use in source and binary forms, with or without modification, are permitted provided that the
* following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the
* following disclaimer.
* 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the
* following disclaimer in the documentation and/or other materials provided with the distribution.
* 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote
* products derived from this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
* INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
* WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package org.codehaus.janino;
import java.io.DataOutputStream;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.codehaus.commons.compiler.InternalCompilerException;
import org.codehaus.commons.nullanalysis.Nullable;
import org.codehaus.janino.util.ClassFile;
import org.codehaus.janino.util.ClassFile.AttributeInfo;
import org.codehaus.janino.util.ClassFile.ConstantClassInfo;
import org.codehaus.janino.util.ClassFile.LineNumberTableAttribute.Entry;
import org.codehaus.janino.util.ClassFile.StackMapTableAttribute;
import org.codehaus.janino.util.ClassFile.StackMapTableAttribute.AppendFrame;
import org.codehaus.janino.util.ClassFile.StackMapTableAttribute.ChopFrame;
import org.codehaus.janino.util.ClassFile.StackMapTableAttribute.FullFrame;
import org.codehaus.janino.util.ClassFile.StackMapTableAttribute.ObjectVariableInfo;
import org.codehaus.janino.util.ClassFile.StackMapTableAttribute.SameFrame;
import org.codehaus.janino.util.ClassFile.StackMapTableAttribute.SameFrameExtended;
import org.codehaus.janino.util.ClassFile.StackMapTableAttribute.SameLocals1StackItemFrame;
import org.codehaus.janino.util.ClassFile.StackMapTableAttribute.SameLocals1StackItemFrameExtended;
import org.codehaus.janino.util.ClassFile.StackMapTableAttribute.StackMapFrame;
import org.codehaus.janino.util.ClassFile.StackMapTableAttribute.UninitializedVariableInfo;
import org.codehaus.janino.util.ClassFile.StackMapTableAttribute.VerificationTypeInfo;
/**
* The context of the compilation of a function (constructor or method). Manages generation of byte code, the exception
* table, generation of line number tables, allocation of local variables, determining of stack size and local variable
* table size and flow analysis.
*/
public
class CodeContext {
private static final int INITIAL_SIZE = 128;
private final ClassFile classFile;
private int maxStack;
private short maxLocals;
private byte[] code;
private final Offset beginning;
private final Inserter end;
private Inserter currentInserter;
private final List exceptionTableEntries;
/**
* All the local variables that are allocated in any block in this {@link CodeContext}.
*/
private final List allLocalVars = new ArrayList();
/**
* Each List of Java.LocalVariableSlot is the local variables allocated for a block. They are pushed and popped
* onto the list together to make allocation of the next local variable slot easy.
*/
@Nullable private LocalScope currentLocalScope;
static
class LocalScope {
@Nullable final LocalScope parent;
final short startingLocalVariableSlot;
final List localVars = new ArrayList();
final StackMap startingStackMap;
LocalScope(@Nullable LocalScope parent, short startingLocalSlot, StackMap startingStackMap) {
this.parent = parent;
this.startingLocalVariableSlot = startingLocalSlot;
this.startingStackMap = startingStackMap;
}
}
private short nextLocalVariableSlot;
private final List relocatables = new ArrayList();
/**
* Creates an empty "Code" attribute.
*/
public
CodeContext(ClassFile classFile) {
this.classFile = classFile;
this.maxStack = 0;
this.maxLocals = 0;
this.code = new byte[CodeContext.INITIAL_SIZE];
this.exceptionTableEntries = new ArrayList();
this.beginning = new Offset();
this.beginning.offset = 0;
this.currentInserter = new Inserter();
this.currentInserter.offset = 0;
this.currentInserter.setStackMap(new StackMap(new VerificationTypeInfo[0], new VerificationTypeInfo[0]));
this.beginning.next = this.currentInserter;
this.currentInserter.prev = this.beginning;
this.end = this.currentInserter;
}
/**
* The {@link ClassFile} this context is related to.
*/
public ClassFile
getClassFile() { return this.classFile; }
/**
* Allocates space for a local variable of the given size (1 or 2) on the local variable array.
*
* As a side effect, the "max_locals" field of the "Code" attribute is updated.
*
*
* The only way to deallocate local variables is to {@link #saveLocalVariables()} and later {@link
* #restoreLocalVariables()}.
*
*
* @param size The number of slots to allocate (1 or 2)
* @return The slot index of the allocated variable
*/
public short
allocateLocalVariable(short size) { return this.allocateLocalVariable(size, null, null).getSlotIndex(); }
/**
* Allocates space for a local variable of the given size (1 or 2) on the local variable array. As a side effect,
* the "max_locals" field of the "Code" attribute is updated.
*
* The only way to deallocate local variables is to {@link #saveLocalVariables()} and later {@link
* #restoreLocalVariables()}.
*
*
* @param size Number of slots to use (1 or 2)
* @param name The variable name; if {@code null}, then the variable won't be written to the LocalVariableTable
* @param type The variable type; if the name is not null, then the type is needed to write to the
* LocalVariableTable
*/
public Java.LocalVariableSlot
allocateLocalVariable(short size, @Nullable String name, @Nullable IClass type) {
LocalScope currentScope = this.currentLocalScope;
assert currentScope != null : "saveLocalVariables must be called first";
final List currentVars = currentScope.localVars;
Java.LocalVariableSlot slot = new Java.LocalVariableSlot(name, this.nextLocalVariableSlot, type);
if (name != null) {
slot.setStart(this.newOffset());
}
// // This check would be wrong, because one (Long|Double)_variable_info maps TWO local variable slots.
// if (this.nextLocalVariableSlot != this.currentInserter.stackMap.locals().length) {
// throw new InternalCompilerException(...);
// }
this.nextLocalVariableSlot += size;
currentVars.add(slot);
this.allLocalVars.add(slot);
if (this.nextLocalVariableSlot > this.maxLocals) {
this.maxLocals = this.nextLocalVariableSlot;
}
return slot;
}
/**
* Remembers the current size of the local variables array.
*/
public List
saveLocalVariables() {
// Push empty list on the local scope stack to hold a new block's local vars.
return (this.currentLocalScope = new LocalScope(
this.currentLocalScope,
this.nextLocalVariableSlot,
this.currentInserter.getStackMap()
)).localVars;
}
/**
* Restores the previous size of the local variables array. This MUST to be called for every call to
* saveLocalVariables as it closes the variable extent for all the active local variables in the current block.
*/
public void
restoreLocalVariables() {
// Pop the list containing the current block's local vars.
LocalScope scopeToPop = this.currentLocalScope;
assert scopeToPop != null;
for (Java.LocalVariableSlot slot : scopeToPop.localVars) {
if (slot.getName() != null) slot.setEnd(this.newOffset());
this.allLocalVars.remove(slot);
}
this.currentLocalScope = scopeToPop.parent;
// Truncate the stack map.
if (this.currentLocalScope != null) {
StackMap sm = this.currentInserter.getStackMap();
if (sm != null) {
int numLocalsInStackMap = 0;
for (VerificationTypeInfo slot : sm.locals()) {
if (slot != StackMapTableAttribute.TOP_VARIABLE_INFO) {
numLocalsInStackMap++;
}
}
while (numLocalsInStackMap-- > this.allLocalVars.size()) {
sm = sm.popLocal();
}
this.currentInserter.setStackMap(sm);
}
}
// reuse local variable slots of the popped scope
this.nextLocalVariableSlot = scopeToPop.startingLocalVariableSlot;
}
/**
* @param lineNumberTableAttributeNameIndex 0 == don't generate a "LineNumberTable" attribute
*/
protected void
storeCodeAttributeBody(
DataOutputStream dos,
short lineNumberTableAttributeNameIndex,
short localVariableTableAttributeNameIndex,
short stackMapTableAttributeNameIndex
) throws IOException {
dos.writeShort(this.maxStack); // max_stack
dos.writeShort(this.maxLocals); // max_locals
dos.writeInt(this.end.offset); // code_length
dos.write(this.code, 0, this.end.offset); // code
dos.writeShort(this.exceptionTableEntries.size()); // exception_table_length
for (ExceptionTableEntry exceptionTableEntry : this.exceptionTableEntries) { // exception_table
dos.writeShort(exceptionTableEntry.startPc.offset);
dos.writeShort(exceptionTableEntry.endPc.offset);
dos.writeShort(exceptionTableEntry.handlerPc.offset);
dos.writeShort(exceptionTableEntry.catchType);
}
List attributes = new ArrayList();
// Add "LineNumberTable" attribute.
if (lineNumberTableAttributeNameIndex != 0) {
List lnt = new ArrayList();
for (Offset o = this.beginning; o != null; o = o.next) {
if (o instanceof LineNumberOffset) {
int offset = o.offset;
if (offset > 0xffff) {
throw new InternalCompilerException("LineNumberTable entry offset out of range");
}
short lineNumber = ((LineNumberOffset) o).lineNumber;
lnt.add(new ClassFile.LineNumberTableAttribute.Entry((short) offset, lineNumber));
}
}
ClassFile.LineNumberTableAttribute.Entry[] lnte = (ClassFile.LineNumberTableAttribute.Entry[]) lnt.toArray(
new ClassFile.LineNumberTableAttribute.Entry[lnt.size()]
);
attributes.add(new ClassFile.LineNumberTableAttribute(
lineNumberTableAttributeNameIndex, // attributeNameIndex
lnte // lineNumberTableEntries
));
}
// Add "LocalVariableTable" attribute.
if (localVariableTableAttributeNameIndex != 0) {
ClassFile.AttributeInfo ai = this.storeLocalVariableTable(dos, localVariableTableAttributeNameIndex);
if (ai != null) attributes.add(ai);
}
// Add the "StackMapTable" attribute.
{
// Skip the "zeroth" frame.
Offset frame = this.beginning;
Offset previousFrame = null;
while (frame.offset == 0) {
previousFrame = frame;
frame = frame.next;
assert frame != null;
}
assert previousFrame != null;
List smfs = new ArrayList();
for (; frame != null && frame.offset != this.end.offset; frame = frame.next) {
// "Padder" is used to insert the padding bytes of the LOOKUPSWITCH instruction; skip it, because
// it sits INSIDE the instruction and would cause a "StackMapTable error: bad offset".
if (frame instanceof Java.Padder) continue;
// "FourByteOffset" is used to write the offsets of the LOOKUPSWITCH instruction. Skip it, because
// it sits INSIDE the instruction and would cause a "StackMapTable error: bad offset".
if (frame instanceof FourByteOffset) continue;
{
Offset next = frame.next;
if (next != null && frame.offset == next.offset) continue;
}
final int offsetDelta = previousFrame.offset == 0 ? frame.offset : frame.offset - previousFrame.offset - 1;
final VerificationTypeInfo[] frameOperands = frame.getStackMap().operands();
final int frameOperandsLength = frameOperands.length;
final VerificationTypeInfo[] frameLocals = frame.getStackMap().locals();
final int frameLocalsLength = frameLocals.length;
final VerificationTypeInfo[] previousFrameLocals = previousFrame.getStackMap().locals();
final int previousFrameLocalsLength = previousFrameLocals.length;
int k;
// Encode the stack map entry delta as "frames", see JVMS11 4.7.4
if (
frameOperandsLength == 0
&& Arrays.equals(frameLocals, previousFrameLocals)
) {
if (offsetDelta <= 63) {
smfs.add(new SameFrame(offsetDelta)); // same_frame
} else {
smfs.add(new SameFrameExtended(offsetDelta)); // same_frame_extended
}
} else
if (
frameOperandsLength == 1
&& Arrays.equals(frameLocals, previousFrameLocals)
) {
if (offsetDelta <= 63) {
smfs.add(new SameLocals1StackItemFrame( // same_locals_1_stack_item_frame
offsetDelta, // offset_delta
frameOperands[0] // stack
));
} else {
smfs.add(new SameLocals1StackItemFrameExtended( // same_locals_1_stack_item_frame_extended
offsetDelta, // offset_delta
frameOperands[0] // stack
));
}
} else
if (
frameOperandsLength == 0
&& (k = previousFrameLocalsLength - frameLocalsLength) >= 1
&& k <= 3
&& Arrays.equals(frameLocals, Arrays.copyOf(previousFrameLocals, frameLocalsLength))
) {
smfs.add(new ChopFrame(offsetDelta, k)); // chop_frame
} else
if (
frameOperandsLength == 0
&& (k = frameLocalsLength - previousFrameLocalsLength) >= 1
&& k <= 3
&& Arrays.equals(previousFrameLocals, Arrays.copyOf(frameLocals, previousFrameLocalsLength))
) {
smfs.add(new AppendFrame( // append_frame
offsetDelta, // offset_delta
(VerificationTypeInfo[]) Arrays.copyOfRange( // locals
frameLocals,
previousFrameLocalsLength,
frameLocalsLength
)
));
} else
{
smfs.add(new FullFrame( // full_frame
offsetDelta, // offset_delta
frameLocals, // locals
frameOperands // stack
));
}
previousFrame = frame;
}
attributes.add(
new StackMapTableAttribute(
stackMapTableAttributeNameIndex,
(StackMapFrame[]) smfs.toArray(new StackMapFrame[smfs.size()])
)
);
}
dos.writeShort(attributes.size()); // attributes_count
for (ClassFile.AttributeInfo attribute : attributes) { // attributes;
attribute.store(dos);
}
}
/**
* @return A {@link org.codehaus.janino.util.ClassFile.LocalVariableTableAttribute} for this {@link CodeContext}
*/
@Nullable protected ClassFile.AttributeInfo
storeLocalVariableTable(DataOutputStream dos, short localVariableTableAttributeNameIndex) {
ClassFile cf = this.getClassFile();
final List
entryList = new ArrayList();
for (Java.LocalVariableSlot slot : this.getAllLocalVars()) {
String localVariableName = slot.getName();
if (localVariableName != null) {
String typeName = slot.getType().getDescriptor();
final short classSlot = cf.addConstantUtf8Info(typeName);
final short varNameSlot = cf.addConstantUtf8Info(localVariableName);
Offset start = slot.getStart();
Offset end2 = slot.getEnd();
assert start != null;
assert end2 != null;
ClassFile.LocalVariableTableAttribute.Entry entry = new ClassFile.LocalVariableTableAttribute.Entry(
(short) start.offset,
(short) (end2.offset - start.offset),
varNameSlot,
classSlot,
slot.getSlotIndex()
);
entryList.add(entry);
}
}
if (entryList.size() > 0) {
Object entries = entryList.toArray(new ClassFile.LocalVariableTableAttribute.Entry[entryList.size()]);
return new ClassFile.LocalVariableTableAttribute(
localVariableTableAttributeNameIndex,
(ClassFile.LocalVariableTableAttribute.Entry[]) entries
);
}
return null;
}
/**
* Fixes up all of the offsets and relocate() all relocatables.
*/
public void
fixUpAndRelocate() {
this.maybeGrow();
this.fixUp();
this.relocate();
}
/**
* Grow the code if relocatables are required to.
*/
private void
maybeGrow() {
for (Relocatable relocatable : this.relocatables) {
relocatable.grow();
}
}
/**
* Fixes up all offsets.
*/
private void
fixUp() {
for (Offset o = this.beginning; o != this.end; o = o.next) {
assert o != null;
if (o instanceof FixUp) ((FixUp) o).fixUp();
}
}
/**
* Relocates all relocatables.
*/
private void
relocate() {
for (Relocatable relocatable : this.relocatables) {
relocatable.relocate();
}
}
/**
* Inserts a sequence of bytes at the current insertion position. Creates {@link LineNumberOffset}s as necessary.
*/
public void
write(byte[] b) {
if (b.length == 0) return;
// CAVEAT: "makeSpace()" may change "this.code", so make sure to call "makeSpace()" _before_ using "this.code"!
int o = this.makeSpace(b.length);
System.arraycopy(b, 0, this.code, o, b.length);
}
/**
* Inserts a byte at the current insertion position. Creates {@link LineNumberOffset}s as necessary.
*
* This method is an optimization to avoid allocating small byte[] and ease GC load.
*
*/
public void
write(byte b1) {
// CAVEAT: "makeSpace()" may change "this.code", so make sure to call "makeSpace()" _before_ using "this.code"!
int o = this.makeSpace(1);
this.code[o] = b1;
}
/**
* Inserts bytes at the current insertion position. Creates {@link LineNumberOffset}s as necessary.
*
* This method is an optimization to avoid allocating small byte[] and ease GC load.
*
*/
public void
write(byte b1, byte b2) {
int o = this.makeSpace(2);
this.code[o++] = b1;
this.code[o] = b2;
}
/**
* Inserts bytes at the current insertion position. Creates {@link LineNumberOffset}s as necessary.
*
* This method is an optimization to avoid allocating small byte[] and ease GC load.
*
*/
public void
write(byte b1, byte b2, byte b3) {
int o = this.makeSpace(3);
this.code[o++] = b1;
this.code[o++] = b2;
this.code[o] = b3;
}
/**
* Inserts bytes at the current insertion position. Creates {@link LineNumberOffset}s as necessary.
*
* This method is an optimization to avoid allocating small byte[] and ease GC load.
*
*/
public void
write(byte b1, byte b2, byte b3, byte b4) {
int o = this.makeSpace(4);
this.code[o++] = b1;
this.code[o++] = b2;
this.code[o++] = b3;
this.code[o] = b4;
}
public void
addLineNumberOffset(int lineNumber) {
if (lineNumber == -1) return;
if (lineNumber > 0xffff) lineNumber = 0xffff;
// Find out whether the line number is different from the line number of the preceding insertion,
// and, if so, insert a LineNumberOffset object, which will later lead to a LineNumberTable entry.
for (Offset o = this.currentInserter.prev; o != this.beginning; o = o.prev) {
assert o != null;
if (o instanceof LineNumberOffset) {
if ((((LineNumberOffset) o).lineNumber & 0xffff) == lineNumber) return;
break;
}
}
// Insert a LineNumberOffset _before_ the current inserter.
LineNumberOffset lno = new LineNumberOffset(this.currentInserter.offset, this.currentInserter.getStackMap(), (short) lineNumber);
Offset cip = this.currentInserter.prev;
assert cip != null;
lno.prev = cip;
lno.next = this.currentInserter;
cip.next = lno;
this.currentInserter.prev = lno;
}
/**
* Inserts size NUL bytes at the current inserter's offset, advances the current inserter's offset by
* size, creates {@link LineNumberOffset}s as necessary, and returns the current inserter's original
* offset (the offset of the first NUL byte that was inserted).
*
* Because the class file format does not support line numbers greater than 65535, these are treated as 65535.
*
*
* @param size The number of NUL bytes to inject
* @return The offset of the first inserted byte
*/
public int
makeSpace(final int size) {
final int cio = this.currentInserter.offset;
if (size == 0) return cio;
if (this.end.offset + size <= this.code.length) {
// Optimization to avoid a trivial method call in the common case
if (cio != this.end.offset) {
System.arraycopy(this.code, cio, this.code, cio + size, this.end.offset - cio);
}
} else {
byte[] oldCode = this.code;
//double size to avoid horrible performance, but don't grow over our limit
int newSize = Math.max(Math.min(oldCode.length * 2, 0xffff), oldCode.length + size);
if (newSize > 0xffff) throw new InternalCompilerException("Code grows beyond 64 KB");
this.code = new byte[newSize];
System.arraycopy(oldCode, 0, this.code, 0, cio);
System.arraycopy(oldCode, cio, this.code, cio + size, this.end.offset - cio);
}
Arrays.fill(this.code, cio, cio + size, (byte) 0);
for (Offset o = this.currentInserter; o != null; o = o.next) o.offset += size;
return cio;
}
/**
*/
public void
writeShort(int v) { this.write((byte) (v >> 8), (byte) v); }
/**
* Generates a "branch" instruction.
*
* @param opcode One of {@link Opcode#GOTO}, {@link Opcode#JSR} and Opcode.IF*
* @param dst Where to branch
*/
public void
writeBranch(int opcode, final Offset dst) {
if (dst.offset == -1) {
if (dst.stackMap == null) {
dst.stackMap = this.currentInserter.getStackMap();
}
}
this.relocatables.add(new Branch(opcode, dst));
this.write((byte) opcode, (byte) -1, (byte) -1);
}
private
class Branch extends Relocatable {
Branch(int opcode, Offset destination) {
this.opcode = opcode;
this.source = CodeContext.this.newInserter();
this.destination = destination;
if (opcode == Opcode.JSR_W || opcode == Opcode.GOTO_W) {
// No need to expand wide opcodes.
this.expanded = true;
} else {
this.expanded = false;
}
}
@Override public void
grow() {
if (this.destination.offset == Offset.UNSET) {
throw new InternalCompilerException("Cannot relocate branch to unset destination offset");
}
int offset = this.destination.offset - this.source.offset;
@SuppressWarnings("deprecation") final int opcodeJsr = Opcode.JSR;
if (!this.expanded && (offset > Short.MAX_VALUE || offset < Short.MIN_VALUE)) {
//we want to insert the data without skewing our source position,
//so we will cache it and then restore it later.
final int pos = this.source.offset;
CodeContext.this.pushInserter(this.source);
{
// Promotion to a wide instruction only requires 2 extra bytes. Everything else requires a new
// GOTO_W instruction after a negated if (5 extra bytes).
CodeContext.this.makeSpace(this.opcode == Opcode.GOTO || this.opcode == opcodeJsr ? 2 : 5);
}
CodeContext.this.popInserter();
this.source.offset = pos;
this.expanded = true;
}
}
@Override public void
relocate() {
if (this.destination.offset == Offset.UNSET) {
throw new InternalCompilerException("Cannot relocate branch to unset destination offset");
}
int offset = this.destination.offset - this.source.offset;
@SuppressWarnings("deprecation") final int opcodeJsr = Opcode.JSR;
final byte[] ba;
if (!this.expanded) {
//we fit in a 16-bit jump
ba = new byte[] { (byte) this.opcode, (byte) (offset >> 8), (byte) offset };
} else {
if (this.opcode == Opcode.GOTO || this.opcode == opcodeJsr) {
ba = new byte[] {
(byte) (this.opcode + 33), // GOTO => GOTO_W; JSR => JSR_W
(byte) (offset >> 24),
(byte) (offset >> 16),
(byte) (offset >> 8),
(byte) offset
};
} else
{
//exclude the if-statement from jump target
//if jumping backwards this will increase the jump to go over it
//if jumping forwards this will decrease the jump by it
offset -= 3;
// [if cond offset]
//expands to
// [if !cond skip_goto]
// [GOTO_W offset]
ba = new byte[] {
(byte) CodeContext.invertBranchOpcode(this.opcode),
0,
8, // Jump from this instruction past the GOTO_W
(byte) Opcode.GOTO_W,
(byte) (offset >> 24),
(byte) (offset >> 16),
(byte) (offset >> 8),
(byte) offset
};
}
}
System.arraycopy(ba, 0, CodeContext.this.code, this.source.offset, ba.length);
}
private boolean expanded; //marks whether this has been expanded to account for a wide branch
private final int opcode;
private final Inserter source;
private final Offset destination;
}
/**
* E.g. {@link Opcode#IFLT} ("less than") inverts to {@link Opcode#IFGE} ("greater than or equal to").
*/
private static int
invertBranchOpcode(int branchOpcode) {
final Integer result = (Integer) CodeContext.BRANCH_OPCODE_INVERSION.get(Integer.valueOf(branchOpcode));
assert result != null : branchOpcode;
return result;
}
private static final Map
BRANCH_OPCODE_INVERSION = CodeContext.createBranchOpcodeInversion();
private static Map
createBranchOpcodeInversion() {
Map m = new HashMap();
m.put(Opcode.IF_ACMPEQ, Opcode.IF_ACMPNE);
m.put(Opcode.IF_ACMPNE, Opcode.IF_ACMPEQ);
m.put(Opcode.IF_ICMPEQ, Opcode.IF_ICMPNE);
m.put(Opcode.IF_ICMPNE, Opcode.IF_ICMPEQ);
m.put(Opcode.IF_ICMPGE, Opcode.IF_ICMPLT);
m.put(Opcode.IF_ICMPLT, Opcode.IF_ICMPGE);
m.put(Opcode.IF_ICMPGT, Opcode.IF_ICMPLE);
m.put(Opcode.IF_ICMPLE, Opcode.IF_ICMPGT);
m.put(Opcode.IFEQ, Opcode.IFNE);
m.put(Opcode.IFNE, Opcode.IFEQ);
m.put(Opcode.IFGE, Opcode.IFLT);
m.put(Opcode.IFLT, Opcode.IFGE);
m.put(Opcode.IFGT, Opcode.IFLE);
m.put(Opcode.IFLE, Opcode.IFGT);
m.put(Opcode.IFNULL, Opcode.IFNONNULL);
m.put(Opcode.IFNONNULL, Opcode.IFNULL);
return Collections.unmodifiableMap(m);
}
/**
* Writes a four-byte offset (as it is used in TABLESWITCH and LOOKUPSWITCH) into this code context.
*/
public void
writeOffset(Offset src, final Offset dst) {
Offset o = new FourByteOffset();
o.set();
this.relocatables.add(new OffsetBranch(o, src, dst));
this.makeSpace(4);
}
private final class FourByteOffset extends Offset {}
private
class OffsetBranch extends Relocatable {
OffsetBranch(Offset where, Offset source, Offset destination) {
this.where = where;
this.source = source;
this.destination = destination;
}
@Override public void
grow() {}
@Override public void
relocate() {
if (this.source.offset == Offset.UNSET || this.destination.offset == Offset.UNSET) {
throw new InternalCompilerException("Cannot relocate offset branch to unset destination offset");
}
int offset = this.destination.offset - this.source.offset;
byte[] ba = new byte[] {
(byte) (offset >> 24),
(byte) (offset >> 16),
(byte) (offset >> 8),
(byte) offset
};
System.arraycopy(ba, 0, CodeContext.this.code, this.where.offset, 4);
}
private final Offset where, source, destination;
}
/**
* Creates and inserts an {@link CodeContext.Offset} at the current inserter's current position.
*/
public Offset
newOffset() {
Offset o = new Offset();
o.set();
return o;
}
/**
* Allocates an {@link Inserter}, set it to the current offset, and inserts it before the current offset.
*
* In clear text, this means that you can continue writing to the "Code" attribute, then {@link
* #pushInserter(CodeContext.Inserter)} the {@link Inserter}, then write again (which inserts bytes into the
* "Code" attribute at the previously remembered position), and then {@link #popInserter()}.
*
*/
public Inserter
newInserter() { Inserter i = new Inserter(); i.set(); return i; }
/**
* @return The current inserter
*/
public Inserter
currentInserter() { return this.currentInserter; }
/**
* Remembers the current {@link Inserter}, then replaces it with the new one.
*/
public void
pushInserter(Inserter ins) {
// The current inserter MUST have a stack map; verify.
ins.getStackMap();
if (ins.nextInserter != null) {
throw new InternalCompilerException("An Inserter can only be pushed once at a time");
}
ins.nextInserter = this.currentInserter;
this.currentInserter = ins;
}
/**
* Replaces the current {@link Inserter} with the remembered one (see {@link #pushInserter(CodeContext.Inserter)}).
*/
public void
popInserter() {
Inserter ni = this.currentInserter.nextInserter;
if (ni == null) throw new InternalCompilerException("Code inserter stack underflow");
ni.getStackMap();
this.currentInserter.nextInserter = null; // Mark it as "unpushed".
this.currentInserter = ni;
}
/**
* A class that represents an offset within a "Code" attribute.
*
* The concept of an "offset" is that if one writes into the middle of a "Code" attribute, all offsets behind the
* insertion point are automatically shifted.
*
*/
public
class Offset {
/**
* The offset in the code attribute that this object represents.
*/
int offset = Offset.UNSET;
/**
* Links to preceding and succeeding offsets. Both are {@code null} before {@link #set()} is called,
* and both are non-{@code null} after {@link #set()} has been called. This implies that {@link
* #set()} must be invoked at most once.
*/
@Nullable Offset prev, next;
/**
* Special value for {@link #offset} which indicates that this {@link Offset} has not yet been {@link #set()}
*/
static final int UNSET = -1;
@Nullable private StackMap stackMap; // Is null until "set()".
/**
* Sets this "Offset" to the offset of the current inserter; inserts this "Offset" before the current inserter.
*/
public void
set() {
this.setOffset();
this.setStackMap();
Inserter ci = CodeContext.this.currentInserter;
Offset cip = ci.prev;
assert cip != null;
this.prev = cip;
this.next = ci;
cip.next = this;
ci.prev = this;
}
/**
* Merges the stack maps of the current inserter and THIS offset, and assigns the result to the current
* inserter and THIS offset.
*/
void
setStackMap() {
Inserter ci = CodeContext.this.currentInserter;
((Offset) ci).stackMap = this.stackMap = CodeContext.mergeStackMaps(((Offset) ci).stackMap, this.stackMap);
}
public void
setOffset() {
Inserter ci = CodeContext.this.currentInserter;
if (this.offset != Offset.UNSET) throw new InternalCompilerException("Offset already set");
this.offset = ci.offset;
}
public StackMap
getStackMap() { return this.stackMap; }
public void
setStackMap(StackMap stackMap) { this.stackMap = stackMap; }
/**
* @return The {@link CodeContext} that this {@link Offset} belongs to
*/
public final CodeContext getCodeContext() { return CodeContext.this; }
@Override public String
toString() { return CodeContext.this.classFile.getThisClassName() + ": " + this.offset; }
}
@Nullable private static final StackMap
mergeStackMaps(@Nullable StackMap sm1, @Nullable StackMap sm2) {
if (sm1 == null) return sm2;
if (sm2 == null) return sm1;
if (sm1 == sm2) return sm1;
if (sm1.equals(sm2)) return sm1;
if (!Arrays.equals(sm1.operands(), sm2.operands())) {
throw new InternalCompilerException("Inconsistent operand stack: " + sm1 + " vs. " + sm2);
}
VerificationTypeInfo[] locals1 = sm1.locals();
VerificationTypeInfo[] locals2 = sm2.locals();
VerificationTypeInfo[] tmp = new VerificationTypeInfo[Math.min(locals1.length, locals2.length)];
for (int i = 0; i < tmp.length; i++) {
VerificationTypeInfo local1 = locals1[i];
VerificationTypeInfo local2 = locals2[i];
VerificationTypeInfo tmpLocal;
if (local1.equals(local2)) {
tmpLocal = local1;
} else
if (local1 == StackMapTableAttribute.TOP_VARIABLE_INFO || local2 == StackMapTableAttribute.TOP_VARIABLE_INFO) {
tmpLocal = StackMapTableAttribute.TOP_VARIABLE_INFO;
} else
{
// This check would be wrong, because the different flow paths may have used the slot
// for different locals.
// throw new InternalCompilerException(
// "Inconsistent local variable #" + i + " verification type: " + sm1 + " vs. " + sm2
// );
tmpLocal = StackMapTableAttribute.TOP_VARIABLE_INFO;
}
tmp[i] = tmpLocal;
}
return new StackMap(tmp, sm1.operands());
}
/**
* Adds another entry to the "exception_table" of this code attribute (see JVMS 4.7.3).
*
* @param catchTypeFd {@code null} means {@code finally} clause
*/
public void
addExceptionTableEntry(Offset startPc, Offset endPc, Offset handlerPc, @Nullable String catchTypeFd) {
this.exceptionTableEntries.add(new ExceptionTableEntry(
startPc,
endPc,
handlerPc,
catchTypeFd == null ? (short) 0 : this.classFile.addConstantClassInfo(catchTypeFd)
));
}
/**
* Representation of an entry in the "exception_table" of a "Code" attribute (see JVMS 4.7.3).
*/
private static
class ExceptionTableEntry {
ExceptionTableEntry(Offset startPc, Offset endPc, Offset handlerPc, short catchType) {
this.startPc = startPc;
this.endPc = endPc;
this.handlerPc = handlerPc;
this.catchType = catchType;
}
final Offset startPc, endPc, handlerPc;
final short catchType; // 0 == "finally" clause
}
/**
* A class that implements an insertion point into a "Code" attribute.
*/
public
class Inserter extends Offset {
@Nullable private Inserter nextInserter; // null == not in "currentInserter" stack
}
/**
* An {@link Offset} who's sole purpose is to later create a 'LineNumberTable' attribute.
*/
public
class LineNumberOffset extends Offset {
private final short lineNumber;
/**
* @param lineNumber 1...65535
*/
public
LineNumberOffset(int offset, StackMap stackMap, short lineNumber) {
this.lineNumber = lineNumber;
this.offset = offset;
this.setStackMap(stackMap);
}
}
private abstract
class Relocatable {
/**
* Grows the code if the relocation cannot be done without growing code.
*/
public abstract void grow();
/**
* Relocates this object.
*/
public abstract void relocate();
}
/**
* A throw-in interface that marks {@link CodeContext.Offset}s as "fix-ups": During the execution of {@link
* CodeContext#fixUp}, all "fix-ups" are invoked and can do last touches to the code attribute.
*
* This is currently used for inserting the "padding bytes" into the TABLESWITCH and LOOKUPSWITCH instructions.
*
*/
public
interface FixUp {
/**
* @see FixUp
*/
void fixUp();
}
/**
* @return All the local variables that are allocated in any block in this {@link CodeContext}
*/
public List
getAllLocalVars() { return this.allLocalVars; }
/**
* Removes all code between from and to. Also removes any {@link CodeContext.Relocatable}s
* existing in that range.
*/
public void
removeCode(Offset from, Offset to) {
if (from == to) return;
int size = to.offset - from.offset;
assert size >= 0;
if (size == 0) return; // Short circuit.
// Shift down the bytecode past "to".
System.arraycopy(this.code, to.offset, this.code, from.offset, this.end.offset - to.offset);
// Invalidate all offsets between "from" and "to".
// Remove all relocatables that originate between "from" and "to".
Set invalidOffsets = new HashSet();
{
Offset o = from.next;
assert o != null;
for (; o != to;) {
assert o != null;
invalidOffsets.add(o);
// Invalidate the offset for fast failure.
final Offset n = o.next;
o.offset = -77;
o.prev = null;
o.next = null;
o = n;
assert o != null;
}
for (;;) {
o.offset -= size;
if (o == this.end) break;
o = o.next;
assert o != null;
}
}
// Invalidate all relocatables which originate or target a removed offset.
for (Iterator it = this.relocatables.iterator(); it.hasNext();) {
Relocatable r = (Relocatable) it.next();
if (r instanceof Branch) {
Branch b = (Branch) r;
if (invalidOffsets.contains(b.source)) {
it.remove();
} else {
assert !invalidOffsets.contains(b.destination);
}
}
if (r instanceof OffsetBranch) {
OffsetBranch ob = (OffsetBranch) r;
if (invalidOffsets.contains(ob.source)) {
it.remove();
} else {
assert !invalidOffsets.contains(ob.destination);
}
}
}
for (Iterator it = this.exceptionTableEntries.iterator(); it.hasNext();) {
ExceptionTableEntry ete = (ExceptionTableEntry) it.next();
// Start, end and handler must either ALL lie IN the range to remove or ALL lie outside.
if (invalidOffsets.contains(ete.startPc)) {
assert invalidOffsets.contains(ete.endPc);
assert invalidOffsets.contains(ete.handlerPc);
it.remove();
} else {
assert !invalidOffsets.contains(ete.endPc);
assert !invalidOffsets.contains(ete.handlerPc);
}
}
// Remove local variables in dead-code block.
for (Iterator it = this.allLocalVars.iterator(); it.hasNext();) {
final Java.LocalVariableSlot var = (Java.LocalVariableSlot) it.next();
if (invalidOffsets.contains(var.getStart())) {
assert invalidOffsets.contains(var.getEnd());
it.remove();
} else {
assert !invalidOffsets.contains(var.getEnd());
}
}
from.next = to;
to.prev = from;
}
@Override public String
toString() { return this.classFile.getThisClassName() + "/cio=" + this.currentInserter.offset; }
// Convenience methods for "pushOperand(VTI)".
/**
* Pushes one {@code object_variable_info}, {@code integer_variable_info}, {@code double_variable_info}, {@code
* float_variable_info} or {@code long_variable_info} entry onto the current inserter's operand stack.
*/
public void
pushOperand(String fieldDescriptor) {
if (Descriptor.isReference(fieldDescriptor)) {
this.pushObjectOperand(fieldDescriptor);
} else
if (
fieldDescriptor.equals(Descriptor.BYTE)
|| fieldDescriptor.equals(Descriptor.CHAR)
|| fieldDescriptor.equals(Descriptor.INT)
|| fieldDescriptor.equals(Descriptor.SHORT)
|| fieldDescriptor.equals(Descriptor.BOOLEAN)
) {
this.pushIntOperand();
} else
if (fieldDescriptor.equals(Descriptor.DOUBLE)) {
this.pushDoubleOperand();
} else
if (fieldDescriptor.equals(Descriptor.FLOAT)) {
this.pushFloatOperand();
} else
if (fieldDescriptor.equals(Descriptor.LONG)) {
this.pushLongOperand();
} else
{
throw new AssertionError(fieldDescriptor);
}
}
public void pushTopOperand() { this.pushOperand(StackMapTableAttribute.TOP_VARIABLE_INFO); }
public void pushIntOperand() { this.pushOperand(StackMapTableAttribute.INTEGER_VARIABLE_INFO); }
public void pushLongOperand() { this.pushOperand(StackMapTableAttribute.LONG_VARIABLE_INFO); }
public void pushFloatOperand() { this.pushOperand(StackMapTableAttribute.FLOAT_VARIABLE_INFO); }
public void pushDoubleOperand() { this.pushOperand(StackMapTableAttribute.DOUBLE_VARIABLE_INFO); }
public void pushNullOperand() { this.pushOperand(StackMapTableAttribute.NULL_VARIABLE_INFO); }
public void pushUninitializedThisOperand() { this.pushOperand(StackMapTableAttribute.UNINITIALIZED_THIS_VARIABLE_INFO); }
public void
pushUninitializedOperand() {
final Offset o = this.newOffset();
final UninitializedVariableInfo uvi = this.classFile.newUninitializedVariableInfo((short) o.offset);
this.relocatables.add(new Relocatable() {
@Override public void
grow() {}
@Override public void
relocate() { uvi.offset = (short) o.offset; }
});
this.pushOperand(uvi);
}
public void
pushObjectOperand(String fieldDescriptor) {
this.pushOperand(this.classFile.newObjectVariableInfo(fieldDescriptor));
}
public void
pushOperand(VerificationTypeInfo topOperand) {
final Inserter ci = this.currentInserter();
StackMap sm = ci.getStackMap();
sm = sm.pushOperand(topOperand);
ci.setStackMap(sm);
int ss = 0;
for (VerificationTypeInfo vti : sm.operands()) ss += vti.category();
if (ss > this.maxStack) this.maxStack = ss;
}
/**
* @return Whether the top operand is a {@code null_variable_info}
*/
public boolean
peekNullOperand() {
return this.peekOperand() == StackMapTableAttribute.NULL_VARIABLE_INFO;
}
/**
* @return Whether the top operand is a {@code object_variable_info}
*/
public boolean
peekObjectOperand() {
return this.peekOperand() instanceof StackMapTableAttribute.ObjectVariableInfo;
}
/**
* @return The verification type of the top operand
*/
public VerificationTypeInfo
peekOperand() { return this.currentInserter().getStackMap().peekOperand(); }
/**
* Pops one entry from the current inserter's operand stack.
*/
public VerificationTypeInfo
popOperand() {
Inserter ci = this.currentInserter();
StackMap sm = ci.getStackMap();
for (;;) {
VerificationTypeInfo result = sm.peekOperand();
sm = sm.popOperand();
if (result != StackMapTableAttribute.TOP_VARIABLE_INFO) {
ci.setStackMap(sm);
return result;
}
}
}
/**
* Pops the top entry from the operand stack and assert that it equals expected.
*/
public void
popOperand(VerificationTypeInfo expected) {
VerificationTypeInfo actual = this.popOperand();
assert actual.equals(expected) : actual;
}
/**
* Pops the top operand, asserts that it is an {@code integer_variable_info}, {@code long_variable_info}, {@code
* float_variable_info}, {@code double_variable_info} or {@code variable_object_info}, and asserts that it matches
* the given field descriptor.
*/
public void
popOperand(String expectedFd) {
VerificationTypeInfo vti = this.popOperand();
if (vti == StackMapTableAttribute.INTEGER_VARIABLE_INFO) {
assert (
expectedFd.equals(Descriptor.BOOLEAN)
|| expectedFd.equals(Descriptor.BYTE)
|| expectedFd.equals(Descriptor.CHAR)
|| expectedFd.equals(Descriptor.SHORT)
|| expectedFd.equals(Descriptor.INT)
) : expectedFd;
} else
if (vti == StackMapTableAttribute.LONG_VARIABLE_INFO) {
assert expectedFd.equals(Descriptor.LONG) : expectedFd;
} else
if (vti == StackMapTableAttribute.FLOAT_VARIABLE_INFO) {
assert expectedFd.equals(Descriptor.FLOAT) : expectedFd;
} else
if (vti == StackMapTableAttribute.DOUBLE_VARIABLE_INFO) {
assert expectedFd.equals(Descriptor.DOUBLE) : expectedFd;
} else
if (vti == StackMapTableAttribute.NULL_VARIABLE_INFO) {
assert expectedFd.equals(Descriptor.VOID) : expectedFd;
} else
if (vti instanceof StackMapTableAttribute.ObjectVariableInfo) {
assert Descriptor.isReference(expectedFd) : expectedFd + " vs. " + vti;
final ObjectVariableInfo ovi = (StackMapTableAttribute.ObjectVariableInfo) vti;
final ConstantClassInfo cci = this.classFile.getConstantClassInfo(ovi.getConstantClassInfoIndex());
final String computationalTypeFd = Descriptor.fromInternalForm(cci.getName(this.classFile));
assert expectedFd.equals(computationalTypeFd) : expectedFd + " vs. " + computationalTypeFd;
} else
if (vti instanceof StackMapTableAttribute.UninitializedVariableInfo) {
assert Descriptor.isReference(expectedFd) : expectedFd;
} else
{
throw new AssertionError(vti);
}
}
public void
popOperandAssignableTo(String declaredFd) {
if (Descriptor.isPrimitive(declaredFd)) {
this.popOperand(declaredFd);
} else {
this.popObjectOrUninitializedOrUninitializedThisOperand();
}
}
/**
* Asserts that the top operand is an {@code integer_variable_info} and pops it.
*/
public void
popIntOperand() { this.popOperand(StackMapTableAttribute.INTEGER_VARIABLE_INFO); }
/**
* Asserts that the top operand is a {@code long_variable_info} and pops it.
*/
public void
popLongOperand() { this.popOperand(StackMapTableAttribute.LONG_VARIABLE_INFO); }
/**
* Asserts that the top operand is an {@code uninitializedThis_variable_info} and pops it.
*/
public void
popUninitializedThisOperand() { this.popOperand(StackMapTableAttribute.UNINITIALIZED_THIS_VARIABLE_INFO); }
/**
* Asserts that the top operand is an {@code uninitialized_variable_info} and pops it.
*/
public void
popUninitializedVariableOperand() {
final VerificationTypeInfo op = this.popOperand();
assert op instanceof StackMapTableAttribute.UninitializedVariableInfo : String.valueOf(op);
}
/**
* Asserts that the top operand is an {@code object_variable_info} or a {@code null_variable_info} and pops it.
*/
public void
popReferenceOperand() {
assert this.peekObjectOperand() || this.peekNullOperand() : this.peekOperand();
this.popOperand();
}
/**
* Asserts that the top operand is an {@code object_variable_info}, and pops it.
*
* @return The field descriptor of the popped object operand
*/
public String
popObjectOperand() {
VerificationTypeInfo vti = this.popOperand();
assert vti instanceof StackMapTableAttribute.ObjectVariableInfo : vti;
final ObjectVariableInfo ovi = (StackMapTableAttribute.ObjectVariableInfo) vti;
short ccii = ovi.getConstantClassInfoIndex();
ConstantClassInfo cci = this.classFile.getConstantClassInfo(ccii);
return Descriptor.fromInternalForm(cci.getName(this.classFile));
}
/**
* Asserts that the top operand is an {@code object_variable_info}, {@code uninitialized_variable_info} or
* {@code uninitializedThis_variable_info}, and pops it.
*/
public VerificationTypeInfo
popObjectOrUninitializedOrUninitializedThisOperand() {
VerificationTypeInfo result = this.popOperand();
assert (
result instanceof StackMapTableAttribute.UninitializedVariableInfo
|| result instanceof StackMapTableAttribute.ObjectVariableInfo
|| result == StackMapTableAttribute.UNINITIALIZED_THIS_VARIABLE_INFO
) : result;
return result;
}
/**
* Asserts that the top operand is an {@code int_variable_info} or {@code long_variable_info}, then pops and
* returns it.
*/
public VerificationTypeInfo
popIntOrLongOperand() {
VerificationTypeInfo result = this.popOperand();
assert (
result == StackMapTableAttribute.INTEGER_VARIABLE_INFO
|| result == StackMapTableAttribute.LONG_VARIABLE_INFO
) : result;
return result;
}
}