/*
* Copyright 2003-2009 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 java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import org.codehaus.groovy.GroovyBugError;
import org.codehaus.groovy.ast.ASTNode;
import org.codehaus.groovy.ast.ClassHelper;
import org.codehaus.groovy.ast.ClassNode;
import org.codehaus.groovy.ast.CodeVisitorSupport;
import org.codehaus.groovy.ast.ConstructorNode;
import org.codehaus.groovy.ast.FieldNode;
import org.codehaus.groovy.ast.InnerClassNode;
import org.codehaus.groovy.ast.MethodNode;
import org.codehaus.groovy.ast.Parameter;
import org.codehaus.groovy.ast.Variable;
import org.codehaus.groovy.ast.VariableScope;
import org.codehaus.groovy.ast.expr.ArgumentListExpression;
import org.codehaus.groovy.ast.expr.ClassExpression;
import org.codehaus.groovy.ast.expr.ClosureExpression;
import org.codehaus.groovy.ast.expr.ConstantExpression;
import org.codehaus.groovy.ast.expr.ConstructorCallExpression;
import org.codehaus.groovy.ast.expr.Expression;
import org.codehaus.groovy.ast.expr.FieldExpression;
import org.codehaus.groovy.ast.expr.MethodCallExpression;
import org.codehaus.groovy.ast.expr.TupleExpression;
import org.codehaus.groovy.ast.expr.VariableExpression;
import org.codehaus.groovy.ast.stmt.BlockStatement;
import org.codehaus.groovy.ast.stmt.ExpressionStatement;
import org.codehaus.groovy.ast.stmt.ReturnStatement;
import org.codehaus.groovy.classgen.AsmClassGenerator;
import org.codehaus.groovy.classgen.Verifier;
import org.objectweb.asm.MethodVisitor;
import static org.objectweb.asm.Opcodes.*;
public class ClosureWriter {
protected interface UseExistingReference {}
private final Map<Expression,ClassNode> closureClassMap;
private final WriterController controller;
private final WriterControllerFactory factory;
public ClosureWriter(WriterController wc) {
this.controller = wc;
closureClassMap = new HashMap<Expression,ClassNode>();
factory = new WriterControllerFactory() {
public WriterController makeController(final WriterController normalController) {
return controller;
}
};
}
public void writeClosure(ClosureExpression expression) {
CompileStack compileStack = controller.getCompileStack();
MethodVisitor mv = controller.getMethodVisitor();
ClassNode classNode = controller.getClassNode();
AsmClassGenerator acg = controller.getAcg();
ClassNode closureClass = getOrAddClosureClass(expression, 0);
String closureClassinternalName = BytecodeHelper.getClassInternalName(closureClass);
List constructors = closureClass.getDeclaredConstructors();
ConstructorNode node = (ConstructorNode) constructors.get(0);
Parameter[] localVariableParams = node.getParameters();
mv.visitTypeInsn(NEW, closureClassinternalName);
mv.visitInsn(DUP);
if (controller.isStaticMethod() || compileStack.isInSpecialConstructorCall()) {
(new ClassExpression(classNode)).visit(acg);
(new ClassExpression(controller.getOutermostClass())).visit(acg);
} else {
mv.visitVarInsn(ALOAD, 0);
controller.getOperandStack().push(ClassHelper.OBJECT_TYPE);
loadThis();
}
// now let's load the various parameters we're passing
// we start at index 2 because the first variable we pass
// is the owner instance and at this point it is already
// on the stack
for (int i = 2; i < localVariableParams.length; i++) {
Parameter param = localVariableParams[i];
String name = param.getName();
loadReference(name, controller);
if (param.getNodeMetaData(ClosureWriter.UseExistingReference.class)==null) {
param.setNodeMetaData(ClosureWriter.UseExistingReference.class,Boolean.TRUE);
}
}
// we may need to pass in some other constructors
//cv.visitMethodInsn(INVOKESPECIAL, innerClassinternalName, "<init>", prototype + ")V");
mv.visitMethodInsn(INVOKESPECIAL, closureClassinternalName, "<init>", BytecodeHelper.getMethodDescriptor(ClassHelper.VOID_TYPE, localVariableParams), false);
controller.getOperandStack().replace(ClassHelper.CLOSURE_TYPE, localVariableParams.length);
}
public static void loadReference(String name, WriterController controller) {
CompileStack compileStack = controller.getCompileStack();
MethodVisitor mv = controller.getMethodVisitor();
ClassNode classNode = controller.getClassNode();
AsmClassGenerator acg = controller.getAcg();
// compileStack.containsVariable(name) means to ask if the variable is already declared
// compileStack.getScope().isReferencedClassVariable(name) means to ask if the variable is a field
// If it is no field and is not yet declared, then it is either a closure shared variable or
// an already declared variable.
if (!compileStack.containsVariable(name) && compileStack.getScope().isReferencedClassVariable(name)) {
acg.visitFieldExpression(new FieldExpression(classNode.getDeclaredField(name)));
} else {
BytecodeVariable v = compileStack.getVariable(name, !classNodeUsesReferences(controller.getClassNode()));
if (v == null) {
// variable is not on stack because we are
// inside a nested Closure and this variable
// was not used before
// then load it from the Closure field
FieldNode field = classNode.getDeclaredField(name);
mv.visitVarInsn(ALOAD, 0);
mv.visitFieldInsn(GETFIELD, controller.getInternalClassName(), name, BytecodeHelper.getTypeDescription(field.getType()));
} else {
mv.visitVarInsn(ALOAD, v.getIndex());
}
controller.getOperandStack().push(ClassHelper.REFERENCE_TYPE);
}
}
public ClassNode getOrAddClosureClass(ClosureExpression expression, int mods) {
ClassNode closureClass = closureClassMap.get(expression);
if (closureClass == null) {
closureClass = createClosureClass(expression, mods);
closureClassMap.put(expression, closureClass);
controller.getAcg().addInnerClass(closureClass);
closureClass.addInterface(ClassHelper.GENERATED_CLOSURE_Type);
closureClass.putNodeMetaData(WriterControllerFactory.class, factory);
}
return closureClass;
}
private static boolean classNodeUsesReferences(ClassNode classNode) {
boolean ret = classNode.getSuperClass() == ClassHelper.CLOSURE_TYPE;
if (ret) return ret;
if (classNode instanceof InnerClassNode) {
InnerClassNode inner = (InnerClassNode) classNode;
return inner.isAnonymous();
}
return false;
}
protected ClassNode createClosureClass(ClosureExpression expression, int mods) {
ClassNode classNode = controller.getClassNode();
ClassNode outerClass = controller.getOutermostClass();
MethodNode methodNode = controller.getMethodNode();
String name = classNode.getName() + "$"
+ controller.getContext().getNextClosureInnerName(outerClass, classNode, methodNode); // add a more informative name
boolean staticMethodOrInStaticClass = controller.isStaticMethod() || classNode.isStaticClass();
Parameter[] parameters = expression.getParameters();
if (parameters == null) {
parameters = Parameter.EMPTY_ARRAY;
} else if (parameters.length == 0) {
// let's create a default 'it' parameter
Parameter it = new Parameter(ClassHelper.OBJECT_TYPE, "it", ConstantExpression.NULL);
parameters = new Parameter[]{it};
Variable ref = expression.getVariableScope().getDeclaredVariable("it");
if (ref!=null) it.setClosureSharedVariable(ref.isClosureSharedVariable());
}
Parameter[] localVariableParams = getClosureSharedVariables(expression);
removeInitialValues(localVariableParams);
InnerClassNode answer = new InnerClassNode(classNode, name, mods, ClassHelper.CLOSURE_TYPE.getPlainNodeReference());
answer.setEnclosingMethod(controller.getMethodNode());
answer.setSynthetic(true);
answer.setUsingGenerics(outerClass.isUsingGenerics());
answer.setSourcePosition(expression);
if (staticMethodOrInStaticClass) {
answer.setStaticClass(true);
}
if (controller.isInScriptBody()) {
answer.setScriptBody(true);
}
MethodNode method =
answer.addMethod("doCall", ACC_PUBLIC, ClassHelper.OBJECT_TYPE, parameters, ClassNode.EMPTY_ARRAY, expression.getCode());
method.setSourcePosition(expression);
VariableScope varScope = expression.getVariableScope();
if (varScope == null) {
throw new RuntimeException(
"Must have a VariableScope by now! for expression: " + expression + " class: " + name);
} else {
method.setVariableScope(varScope.copy());
}
if (parameters.length > 1
|| (parameters.length == 1
&& parameters[0].getType() != null
&& parameters[0].getType() != ClassHelper.OBJECT_TYPE
&& !ClassHelper.OBJECT_TYPE.equals(parameters[0].getType().getComponentType())))
{
// let's add a typesafe call method
MethodNode call = answer.addMethod(
"call",
ACC_PUBLIC,
ClassHelper.OBJECT_TYPE,
parameters,
ClassNode.EMPTY_ARRAY,
new ReturnStatement(
new MethodCallExpression(
VariableExpression.THIS_EXPRESSION,
"doCall",
new ArgumentListExpression(parameters))));
call.setSourcePosition(expression);
}
// let's make the constructor
BlockStatement block = new BlockStatement();
// this block does not get a source position, because we don't
// want this synthetic constructor to show up in corbertura reports
VariableExpression outer = new VariableExpression("_outerInstance");
outer.setSourcePosition(expression);
block.getVariableScope().putReferencedLocalVariable(outer);
VariableExpression thisObject = new VariableExpression("_thisObject");
thisObject.setSourcePosition(expression);
block.getVariableScope().putReferencedLocalVariable(thisObject);
TupleExpression conArgs = new TupleExpression(outer, thisObject);
block.addStatement(
new ExpressionStatement(
new ConstructorCallExpression(
ClassNode.SUPER,
conArgs)));
// let's assign all the parameter fields from the outer context
for (Parameter param : localVariableParams) {
String paramName = param.getName();
ClassNode type = param.getType();
if (true) {
VariableExpression initialValue = new VariableExpression(paramName);
initialValue.setAccessedVariable(param);
initialValue.setUseReferenceDirectly(true);
ClassNode realType = type;
type = ClassHelper.makeReference();
param.setType(ClassHelper.makeReference());
FieldNode paramField = answer.addField(paramName, ACC_PRIVATE | ACC_SYNTHETIC, type, initialValue);
paramField.setOriginType(ClassHelper.getWrapper(param.getOriginType()));
paramField.setHolder(true);
String methodName = Verifier.capitalize(paramName);
// let's add a getter & setter
Expression fieldExp = new FieldExpression(paramField);
answer.addMethod(
"get" + methodName,
ACC_PUBLIC,
realType.getPlainNodeReference(),
Parameter.EMPTY_ARRAY,
ClassNode.EMPTY_ARRAY,
new ReturnStatement(fieldExp));
}
}
Parameter[] params = new Parameter[2 + localVariableParams.length];
params[0] = new Parameter(ClassHelper.OBJECT_TYPE, "_outerInstance");
params[1] = new Parameter(ClassHelper.OBJECT_TYPE, "_thisObject");
System.arraycopy(localVariableParams, 0, params, 2, localVariableParams.length);
ASTNode sn = answer.addConstructor(ACC_PUBLIC, params, ClassNode.EMPTY_ARRAY, block);
sn.setSourcePosition(expression);
correctAccessedVariable(answer,expression);
return answer;
}
private void correctAccessedVariable(final InnerClassNode closureClass, ClosureExpression ce) {
CodeVisitorSupport visitor = new CodeVisitorSupport() {
@Override
public void visitVariableExpression(VariableExpression expression) {
Variable v = expression.getAccessedVariable();
if (v==null) return;
if (!(v instanceof FieldNode)) return;
String name = expression.getName();
FieldNode fn = closureClass.getDeclaredField(name);
if (fn != null) { // only overwrite if we find something more specific
expression.setAccessedVariable(fn);
}
}
};
visitor.visitClosureExpression(ce);
}
/*
* this method is called for local variables shared between scopes.
* These variables must not have init values because these would
* then in later steps be used to create multiple versions of the
* same method, in this case the constructor. A closure should not
* have more than one constructor!
*/
private void removeInitialValues(Parameter[] params) {
for (int i = 0; i < params.length; i++) {
if (params[i].hasInitialExpression()) {
Parameter p = new Parameter(params[i].getType(), params[i].getName());
p.setOriginType(p.getOriginType());
params[i] = p;
}
}
}
public boolean addGeneratedClosureConstructorCall(ConstructorCallExpression call) {
ClassNode classNode = controller.getClassNode();
if (!classNode.declaresInterface(ClassHelper.GENERATED_CLOSURE_Type)) return false;
AsmClassGenerator acg = controller.getAcg();
OperandStack operandStack = controller.getOperandStack();
MethodVisitor mv = controller.getMethodVisitor();
mv.visitVarInsn(ALOAD, 0);
ClassNode callNode = classNode.getSuperClass();
TupleExpression arguments = (TupleExpression) call.getArguments();
if (arguments.getExpressions().size()!=2) throw new GroovyBugError("expected 2 arguments for closure constructor super call, but got"+arguments.getExpressions().size());
arguments.getExpression(0).visit(acg);
operandStack.box();
arguments.getExpression(1).visit(acg);
operandStack.box();
//TODO: replace with normal String, p not needed
Parameter p = new Parameter(ClassHelper.OBJECT_TYPE,"_p");
String descriptor = BytecodeHelper.getMethodDescriptor(ClassHelper.VOID_TYPE, new Parameter[]{p,p});
mv.visitMethodInsn(INVOKESPECIAL, BytecodeHelper.getClassInternalName(callNode), "<init>", descriptor, false);
operandStack.remove(2);
return true;
}
protected Parameter[] getClosureSharedVariables(ClosureExpression ce) {
VariableScope scope = ce.getVariableScope();
Parameter[] ret = new Parameter[scope.getReferencedLocalVariablesCount()];
int index = 0;
for (Iterator iter = scope.getReferencedLocalVariablesIterator(); iter.hasNext();) {
Variable element = (org.codehaus.groovy.ast.Variable) iter.next();
Parameter p = new Parameter(element.getType(), element.getName());
p.setOriginType(element.getOriginType());
p.setClosureSharedVariable(element.isClosureSharedVariable());
ret[index] = p;
index++;
}
return ret;
}
private void loadThis() {
MethodVisitor mv = controller.getMethodVisitor();
mv.visitVarInsn(ALOAD, 0);
if (controller.isInClosure()) {
mv.visitMethodInsn(INVOKEVIRTUAL, "groovy/lang/Closure", "getThisObject", "()Ljava/lang/Object;", false);
controller.getOperandStack().push(ClassHelper.OBJECT_TYPE);
} else {
controller.getOperandStack().push(controller.getClassNode());
}
}
}