org.codehaus.groovy.classgen.asm.CompileStack Maven / Gradle / Ivy
/*
* Copyright 2003-2013 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.codehaus.groovy.classgen.asm;
import org.codehaus.groovy.GroovyBugError;
import org.codehaus.groovy.ast.ClassHelper;
import org.codehaus.groovy.ast.ClassNode;
import org.codehaus.groovy.ast.Parameter;
import org.codehaus.groovy.ast.Variable;
import org.codehaus.groovy.ast.VariableScope;
import org.objectweb.asm.Label;
import org.objectweb.asm.MethodVisitor;
import org.objectweb.asm.Opcodes;
import java.util.*;
/**
* This class is a helper for AsmClassGenerator. It manages
* different aspects of the code of a code block like
* handling labels, defining variables, and scopes.
* After a MethodNode is visited clear should be called, for
* initialization the method init should be used.
*
* Some Notes:
*
* - every push method will require a later pop call
*
- method parameters may define a category 2 variable, so
* don't ignore the type stored in the variable object
*
- the index of the variable may not be as assumed when
* the variable is a parameter of a method because the
* parameter may be used in a closure, so don't ignore
* the stored variable index
*
- the names of temporary variables can be ignored. The names
* are only used for debugging and do not conflict with each
* other or normal variables. For accessing, the index of the
* variable must be used.
*
- never mix temporary and normal variables by changes to this class.
* While the name is very important for a normal variable, it is only a
* helper construct for temporary variables. That means for example a
* name for a temporary variable can be used multiple times without
* conflict. So mixing them both may lead to the problem that a normal
* or temporary variable is hidden or even removed. That must not happen!
*
*
*
* @see org.codehaus.groovy.classgen.AsmClassGenerator
* @author Jochen Theodorou
*/
public class CompileStack implements Opcodes {
/**
* @todo remove optimization of this.foo -> this.@foo
*
*/
// state flag
private boolean clear=true;
// current scope
private VariableScope scope;
// current label for continue
private Label continueLabel;
// current label for break
private Label breakLabel;
// available variables on stack
private Map stackVariables = new HashMap();
// index of the last variable on stack
private int currentVariableIndex = 1;
// index for the next variable on stack
private int nextVariableIndex = 1;
// currently temporary variables in use
private final LinkedList temporaryVariables = new LinkedList();
// overall used variables for a method/constructor
private final LinkedList usedVariables = new LinkedList();
// map containing named labels of parenting blocks
private Map superBlockNamedLabels = new HashMap();
// map containing named labels of current block
private Map currentBlockNamedLabels = new HashMap();
// list containing finally blocks
// such a block is created by synchronized or finally and
// must be called for break/continue/return
private LinkedList finallyBlocks = new LinkedList();
private LinkedList visitedBlocks = new LinkedList();
private Label thisStartLabel, thisEndLabel;
// private MethodVisitor mv;
// helper to handle different stack based variables
private final LinkedList stateStack = new LinkedList();
// handle different states for the implicit "this"
private LinkedList implicitThisStack = new LinkedList();
// handle different states for being on the left hand side
private LinkedList lhsStack = new LinkedList();
{
implicitThisStack.add(false);
lhsStack.add(false);
}
// defines the first variable index usable after
// all parameters of a method
private int localVariableOffset;
// this is used to store the goals for a "break foo" call
// in a loop where foo is a label.
private final Map namedLoopBreakLabel = new HashMap();
// this is used to store the goals for a "continue foo" call
// in a loop where foo is a label.
private final Map namedLoopContinueLabel = new HashMap();
private String className;
private LinkedList typedExceptions = new LinkedList();
private LinkedList untypedExceptions = new LinkedList();
// stores if on left-hand-side during compilation
private boolean lhs;
// stores if implicit or explicit this is used.
private boolean implicitThis;
private WriterController controller;
private boolean inSpecialConstructallCall;
protected static class LabelRange {
public Label start;
public Label end;
}
public static class BlockRecorder {
private boolean isEmpty = true;
public Runnable excludedStatement;
public LinkedList ranges;
public BlockRecorder() {
ranges = new LinkedList();
}
public BlockRecorder(Runnable excludedStatement) {
this();
this.excludedStatement = excludedStatement;
}
public void startRange(Label start) {
LabelRange range = new LabelRange();
range.start = start;
ranges.add(range);
isEmpty = false;
}
public void closeRange(Label end) {
ranges.getLast().end = end;
}
}
private class ExceptionTableEntry {
Label start,end,goal;
String sig;
}
private class StateStackElement {
final VariableScope scope;
final Label continueLabel;
final Label breakLabel;
final Map stackVariables;
final Map currentBlockNamedLabels;
final LinkedList finallyBlocks;
final boolean inSpecialConstructallCall;
StateStackElement() {
scope = CompileStack.this.scope;
continueLabel = CompileStack.this.continueLabel;
breakLabel = CompileStack.this.breakLabel;
stackVariables = CompileStack.this.stackVariables;
currentBlockNamedLabels = CompileStack.this.currentBlockNamedLabels;
finallyBlocks = CompileStack.this.finallyBlocks;
inSpecialConstructallCall = CompileStack.this.inSpecialConstructallCall;
}
}
public CompileStack(WriterController wc) {
this.controller = wc;
}
public void pushState() {
stateStack.add(new StateStackElement());
stackVariables = new HashMap(stackVariables);
finallyBlocks = new LinkedList(finallyBlocks);
}
private void popState() {
if (stateStack.size()==0) {
throw new GroovyBugError("Tried to do a pop on the compile stack without push.");
}
StateStackElement element = (StateStackElement) stateStack.removeLast();
scope = element.scope;
continueLabel = element.continueLabel;
breakLabel = element.breakLabel;
stackVariables = element.stackVariables;
finallyBlocks = element.finallyBlocks;
inSpecialConstructallCall = element.inSpecialConstructallCall;
}
public Label getContinueLabel() {
return continueLabel;
}
public Label getBreakLabel() {
return breakLabel;
}
public void removeVar(int tempIndex) {
final BytecodeVariable head = (BytecodeVariable) temporaryVariables.removeFirst();
if (head.getIndex() != tempIndex) {
temporaryVariables.addFirst(head);
throw new GroovyBugError(
"CompileStack#removeVar: tried to remove a temporary " +
"variable with index "+ tempIndex + " in wrong order. " +
"Current temporary variables=" + temporaryVariables);
}
}
private void setEndLabels(){
Label endLabel = new Label();
controller.getMethodVisitor().visitLabel(endLabel);
for (Iterator iter = stackVariables.values().iterator(); iter.hasNext();) {
BytecodeVariable var = (BytecodeVariable) iter.next();
var.setEndLabel(endLabel);
}
thisEndLabel = endLabel;
}
public void pop() {
setEndLabels();
popState();
}
public VariableScope getScope() {
return scope;
}
/**
* creates a temporary variable.
*
* @param var defines type and name
* @param store defines if the toplevel argument of the stack should be stored
* @return the index used for this temporary variable
*/
public int defineTemporaryVariable(org.codehaus.groovy.ast.Variable var, boolean store) {
return defineTemporaryVariable(var.getName(), var.getType(),store);
}
public BytecodeVariable getVariable(String variableName ) {
return getVariable(variableName,true);
}
/**
* Returns a normal variable.
*
* If mustExist
is true and the normal variable doesn't exist,
* then this method will throw a GroovyBugError. It is not the intention of
* this method to let this happen! And the exception should not be used for
* flow control - it is just acting as an assertion. If the exception is thrown
* then it indicates a bug in the class using CompileStack.
* This method can also not be used to return a temporary variable.
* Temporary variables are not normal variables.
*
* @param variableName name of the variable
* @param mustExist throw exception if variable does not exist
* @return the normal variable or null if not found (and mustExist
not true)
*/
public BytecodeVariable getVariable(String variableName, boolean mustExist) {
if (variableName.equals("this")) return BytecodeVariable.THIS_VARIABLE;
if (variableName.equals("super")) return BytecodeVariable.SUPER_VARIABLE;
BytecodeVariable v = (BytecodeVariable) stackVariables.get(variableName);
if (v == null && mustExist)
throw new GroovyBugError("tried to get a variable with the name " + variableName + " as stack variable, but a variable with this name was not created");
return v;
}
/**
* creates a temporary variable.
*
* @param name defines type and name
* @param store defines if the toplevel argument of the stack should be stored
* @return the index used for this temporary variable
*/
public int defineTemporaryVariable(String name,boolean store) {
return defineTemporaryVariable(name, ClassHelper.DYNAMIC_TYPE,store);
}
/**
* creates a temporary variable.
*
* @param name defines the name
* @param node defines the node
* @param store defines if the toplevel argument of the stack should be stored
* @return the index used for this temporary variable
*/
public int defineTemporaryVariable(String name, ClassNode node, boolean store) {
BytecodeVariable answer = defineVar(name, node, false, false);
temporaryVariables.addFirst(answer); // TRICK: we add at the beginning so when we find for remove or get we always have the last one
usedVariables.removeLast();
if (store) controller.getOperandStack().storeVar(answer);
return answer.getIndex();
}
private void resetVariableIndex(boolean isStatic) {
temporaryVariables.clear();
if (!isStatic) {
currentVariableIndex=1;
nextVariableIndex=1;
} else {
currentVariableIndex=0;
nextVariableIndex=0;
}
}
/**
* Clears the state of the class. This method should be called
* after a MethodNode is visited. Note that a call to init will
* fail if clear is not called before
*/
public void clear() {
if (stateStack.size()>1) {
int size = stateStack.size()-1;
throw new GroovyBugError("the compile stack contains "+size+" more push instruction"+(size==1?"":"s")+" than pops.");
}
if (lhsStack.size()>1) {
int size = lhsStack.size()-1;
throw new GroovyBugError("lhs stack is supposed to be empty, but has " +
size + " elements left.");
}
if (implicitThisStack.size()>1) {
int size = implicitThisStack.size()-1;
throw new GroovyBugError("implicit 'this' stack is supposed to be empty, but has " +
size + " elements left.");
}
clear = true;
MethodVisitor mv = controller.getMethodVisitor();
// br experiment with local var table so debuggers can retrieve variable names
if (true) {//AsmClassGenerator.CREATE_DEBUG_INFO) {
if (thisEndLabel==null) setEndLabels();
if (!scope.isInStaticContext()) {
// write "this"
mv.visitLocalVariable("this", className, null, thisStartLabel, thisEndLabel, 0);
}
for (Iterator iterator = usedVariables.iterator(); iterator.hasNext();) {
BytecodeVariable v = (BytecodeVariable) iterator.next();
ClassNode t = v.getType();
if (v.isHolder()) t = ClassHelper.REFERENCE_TYPE;
String type = BytecodeHelper.getTypeDescription(t);
Label start = v.getStartLabel();
Label end = v.getEndLabel();
mv.visitLocalVariable(v.getName(), type, null, start, end, v.getIndex());
}
}
//exception table writing
for (ExceptionTableEntry ep : typedExceptions) {
mv.visitTryCatchBlock(ep.start, ep.end, ep.goal, ep.sig);
}
//exception table writing
for (ExceptionTableEntry ep : untypedExceptions) {
mv.visitTryCatchBlock(ep.start, ep.end, ep.goal, ep.sig);
}
pop();
typedExceptions.clear();
untypedExceptions.clear();
stackVariables.clear();
usedVariables.clear();
scope = null;
finallyBlocks.clear();
mv=null;
resetVariableIndex(false);
superBlockNamedLabels.clear();
currentBlockNamedLabels.clear();
namedLoopBreakLabel.clear();
namedLoopContinueLabel.clear();
continueLabel=null;
breakLabel=null;
thisStartLabel=null;
thisEndLabel=null;
mv = null;
}
public void addExceptionBlock (Label start, Label end, Label goal,
String sig)
{
// this code is in an extra method to avoid
// lazy initialization issues
ExceptionTableEntry ep = new ExceptionTableEntry();
ep.start = start;
ep.end = end;
ep.sig = sig;
ep.goal = goal;
if (sig==null) {
untypedExceptions.add(ep);
} else {
typedExceptions.add(ep);
}
}
/**
* initializes this class for a MethodNode. This method will
* automatically define variables for the method parameters
* and will create references if needed. The created variables
* can be accessed by calling getVariable().
*
*/
public void init(VariableScope el, Parameter[] parameters) {
if (!clear) throw new GroovyBugError("CompileStack#init called without calling clear before");
clear=false;
pushVariableScope(el);
defineMethodVariables(parameters,el.isInStaticContext());
this.className = BytecodeHelper.getTypeDescription(controller.getClassNode());
}
/**
* Causes the statestack to add an element and sets
* the given scope as new current variable scope. Creates
* a element for the state stack so pop has to be called later
*/
public void pushVariableScope(VariableScope el) {
pushState();
scope = el;
superBlockNamedLabels = new HashMap(superBlockNamedLabels);
superBlockNamedLabels.putAll(currentBlockNamedLabels);
currentBlockNamedLabels = new HashMap();
}
/**
* Should be called when decending into a loop that defines
* also a scope. Calls pushVariableScope and prepares labels
* for a loop structure. Creates a element for the state stack
* so pop has to be called later
*/
public void pushLoop(VariableScope el, String labelName) {
pushVariableScope(el);
initLoopLabels(labelName);
}
private void initLoopLabels(String labelName) {
continueLabel = new Label();
breakLabel = new Label();
if (labelName!=null) {
namedLoopBreakLabel.put(labelName,breakLabel);
namedLoopContinueLabel.put(labelName,continueLabel);
}
}
/**
* Should be called when decending into a loop that does
* not define a scope. Creates a element for the state stack
* so pop has to be called later
*/
public void pushLoop(String labelName) {
pushState();
initLoopLabels(labelName);
}
/**
* Used for break foo
inside a loop to end the
* execution of the marked loop. This method will return the
* break label of the loop if there is one found for the name.
* If not, the current break label is returned.
*/
public Label getNamedBreakLabel(String name) {
Label label = getBreakLabel();
Label endLabel = null;
if (name!=null) endLabel = (Label) namedLoopBreakLabel.get(name);
if (endLabel!=null) label = endLabel;
return label;
}
/**
* Used for continue foo
inside a loop to continue
* the execution of the marked loop. This method will return
* the break label of the loop if there is one found for the
* name. If not, getLabel is used.
*/
public Label getNamedContinueLabel(String name) {
Label label = getLabel(name);
Label endLabel = null;
if (name!=null) endLabel = (Label) namedLoopContinueLabel.get(name);
if (endLabel!=null) label = endLabel;
return label;
}
/**
* Creates a new break label and a element for the state stack
* so pop has to be called later
*/
public Label pushSwitch(){
pushState();
breakLabel = new Label();
return breakLabel;
}
/**
* because a boolean Expression may not be evaluated completely
* it is important to keep the registers clean
*/
public void pushBooleanExpression(){
pushState();
}
private BytecodeVariable defineVar(String name, ClassNode type, boolean holder, boolean useReferenceDirectly) {
int prevCurrent = currentVariableIndex;
makeNextVariableID(type,useReferenceDirectly);
int index = currentVariableIndex;
if (holder && !useReferenceDirectly) index = localVariableOffset++;
BytecodeVariable answer = new BytecodeVariable(index, type, name, prevCurrent);
usedVariables.add(answer);
answer.setHolder(holder);
return answer;
}
private void makeLocalVariablesOffset(Parameter[] paras,boolean isInStaticContext) {
resetVariableIndex(isInStaticContext);
for (int i = 0; i < paras.length; i++) {
makeNextVariableID(paras[i].getType(),false);
}
localVariableOffset = nextVariableIndex;
resetVariableIndex(isInStaticContext);
}
private void defineMethodVariables(Parameter[] paras, boolean isInStaticContext) {
Label startLabel = new Label();
thisStartLabel = startLabel;
controller.getMethodVisitor().visitLabel(startLabel);
makeLocalVariablesOffset(paras,isInStaticContext);
for (int i = 0; i < paras.length; i++) {
String name = paras[i].getName();
BytecodeVariable answer;
ClassNode type = paras[i].getType();
if (paras[i].isClosureSharedVariable()) {
boolean useExistingReference = paras[i].getNodeMetaData(ClosureWriter.UseExistingReference.class) != null;
answer = defineVar(name, paras[i].getOriginType(), true, useExistingReference);
answer.setStartLabel(startLabel);
if (!useExistingReference) {
controller.getOperandStack().load(type,currentVariableIndex);
controller.getOperandStack().box();
// GROOVY-4237, the original variable should always appear
// in the variable index, otherwise some programs get into
// trouble. So we define a dummy variable for the packaging
// phase and let it end right away before the normal
// reference will be used
Label newStart = new Label();
controller.getMethodVisitor().visitLabel(newStart);
BytecodeVariable var = new BytecodeVariable(currentVariableIndex, paras[i].getOriginType(), name, currentVariableIndex);
var.setStartLabel(startLabel);
var.setEndLabel(newStart);
usedVariables.add(var);
answer.setStartLabel(newStart);
createReference(answer);
}
} else {
answer = defineVar(name, type, false, false);
answer.setStartLabel(startLabel);
}
stackVariables.put(name, answer);
}
nextVariableIndex = localVariableOffset;
}
private void createReference(BytecodeVariable reference) {
MethodVisitor mv = controller.getMethodVisitor();
mv.visitTypeInsn(NEW, "groovy/lang/Reference");
mv.visitInsn(DUP_X1);
mv.visitInsn(SWAP);
mv.visitMethodInsn(INVOKESPECIAL, "groovy/lang/Reference", "", "(Ljava/lang/Object;)V");
mv.visitVarInsn(ASTORE, reference.getIndex());
}
private void pushInitValue(ClassNode type, MethodVisitor mv) {
if (ClassHelper.isPrimitiveType(type)) {
if (type==ClassHelper.long_TYPE) {
mv.visitInsn(LCONST_0);
} else if (type==ClassHelper.double_TYPE) {
mv.visitInsn(DCONST_0);
} else if (type==ClassHelper.float_TYPE) {
mv.visitInsn(FCONST_0);
} else {
mv.visitLdcInsn(0);
}
} else {
mv.visitInsn(ACONST_NULL);
}
}
/**
* Defines a new Variable using an AST variable.
* @param initFromStack if true the last element of the
* stack will be used to initialize
* the new variable. If false null
* will be used.
*/
public BytecodeVariable defineVariable(Variable v, boolean initFromStack) {
return defineVariable(v, v.getOriginType(), initFromStack);
}
public BytecodeVariable defineVariable(Variable v, ClassNode variableType, boolean initFromStack) {
//TODO: any usage of this method should have different operand stack hanlding
// then the remove(1) here and there in this one can be removed and others
// can be changed
String name = v.getName();
BytecodeVariable answer = defineVar(name, variableType, v.isClosureSharedVariable(), v.isClosureSharedVariable());
stackVariables.put(name, answer);
MethodVisitor mv = controller.getMethodVisitor();
Label startLabel = new Label();
answer.setStartLabel(startLabel);
ClassNode type = answer.getType().redirect();
OperandStack operandStack = controller.getOperandStack();
if (!initFromStack) pushInitValue(type, mv);
operandStack.push(answer.getType());
if (answer.isHolder()) {
operandStack.box();
operandStack.remove(1);
createReference(answer);
} else {
operandStack.storeVar(answer);
}
mv.visitLabel(startLabel);
return answer;
}
/**
* @param name the name of the variable of interest
* @return true if a variable is already defined
*/
public boolean containsVariable(String name) {
return stackVariables.containsKey(name);
}
/**
* Calculates the index of the next free register stores it
* and sets the current variable index to the old value
*/
private void makeNextVariableID(ClassNode type, boolean useReferenceDirectly) {
currentVariableIndex = nextVariableIndex;
if ((type==ClassHelper.long_TYPE || type==ClassHelper.double_TYPE) && !useReferenceDirectly) {
nextVariableIndex++;
}
nextVariableIndex++;
}
/**
* Returns the label for the given name
*/
public Label getLabel(String name) {
if (name==null) return null;
Label l = (Label) superBlockNamedLabels.get(name);
if (l==null) l = createLocalLabel(name);
return l;
}
/**
* creates a new named label
*/
public Label createLocalLabel(String name) {
Label l = (Label) currentBlockNamedLabels.get(name);
if (l==null) {
l = new Label();
currentBlockNamedLabels.put(name,l);
}
return l;
}
public void applyFinallyBlocks(Label label, boolean isBreakLabel) {
// first find the state defining the label. That is the state
// directly after the state not knowing this label. If no state
// in the list knows that label, then the defining state is the
// current state.
StateStackElement result = null;
for (ListIterator iter = stateStack.listIterator(stateStack.size()); iter.hasPrevious();) {
StateStackElement element = (StateStackElement) iter.previous();
if (!element.currentBlockNamedLabels.values().contains(label)) {
if (isBreakLabel && element.breakLabel != label) {
result = element;
break;
}
if (!isBreakLabel && element.continueLabel != label) {
result = element;
break;
}
}
}
List blocksToRemove;
if (result==null) {
// all Blocks do know the label, so use all finally blocks
blocksToRemove = (List) Collections.EMPTY_LIST;
} else {
blocksToRemove = result.finallyBlocks;
}
List blocks = new LinkedList(finallyBlocks);
blocks.removeAll(blocksToRemove);
applyBlockRecorder(blocks);
}
private void applyBlockRecorder(List blocks) {
if (blocks.size()==0 || blocks.size()==visitedBlocks.size()) return;
MethodVisitor mv = controller.getMethodVisitor();
Label end = new Label();
mv.visitInsn(NOP);
mv.visitLabel(end);
Label newStart = new Label();
for (BlockRecorder fb : blocks) {
if (visitedBlocks.contains(fb)) continue;
fb.closeRange(end);
// we exclude the finally block from the exception table
// here to avoid double visiting of finally statements
fb.excludedStatement.run();
fb.startRange(newStart);
}
mv.visitInsn(NOP);
mv.visitLabel(newStart);
}
public void applyBlockRecorder() {
applyBlockRecorder(finallyBlocks);
}
public boolean hasBlockRecorder() {
return !finallyBlocks.isEmpty();
}
public void pushBlockRecorder(BlockRecorder recorder) {
pushState();
finallyBlocks.addFirst(recorder);
}
public void pushBlockRecorderVisit(BlockRecorder finallyBlock) {
visitedBlocks.add(finallyBlock);
}
public void popBlockRecorderVisit(BlockRecorder finallyBlock) {
visitedBlocks.remove(finallyBlock);
}
public void writeExceptionTable(BlockRecorder block, Label goal, String sig) {
if (block.isEmpty) return;
MethodVisitor mv = controller.getMethodVisitor();
for (LabelRange range : block.ranges) {
mv.visitTryCatchBlock(range.start, range.end, goal, sig);
}
}
// public MethodVisitor getMethodVisitor() {
// return mv;
// }
public boolean isLHS() {
return lhs;
}
public void pushLHS(boolean lhs) {
lhsStack.add(lhs);
this.lhs = lhs;
}
public void popLHS() {
lhsStack.removeLast();
this.lhs = lhsStack.getLast();
}
public void pushImplicitThis(boolean implicitThis) {
implicitThisStack.add(implicitThis);
this.implicitThis = implicitThis;
}
public boolean isImplicitThis() {
return implicitThis;
}
public void popImplicitThis() {
implicitThisStack.removeLast();
this.implicitThis = implicitThisStack.getLast();
}
public boolean isInSpecialConstructorCall() {
return inSpecialConstructallCall;
}
public void pushInSpecialConstructorCall() {
pushState();
inSpecialConstructallCall = true;
}
}