Skip to content

Commit

Permalink
DROOLS-1701 Fix closure context handling, misc fn issues (apache#8)
Browse files Browse the repository at this point in the history
  • Loading branch information
evacchi authored and tarilabs committed Jun 26, 2018
1 parent 29c9378 commit 3900241
Show file tree
Hide file tree
Showing 9 changed files with 231 additions and 24 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@

import java.util.Arrays;
import java.util.List;
import java.util.Objects;
import java.util.function.Function;
import java.util.stream.Collectors;

Expand All @@ -33,18 +34,32 @@ public class CompiledCustomFEELFunction extends BaseFEELFunction {

private final List<String> parameters;
private final Function<EvaluationContext, Object> body;
private final EvaluationContext ctx;

public CompiledCustomFEELFunction(String name, List<String> parameters, Function<EvaluationContext, Object> body) {
this(name, parameters, body, null);
}

public CompiledCustomFEELFunction(String name, List<String> parameters, Function<EvaluationContext, Object> body, EvaluationContext ctx) {
super( name );
this.parameters = parameters;
this.body = body;
this.ctx = ctx;
}

@Override
public List<List<String>> getParameterNames() {
return Arrays.asList( parameters );
}

public boolean isProperClosure() {
return ctx != null;
}

public EvaluationContext getEvaluationContext() {
return ctx;
}

@Override
public FEELFnResult<Object> invoke(EvaluationContext ctx, Object[] params ) {
if( params.length != parameters.size() ) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,42 @@ private static Comparable asComparable(Object s) {
}
}

public static <T> T coerceTo(Class<?> paramType, Object value) {
Object actual;
if( paramType.isAssignableFrom( value.getClass() ) ) {
actual = value;
} else {
// try to coerce
if( value instanceof Number ) {
if( paramType == byte.class || paramType == Byte.class ) {
actual = ((Number)value).byteValue();
} else if( paramType == short.class || paramType == Short.class ) {
actual = ((Number) value).shortValue();
} else if( paramType == int.class || paramType == Integer.class ) {
actual = ((Number) value).intValue();
} else if( paramType == long.class || paramType == Long.class ) {
actual = ((Number) value).longValue();
} else if( paramType == float.class || paramType == Float.class ) {
actual = ((Number) value).floatValue();
} else if( paramType == double.class || paramType == Double.class ) {
actual = ((Number) value).doubleValue();
} else {
throw new IllegalArgumentException( "Unable to coerce parameter "+value+". Expected "+paramType+" but found "+value.getClass() );
}
} else if ( value instanceof String
&& ((String) value).length() == 1
&& (paramType == char.class || paramType == Character.class) ) {
actual = ((String) value).charAt(0);
} else if ( value instanceof Boolean && paramType == boolean.class ) {
// Because Boolean can be also null, boolean.class is not assignable from Boolean.class. So we must coerce this.
actual = value;
} else {
throw new IllegalArgumentException( "Unable to coerce parameter "+value+". Expected "+paramType+" but found "+value.getClass() );
}
}
return (T) actual;
}

/**
* Represent a [e1, e2, e3] construct.
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -376,19 +376,33 @@ public static Object invoke(EvaluationContext feelExprCtx, Object function, Obje
return null;
}
if (function instanceof FEELFunction) {
Object[] invocationParams = null;
if (params instanceof List) {
invocationParams = ((List) params).toArray(new Object[]{});
} else if (params instanceof Object[]) {
invocationParams = (Object[]) params;
} else {
invocationParams = new Object[]{params};
Object[] invocationParams = toFunctionParams(params);

FEELFunction f = (FEELFunction) function;

if (function instanceof CompiledCustomFEELFunction) {
CompiledCustomFEELFunction ff = (CompiledCustomFEELFunction) function;
if (ff.isProperClosure()) {
return ff.invokeReflectively(ff.getEvaluationContext(), invocationParams);
}
}
return ((FEELFunction) function).invokeReflectively(feelExprCtx, invocationParams);

return f.invokeReflectively(feelExprCtx, invocationParams);
} else if (function instanceof UnaryTest) {
throw new UnsupportedOperationException("TODO"); // TODO
}
return null;
}

private static Object[] toFunctionParams(Object params) {
Object[] invocationParams = null;
if (params instanceof List) {
invocationParams = ((List) params).toArray(new Object[]{});
} else if (params instanceof Object[]) {
invocationParams = (Object[]) params;
} else {
invocationParams = new Object[]{params};
}
return invocationParams;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Deque;
import java.util.EnumSet;
import java.util.HashMap;
Expand Down Expand Up @@ -59,15 +60,23 @@
import org.drools.javaparser.ast.type.ClassOrInterfaceType;
import org.drools.javaparser.ast.type.UnknownType;
import org.kie.dmn.feel.lang.CompositeType;
import org.kie.dmn.feel.lang.FunctionDefs;
import org.kie.dmn.feel.lang.Type;
import org.kie.dmn.feel.lang.ast.ASTBuilderFactory;
import org.kie.dmn.feel.lang.ast.BaseNode;
import org.kie.dmn.feel.lang.ast.FunctionDefNode;
import org.kie.dmn.feel.lang.ast.InfixOpNode.InfixOperator;
import org.kie.dmn.feel.lang.ast.ListNode;
import org.kie.dmn.feel.lang.ast.RangeNode;
import org.kie.dmn.feel.lang.ast.RangeNode.IntervalBoundary;
import org.kie.dmn.feel.lang.ast.UnaryTestNode.UnaryOperator;
import org.kie.dmn.feel.lang.impl.EvaluationContextImpl;
import org.kie.dmn.feel.lang.impl.FEELEventListenersManager;
import org.kie.dmn.feel.lang.impl.JavaBackedType;
import org.kie.dmn.feel.lang.impl.MapBackedType;
import org.kie.dmn.feel.lang.impl.NamedParameter;
import org.kie.dmn.feel.lang.types.BuiltInType;
import org.kie.dmn.feel.parser.feel11.ASTBuilderVisitor;
import org.kie.dmn.feel.parser.feel11.FEEL_1_1BaseVisitor;
import org.kie.dmn.feel.parser.feel11.FEEL_1_1Parser;
import org.kie.dmn.feel.parser.feel11.FEEL_1_1Parser.ContextEntryContext;
Expand Down Expand Up @@ -99,6 +108,8 @@ public class DirectCompilerVisitor extends FEEL_1_1BaseVisitor<DirectCompilerRes
JavaParser.parseType(Comparable.class.getCanonicalName());
private static final org.drools.javaparser.ast.type.Type TYPE_LIST =
JavaParser.parseType(List.class.getCanonicalName());
public static final ClassOrInterfaceType TYPE_CUSTOM_FEEL_FUNCTION =
JavaParser.parseClassOrInterfaceType(CompiledCustomFEELFunction.class.getSimpleName());

// TODO as this is now compiled it might not be needed for this compilation strategy, just need the layer 0 of input Types, but to be checked.
private ScopeHelper scopeHelper;
Expand Down Expand Up @@ -883,20 +894,65 @@ public DirectCompilerResult visitFormalParameters(FEEL_1_1Parser.FormalParameter

@Override
public DirectCompilerResult visitFunctionDefinition(FEEL_1_1Parser.FunctionDefinitionContext ctx) {
DirectCompilerResult parameters = null;
if (ctx.formalParameters() != null) {
parameters = visit(ctx.formalParameters());
}

boolean external = ctx.external != null;
if (external) {
throw new UnsupportedOperationException("TODO"); // TODO
}
DirectCompilerResult body = visit(ctx.body);
return external ?
declareExternalFunction(ctx) :
declareInternalFunction(ctx.body, ctx.formalParameters());
}

private DirectCompilerResult declareExternalFunction(FEEL_1_1Parser.FunctionDefinitionContext ctx) {
ASTBuilderVisitor evaluatingVisitor = new ASTBuilderVisitor(Collections.emptyMap());
EvaluationContextImpl emptyEvalCtx =
new EvaluationContextImpl(this.getClass().getClassLoader(), new FEELEventListenersManager());

List<String> params = null;
if ( ctx.formalParameters() != null ) {
ListNode listNode = (ListNode)
evaluatingVisitor.visit(ctx.formalParameters());
params = listNode.getElements().stream().map(n -> n.getText()).collect(Collectors.toList());
}

BaseNode evaluatedBody = evaluatingVisitor.visit( ctx.body );
Map<String, Object> conf = (Map<String, Object>) evaluatedBody.evaluate(emptyEvalCtx);
Map<String, String> java = (Map<String, String>) conf.get( "java" );

if (java != null) {

String className = java.get("class");
String methodSignature = java.get("method signature");
if (className == null || methodSignature == null) {
throw new IllegalArgumentException("Invalid class name or method signature: " + className + "::" + methodSignature);
}
Expression methodCallExpr = FunctionDefs.asMethodCall(className, methodSignature, params);
DirectCompilerResult parameters = visit(ctx.formalParameters());

ObjectCreationExpr functionDefExpr = new ObjectCreationExpr();
functionDefExpr.setType(TYPE_CUSTOM_FEEL_FUNCTION);
functionDefExpr.addArgument(ANONYMOUS_STRING_LITERAL);
functionDefExpr.addArgument((parameters != null) ? parameters.getExpression() : EMPTY_LIST);
functionDefExpr.addArgument(anonFunctionEvaluationContext2Object(methodCallExpr));
functionDefExpr.addArgument(new MethodCallExpr(new NameExpr("feelExprCtx"), "current"));
DirectCompilerResult result = DirectCompilerResult.of(functionDefExpr, BuiltInType.FUNCTION);
return result;

} else {
throw new IllegalArgumentException("Invalid external declaration");
}

}

private DirectCompilerResult declareInternalFunction(ExpressionContext bodyCtx, FEEL_1_1Parser.FormalParametersContext parametersCtx) {
DirectCompilerResult body = visit(bodyCtx);
DirectCompilerResult parameters =
parametersCtx == null? null : visit(parametersCtx);
ObjectCreationExpr functionDefExpr = new ObjectCreationExpr();
functionDefExpr.setType(JavaParser.parseClassOrInterfaceType(CompiledCustomFEELFunction.class.getSimpleName()));
functionDefExpr.setType(TYPE_CUSTOM_FEEL_FUNCTION);
functionDefExpr.addArgument(ANONYMOUS_STRING_LITERAL);
functionDefExpr.addArgument((parameters != null) ? parameters.getExpression() : EMPTY_LIST);
functionDefExpr.addArgument(anonFunctionEvaluationContext2Object(body.getExpression()));
functionDefExpr.addArgument(new MethodCallExpr(new NameExpr("feelExprCtx"), "current"));
DirectCompilerResult result = DirectCompilerResult.of(functionDefExpr, BuiltInType.FUNCTION).withFD(body);
if (parameters != null) {
result.withFD(parameters);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@ public interface EvaluationContext {

void exitFrame();

EvaluationContext current();

void setValue(String name, Object value );

Object getValue(String name );
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
package org.kie.dmn.feel.lang;

import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import org.drools.javaparser.JavaParser;
import org.drools.javaparser.ast.NodeList;
import org.drools.javaparser.ast.expr.CastExpr;
import org.drools.javaparser.ast.expr.ClassExpr;
import org.drools.javaparser.ast.expr.Expression;
import org.drools.javaparser.ast.expr.MethodCallExpr;
import org.drools.javaparser.ast.expr.NameExpr;
import org.drools.javaparser.ast.expr.NullLiteralExpr;
import org.drools.javaparser.ast.expr.StringLiteralExpr;
import org.drools.javaparser.ast.type.Type;
import org.kie.dmn.feel.lang.ast.FunctionDefNode;

public class FunctionDefs {

public static Expression asMethodCall(
String className,
String methodSignature,
List<String> params) {
// creating a simple algorithm to find the method in java
// without using any external libraries in this initial implementation
// might need to explicitly use a classloader here
String[] mp = FunctionDefNode.parseMethod(methodSignature);
try {
String methodName = mp[0];
String[] paramTypeNames = FunctionDefNode.parseParams(mp[1]);
ArrayList<Expression> paramExprs = new ArrayList<>();
if (paramTypeNames.length == params.size()) {
for (int i = 0; i < params.size(); i++) {
String paramName = params.get(i);
String paramTypeName = paramTypeNames[i];
Type paramTypeCanonicalName =
JavaParser.parseType(
FunctionDefNode.getType(paramTypeName).getCanonicalName());

Expression param =
new CastExpr(paramTypeCanonicalName,
new MethodCallExpr(
null,
"coerceTo",
new NodeList<>(
new ClassExpr(paramTypeCanonicalName),
new MethodCallExpr(
new NameExpr("feelExprCtx"),
"getValue",
new NodeList<>(new StringLiteralExpr(paramName))
))));

paramExprs.add(param);
}

return new MethodCallExpr(
new NameExpr(className),
methodName,
new NodeList<>(paramExprs));
} else {
return new NullLiteralExpr();
}
} catch (Exception e) {
throw new IllegalArgumentException(e);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -37,8 +37,8 @@ public class FunctionDefNode
extends BaseNode {

private static final String ANONYMOUS = "<anonymous>";
private final Pattern METHOD_PARSER = Pattern.compile( "(.+)\\((.*)\\)" );
private final Pattern PARAMETER_PARSER = Pattern.compile( "([^, ]+)" );
private static final Pattern METHOD_PARSER = Pattern.compile( "(.+)\\((.*)\\)" );
private static final Pattern PARAMETER_PARSER = Pattern.compile( "([^, ]+)" );


private List<NameDefNode> formalParameters;
Expand Down Expand Up @@ -128,7 +128,7 @@ public Object evaluate(EvaluationContext ctx) {
}
}

private Class<?> getType(String typeName)
public static Class<?> getType(String typeName)
throws ClassNotFoundException {
// first check if it is primitive
Class<?> type = convertPrimitiveNameToType( typeName );
Expand All @@ -140,7 +140,7 @@ private Class<?> getType(String typeName)
return type;
}

public String[] parseMethod(String signature ) {
public static String[] parseMethod(String signature ) {
Matcher m = METHOD_PARSER.matcher( signature );
if( m.matches() ) {
String[] result = new String[2];
Expand All @@ -152,7 +152,7 @@ public String[] parseMethod(String signature ) {
}


public String[] parseParams(String params) {
public static String[] parseParams(String params) {
List<String> ps = new ArrayList<>( );
if( params.trim().length() > 0 ) {
Matcher m = PARAMETER_PARSER.matcher( params.trim() );
Expand All @@ -163,7 +163,7 @@ public String[] parseParams(String params) {
return ps.toArray( new String[ps.size()] );
}

public static Class<?> convertPrimitiveNameToType( String typeName ) {
private static Class<?> convertPrimitiveNameToType( String typeName ) {
if (typeName.equals( "int" )) {
return int.class;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,10 +38,14 @@ public class EvaluationContextImpl implements EvaluationContext {
private boolean performRuntimeTypeCheck = false;
private ClassLoader rootClassLoader;

public EvaluationContextImpl(ClassLoader cl, FEELEventListenersManager eventsManager) {
private EvaluationContextImpl(ClassLoader cl, FEELEventListenersManager eventsManager, Deque<ExecutionFrame> stack) {
this.eventsManager = eventsManager;
this.rootClassLoader = cl;
this.stack = new ArrayDeque<>();
this.stack = new ArrayDeque<>(stack);
}

public EvaluationContextImpl(ClassLoader cl, FEELEventListenersManager eventsManager) {
this(cl, eventsManager, new ArrayDeque<>());
// we create a rootFrame to hold all the built in functions
push( RootExecutionFrame.INSTANCE );
// and then create a global frame to be the starting frame
Expand All @@ -55,6 +59,11 @@ public EvaluationContextImpl(FEELEventListenersManager eventsManager, DMNRuntime
this.dmnRuntime = dmnRuntime;
}

@Override
public EvaluationContext current() {
return new EvaluationContextImpl(rootClassLoader, eventsManager, new ArrayDeque<>(stack));
}

public void push(ExecutionFrame obj) {
stack.push( obj );
}
Expand Down
Loading

0 comments on commit 3900241

Please sign in to comment.