All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.apache.lucene.expressions.js.JavascriptCompiler Maven / Gradle / Ivy

/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You 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.apache.lucene.expressions.js;

import java.io.IOException;
import java.io.Reader;
import java.lang.constant.ConstantDescs;
import java.lang.invoke.MethodHandle;
import java.lang.invoke.MethodHandleInfo;
import java.lang.invoke.MethodHandles;
import java.lang.invoke.MethodHandles.Lookup;
import java.lang.invoke.MethodType;
import java.nio.charset.StandardCharsets;
import java.text.ParseException;
import java.util.ArrayDeque;
import java.util.Arrays;
import java.util.Collections;
import java.util.Deque;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Objects;
import java.util.Properties;
import java.util.function.Function;
import java.util.function.Supplier;
import java.util.stream.Stream;
import org.antlr.v4.runtime.ANTLRInputStream;
import org.antlr.v4.runtime.BaseErrorListener;
import org.antlr.v4.runtime.CommonTokenStream;
import org.antlr.v4.runtime.DiagnosticErrorListener;
import org.antlr.v4.runtime.RecognitionException;
import org.antlr.v4.runtime.Recognizer;
import org.antlr.v4.runtime.atn.PredictionMode;
import org.antlr.v4.runtime.tree.ParseTree;
import org.apache.lucene.expressions.Expression;
import org.apache.lucene.expressions.js.JavascriptParser.ExpressionContext;
import org.apache.lucene.search.DoubleValues;
import org.apache.lucene.util.IOUtils;
import org.objectweb.asm.ClassWriter;
import org.objectweb.asm.ConstantDynamic;
import org.objectweb.asm.Handle;
import org.objectweb.asm.Label;
import org.objectweb.asm.Opcodes;
import org.objectweb.asm.Type;
import org.objectweb.asm.commons.GeneratorAdapter;
import org.objectweb.asm.commons.Method;

/**
 * An expression compiler for javascript expressions.
 *
 * 

Example: * *

 *   Expression foo = JavascriptCompiler.compile("((0.3*popularity)/10.0)+(0.7*score)");
 * 
* *

See the {@link org.apache.lucene.expressions.js package documentation} for the supported * syntax and default functions. * *

You can compile with an alternate set of functions via {@link #compile(String, Map)}. For * example: * *

 *   Map<String,MethodHandle> functions = new HashMap<>();
 *   // add all the default functions
 *   functions.putAll(JavascriptCompiler.DEFAULT_FUNCTIONS);
 *   // add cbrt()
 *   functions.put("cbrt", MethodHandles.publicLookup().findStatic(Math.class, "cbrt",
 *                 MethodType.methodType(double.class, double.class)));
 *   // call compile with customized function map
 *   Expression foo = JavascriptCompiler.compile("cbrt(score)+ln(popularity)",
 *                                               functions);
 * 
* *

It is possible to pass any {@link MethodHandle} as function that only takes {@code double} * parameters and returns a {@code double}. The method does not need to be public, it just needs to * be resolved correctly using a private {@link Lookup} instance. Ideally the methods should be * {@code static}, but you can use {@link MethodHandle#bindTo(Object)} to bind it to a receiver. * * @lucene.experimental */ public final class JavascriptCompiler { private static final Lookup LOOKUP = MethodHandles.lookup(); private static final int CLASSFILE_VERSION = Opcodes.V21; private static final MethodType MT_EXPRESSION_CTOR_LOOKUP = MethodType.methodType(void.class, String.class, String[].class); // We use the same class name for all generated classes (they are hidden anyways). // The source code is displayed as "source file name" in stack trace. private static final String COMPILED_EXPRESSION_CLASS = JavascriptCompiler.class.getName() + "$CompiledExpression"; private static final String COMPILED_EXPRESSION_INTERNAL = COMPILED_EXPRESSION_CLASS.replace('.', '/'); private static final Type EXPRESSION_TYPE = Type.getType(Expression.class), FUNCTION_VALUES_TYPE = Type.getType(DoubleValues.class), METHOD_HANDLE_TYPE = Type.getType(MethodHandle.class), JAVASCRIPT_COMPILER_TYPE = Type.getType(JavascriptCompiler.class), THROWABLE_TYPE = Type.getType(Throwable.class); private static final Method EXPRESSION_CTOR = getAsmMethod(void.class, "", String.class, String[].class), EVALUATE_METHOD = getAsmMethod(double.class, "evaluate", DoubleValues[].class), DOUBLE_VAL_METHOD = getAsmMethod(double.class, "doubleValue"), PATCH_STACK_METHOD = getAsmMethod(Throwable.class, "patchStackTrace", Throwable.class, Expression.class); private static final Type[] EVALUATE_EXCEPTIONS = new Type[] {Type.getType(IOException.class)}; private static final Handle DYNAMIC_CONSTANT_BOOTSTRAP_HANDLE = new Handle( Opcodes.H_INVOKESTATIC, JAVASCRIPT_COMPILER_TYPE.getInternalName(), "dynamicConstantBootstrap", MethodType.methodType( MethodHandle.class, Lookup.class, String.class, Class.class, String.class) .toMethodDescriptorString(), false); /** create an ASM Method object from return type, method name, and parameters. */ private static Method getAsmMethod(Class rtype, String name, Class... ptypes) { return new Method(name, MethodType.methodType(rtype, ptypes).toMethodDescriptorString()); } final String sourceText; final Map functions; final boolean picky; /** * Compiles the given expression using default compiler settings. * * @param sourceText The expression to compile * @return A new compiled expression * @throws ParseException on failure to compile */ public static Expression compile(String sourceText) throws ParseException { return compile(sourceText, DEFAULT_FUNCTIONS); } /** * Compiles the given expression with the supplied custom functions using default compiler * settings. * *

Functions must be {@code public static}, return {@code double} and can take from zero to 256 * {@code double} parameters. * * @param sourceText The expression to compile * @param functions map of String names to {@link MethodHandle}s * @return A new compiled expression * @throws ParseException on failure to compile */ public static Expression compile(String sourceText, Map functions) throws ParseException { return compile(sourceText, functions, false); } /** * Converts a legacy map with reflective {@link java.lang.reflect.Method} functions to {@code * Map convertLegacyFunctions( Map functions) throws IllegalAccessException { final var lookup = MethodHandles.publicLookup(); final Map newMap = new HashMap<>(); for (var e : functions.entrySet()) { newMap.put(e.getKey(), lookup.unreflect(e.getValue())); } return newMap; } /** * Compiles the given expression with the supplied custom functions. * *

Functions must be {@code public static}, return {@code double} and can take from zero to 256 * {@code double} parameters. * * @param sourceText The expression to compile * @param functions map of String names to {@link MethodHandle}s * @param picky whether to throw exception on ambiguity or other internal parsing issues (this * option makes things slower too, it is only for debugging). * @return A new compiled expression * @throws ParseException on failure to compile */ static Expression compile(String sourceText, Map functions, boolean picky) throws ParseException { for (MethodHandle m : functions.values()) { checkFunction(m); } return new JavascriptCompiler(sourceText, functions, picky).compileExpression(); } /** * Constructs a compiler for expressions with specific set of functions * * @param sourceText The expression to compile */ private JavascriptCompiler( String sourceText, Map functions, boolean picky) { this.sourceText = Objects.requireNonNull(sourceText, "sourceText"); this.functions = Map.copyOf(functions); this.picky = picky; } /** * Compiles the given expression as hidden class. * * @return A new compiled expression * @throws ParseException on failure to compile */ private Expression compileExpression() throws ParseException { final Map externalsMap = new LinkedHashMap<>(); final ClassWriter classWriter = new ClassWriter(ClassWriter.COMPUTE_FRAMES | ClassWriter.COMPUTE_MAXS); try { generateClass(getAntlrParseTree(), classWriter, externalsMap); } catch (RuntimeException re) { if (re.getCause() instanceof ParseException) { throw (ParseException) re.getCause(); } throw re; } try { final Lookup lookup = LOOKUP.defineHiddenClassWithClassData(classWriter.toByteArray(), functions, true); return invokeConstructor(lookup, lookup.lookupClass(), externalsMap); } catch (ReflectiveOperationException exception) { throw new IllegalStateException( "An internal error occurred attempting to compile the expression (" + sourceText + ").", exception); } } private Expression invokeConstructor( Lookup lookup, Class expressionClass, Map externalsMap) throws ReflectiveOperationException { final MethodHandle ctor = lookup.findConstructor(expressionClass, MT_EXPRESSION_CTOR_LOOKUP); try { return (Expression) ctor.invoke(sourceText, externalsMap.keySet().toArray(String[]::new)); } catch (RuntimeException | Error e) { throw e; } catch (Throwable t) { throw new AssertionError(t); } } /** * Parses the sourceText into an ANTLR 4 parse tree * * @return The ANTLR parse tree * @throws ParseException on failure to parse */ private ParseTree getAntlrParseTree() throws ParseException { final ANTLRInputStream antlrInputStream = new ANTLRInputStream(sourceText); final JavascriptErrorHandlingLexer javascriptLexer = new JavascriptErrorHandlingLexer(antlrInputStream); javascriptLexer.removeErrorListeners(); final JavascriptParser javascriptParser = new JavascriptParser(new CommonTokenStream(javascriptLexer)); javascriptParser.removeErrorListeners(); if (picky) { setupPicky(javascriptParser); } javascriptParser.setErrorHandler(new JavascriptParserErrorStrategy()); return javascriptParser.compile(); } private void setupPicky(JavascriptParser parser) { // Diagnostic listener invokes syntaxError on other listeners for ambiguity issues parser.addErrorListener(new DiagnosticErrorListener(true)); // a second listener to fail the test when the above happens. parser.addErrorListener( new BaseErrorListener() { @Override public void syntaxError( final Recognizer recognizer, final Object offendingSymbol, final int line, final int charPositionInLine, final String msg, final RecognitionException e) { throw new RuntimeException( new ParseException( "line (" + line + "), offset (" + charPositionInLine + "), symbol (" + offendingSymbol + ") " + msg, charPositionInLine)); } }); // Enable exact ambiguity detection (costly). we enable exact since its the default for // DiagnosticErrorListener, life is too short to think about what 'inexact ambiguity' might // mean. parser.getInterpreter().setPredictionMode(PredictionMode.LL_EXACT_AMBIG_DETECTION); } /** Sends the bytecode of class file to {@link ClassWriter}. */ private void generateClass( final ParseTree parseTree, final ClassWriter classWriter, final Map externalsMap) throws ParseException { classWriter.visit( CLASSFILE_VERSION, Opcodes.ACC_PUBLIC | Opcodes.ACC_SUPER | Opcodes.ACC_FINAL, COMPILED_EXPRESSION_INTERNAL, null, EXPRESSION_TYPE.getInternalName(), null); final GeneratorAdapter constructor = new GeneratorAdapter(Opcodes.ACC_PUBLIC, EXPRESSION_CTOR, null, null, classWriter); constructor.loadThis(); constructor.loadArgs(); constructor.invokeConstructor(EXPRESSION_TYPE, EXPRESSION_CTOR); constructor.returnValue(); constructor.endMethod(); final GeneratorAdapter gen = new GeneratorAdapter( Opcodes.ACC_PUBLIC, EVALUATE_METHOD, null, EVALUATE_EXCEPTIONS, classWriter); // add a try/catch block to rewrite stack trace of any Throwable final Label beginTry = gen.newLabel(), endTry = gen.newLabel(), catchHandler = gen.newLabel(); gen.visitTryCatchBlock(beginTry, endTry, catchHandler, THROWABLE_TYPE.getInternalName()); gen.mark(beginTry); // to completely hide the ANTLR visitor we use an anonymous impl: new JavascriptBaseVisitor() { private final Deque typeStack = new ArrayDeque<>(); private final Map constantsMap = new HashMap<>(); @Override public Void visitCompile(JavascriptParser.CompileContext ctx) { typeStack.push(Type.DOUBLE_TYPE); visit(ctx.expression()); typeStack.pop(); return null; } @Override public Void visitPrecedence(JavascriptParser.PrecedenceContext ctx) { visit(ctx.expression()); return null; } @Override public Void visitNumeric(JavascriptParser.NumericContext ctx) { if (ctx.HEX() != null) { pushLong(Long.parseLong(ctx.HEX().getText().substring(2), 16)); } else if (ctx.OCTAL() != null) { pushLong(Long.parseLong(ctx.OCTAL().getText().substring(1), 8)); } else if (ctx.DECIMAL() != null) { gen.push(Double.parseDouble(ctx.DECIMAL().getText())); gen.cast(Type.DOUBLE_TYPE, typeStack.peek()); } else { throw new IllegalStateException("Unknown operation specified: " + ctx.getText()); } return null; } @Override public Void visitExternal(JavascriptParser.ExternalContext ctx) { String text = ctx.VARIABLE().getText(); int arguments = ctx.expression().size(); boolean parens = ctx.LP() != null && ctx.RP() != null; MethodHandle mh = parens ? functions.get(text) : null; try { if (mh != null) { final int arity = mh.type().parameterCount(); if (arguments != arity) { throw new ParseException( "Invalid expression '" + sourceText + "': Expected (" + arity + ") arguments for function call (" + text + "), but found (" + arguments + ").", ctx.start.getStartIndex()); } // place dynamic constant with MethodHandle on top of stack gen.visitLdcInsn( new ConstantDynamic( "func" + constantsMap.computeIfAbsent(text, k -> constantsMap.size()), METHOD_HANDLE_TYPE.getDescriptor(), DYNAMIC_CONSTANT_BOOTSTRAP_HANDLE, text)); // add arguments: typeStack.push(Type.DOUBLE_TYPE); for (int argument = 0; argument < arguments; ++argument) { visit(ctx.expression(argument)); } typeStack.pop(); // invoke MethodHandle of function: gen.visitMethodInsn( Opcodes.INVOKEVIRTUAL, METHOD_HANDLE_TYPE.getInternalName(), "invokeExact", mh.type().descriptorString(), false); gen.cast(Type.DOUBLE_TYPE, typeStack.peek()); } else if (!parens || arguments == 0 && text.contains(".")) { int index; text = normalizeQuotes(ctx.getText()); index = externalsMap.computeIfAbsent(text, k -> externalsMap.size()); gen.loadArg(0); gen.push(index); gen.arrayLoad(FUNCTION_VALUES_TYPE); gen.invokeVirtual(FUNCTION_VALUES_TYPE, DOUBLE_VAL_METHOD); gen.cast(Type.DOUBLE_TYPE, typeStack.peek()); } else { throw new ParseException( "Invalid expression '" + sourceText + "': Unrecognized function call (" + text + ").", ctx.start.getStartIndex()); } return null; } catch (ParseException e) { // The API doesn't allow checked exceptions here, so propagate up the stack. This is // unwrapped // in getAntlrParseTree. throw new RuntimeException(e); } } @Override public Void visitUnary(JavascriptParser.UnaryContext ctx) { if (ctx.BOOLNOT() != null) { Label labelNotTrue = new Label(); Label labelNotReturn = new Label(); typeStack.push(Type.INT_TYPE); visit(ctx.expression()); typeStack.pop(); gen.visitJumpInsn(Opcodes.IFEQ, labelNotTrue); pushBoolean(false); gen.goTo(labelNotReturn); gen.visitLabel(labelNotTrue); pushBoolean(true); gen.visitLabel(labelNotReturn); } else if (ctx.BWNOT() != null) { typeStack.push(Type.LONG_TYPE); visit(ctx.expression()); typeStack.pop(); gen.push(-1L); gen.visitInsn(Opcodes.LXOR); gen.cast(Type.LONG_TYPE, typeStack.peek()); } else if (ctx.ADD() != null) { visit(ctx.expression()); } else if (ctx.SUB() != null) { typeStack.push(Type.DOUBLE_TYPE); visit(ctx.expression()); typeStack.pop(); gen.visitInsn(Opcodes.DNEG); gen.cast(Type.DOUBLE_TYPE, typeStack.peek()); } else { throw new IllegalStateException("Unknown operation specified: " + ctx.getText()); } return null; } @Override public Void visitMuldiv(JavascriptParser.MuldivContext ctx) { int opcode; if (ctx.MUL() != null) { opcode = Opcodes.DMUL; } else if (ctx.DIV() != null) { opcode = Opcodes.DDIV; } else if (ctx.REM() != null) { opcode = Opcodes.DREM; } else { throw new IllegalStateException("Unknown operation specified: " + ctx.getText()); } pushArith(opcode, ctx.expression(0), ctx.expression(1)); return null; } @Override public Void visitAddsub(JavascriptParser.AddsubContext ctx) { int opcode; if (ctx.ADD() != null) { opcode = Opcodes.DADD; } else if (ctx.SUB() != null) { opcode = Opcodes.DSUB; } else { throw new IllegalStateException("Unknown operation specified: " + ctx.getText()); } pushArith(opcode, ctx.expression(0), ctx.expression(1)); return null; } @Override public Void visitBwshift(JavascriptParser.BwshiftContext ctx) { int opcode; if (ctx.LSH() != null) { opcode = Opcodes.LSHL; } else if (ctx.RSH() != null) { opcode = Opcodes.LSHR; } else if (ctx.USH() != null) { opcode = Opcodes.LUSHR; } else { throw new IllegalStateException("Unknown operation specified: " + ctx.getText()); } pushShift(opcode, ctx.expression(0), ctx.expression(1)); return null; } @Override public Void visitBoolcomp(JavascriptParser.BoolcompContext ctx) { int opcode; if (ctx.LT() != null) { opcode = GeneratorAdapter.LT; } else if (ctx.LTE() != null) { opcode = GeneratorAdapter.LE; } else if (ctx.GT() != null) { opcode = GeneratorAdapter.GT; } else if (ctx.GTE() != null) { opcode = GeneratorAdapter.GE; } else { throw new IllegalStateException("Unknown operation specified: " + ctx.getText()); } pushCond(opcode, ctx.expression(0), ctx.expression(1)); return null; } @Override public Void visitBooleqne(JavascriptParser.BooleqneContext ctx) { int opcode; if (ctx.EQ() != null) { opcode = GeneratorAdapter.EQ; } else if (ctx.NE() != null) { opcode = GeneratorAdapter.NE; } else { throw new IllegalStateException("Unknown operation specified: " + ctx.getText()); } pushCond(opcode, ctx.expression(0), ctx.expression(1)); return null; } @Override public Void visitBwand(JavascriptParser.BwandContext ctx) { pushBitwise(Opcodes.LAND, ctx.expression(0), ctx.expression(1)); return null; } @Override public Void visitBwxor(JavascriptParser.BwxorContext ctx) { pushBitwise(Opcodes.LXOR, ctx.expression(0), ctx.expression(1)); return null; } @Override public Void visitBwor(JavascriptParser.BworContext ctx) { pushBitwise(Opcodes.LOR, ctx.expression(0), ctx.expression(1)); return null; } @Override public Void visitBooland(JavascriptParser.BoolandContext ctx) { Label andFalse = new Label(); Label andEnd = new Label(); typeStack.push(Type.INT_TYPE); visit(ctx.expression(0)); gen.visitJumpInsn(Opcodes.IFEQ, andFalse); visit(ctx.expression(1)); gen.visitJumpInsn(Opcodes.IFEQ, andFalse); typeStack.pop(); pushBoolean(true); gen.goTo(andEnd); gen.visitLabel(andFalse); pushBoolean(false); gen.visitLabel(andEnd); return null; } @Override public Void visitBoolor(JavascriptParser.BoolorContext ctx) { Label orTrue = new Label(); Label orEnd = new Label(); typeStack.push(Type.INT_TYPE); visit(ctx.expression(0)); gen.visitJumpInsn(Opcodes.IFNE, orTrue); visit(ctx.expression(1)); gen.visitJumpInsn(Opcodes.IFNE, orTrue); typeStack.pop(); pushBoolean(false); gen.goTo(orEnd); gen.visitLabel(orTrue); pushBoolean(true); gen.visitLabel(orEnd); return null; } @Override public Void visitConditional(JavascriptParser.ConditionalContext ctx) { Label condFalse = new Label(); Label condEnd = new Label(); typeStack.push(Type.INT_TYPE); visit(ctx.expression(0)); typeStack.pop(); gen.visitJumpInsn(Opcodes.IFEQ, condFalse); visit(ctx.expression(1)); gen.goTo(condEnd); gen.visitLabel(condFalse); visit(ctx.expression(2)); gen.visitLabel(condEnd); return null; } private void pushArith(int operator, ExpressionContext left, ExpressionContext right) { pushBinaryOp(operator, left, right, Type.DOUBLE_TYPE, Type.DOUBLE_TYPE, Type.DOUBLE_TYPE); } private void pushShift(int operator, ExpressionContext left, ExpressionContext right) { pushBinaryOp(operator, left, right, Type.LONG_TYPE, Type.INT_TYPE, Type.LONG_TYPE); } private void pushBitwise(int operator, ExpressionContext left, ExpressionContext right) { pushBinaryOp(operator, left, right, Type.LONG_TYPE, Type.LONG_TYPE, Type.LONG_TYPE); } private void pushBinaryOp( int operator, ExpressionContext left, ExpressionContext right, Type leftType, Type rightType, Type returnType) { typeStack.push(leftType); visit(left); typeStack.pop(); typeStack.push(rightType); visit(right); typeStack.pop(); gen.visitInsn(operator); gen.cast(returnType, typeStack.peek()); } private void pushCond(int operator, ExpressionContext left, ExpressionContext right) { Label labelTrue = new Label(); Label labelReturn = new Label(); typeStack.push(Type.DOUBLE_TYPE); visit(left); visit(right); typeStack.pop(); gen.ifCmp(Type.DOUBLE_TYPE, operator, labelTrue); pushBoolean(false); gen.goTo(labelReturn); gen.visitLabel(labelTrue); pushBoolean(true); gen.visitLabel(labelReturn); } private void pushBoolean(boolean truth) { switch (typeStack.peek().getSort()) { case Type.INT: gen.push(truth); break; case Type.LONG: gen.push(truth ? 1L : 0L); break; case Type.DOUBLE: gen.push(truth ? 1. : 0.); break; default: throw new IllegalStateException("Invalid expected type: " + typeStack.peek()); } } private void pushLong(long i) { switch (typeStack.peek().getSort()) { case Type.INT: gen.push((int) i); break; case Type.LONG: gen.push(i); break; case Type.DOUBLE: gen.push((double) i); break; default: throw new IllegalStateException("Invalid expected type: " + typeStack.peek()); } } }.visit(parseTree); gen.mark(endTry); gen.returnValue(); gen.mark(catchHandler); gen.loadThis(); gen.invokeStatic(JAVASCRIPT_COMPILER_TYPE, PATCH_STACK_METHOD); gen.throwException(); gen.endMethod(); classWriter.visitEnd(); } static String normalizeQuotes(String text) { StringBuilder out = new StringBuilder(text.length()); boolean inDoubleQuotes = false; for (int i = 0; i < text.length(); ++i) { char c = text.charAt(i); if (c == '\\') { c = text.charAt(++i); if (c == '\\') { out.append('\\'); // re-escape the backslash } // no escape for double quote } else if (c == '\'') { if (inDoubleQuotes) { // escape in output out.append('\\'); } else { int j = findSingleQuoteStringEnd(text, i); out.append(text, i, j); // copy up to end quote (leave end for append below) i = j; } } else if (c == '"') { c = '\''; // change beginning/ending doubles to singles inDoubleQuotes = !inDoubleQuotes; } out.append(c); } return out.toString(); } static int findSingleQuoteStringEnd(String text, int start) { ++start; // skip beginning while (text.charAt(start) != '\'') { if (text.charAt(start) == '\\') { ++start; // blindly consume escape value } ++start; } return start; } /** * The default set of functions available to expressions. * *

See the {@link org.apache.lucene.expressions.js package documentation} for a list. */ public static final Map DEFAULT_FUNCTIONS = loadDefaultFunctions(); private static Map loadDefaultFunctions() { final Map map = new HashMap<>(); final Lookup publicLookup = MethodHandles.publicLookup(); try { final Properties props = new Properties(); var name = JavascriptCompiler.class.getSimpleName() + ".properties"; try (Reader in = IOUtils.getDecodingReader( IOUtils.requireResourceNonNull( JavascriptCompiler.class.getResourceAsStream(name), name), StandardCharsets.UTF_8)) { props.load(in); } for (final String call : props.stringPropertyNames()) { final String[] vals = props.getProperty(call).split(","); if (vals.length != 3) { throw new Error("Syntax error while reading Javascript functions from resource"); } final Class clazz = Class.forName(vals[0].trim()); final String methodName = vals[1].trim(); final int arity = Integer.parseInt(vals[2].trim()); final MethodHandle mh = publicLookup.findStatic( clazz, methodName, MethodType.methodType(double.class, Collections.nCopies(arity, double.class))); checkFunction(mh); map.put(call, mh); } } catch (ReflectiveOperationException | IOException e) { throw new Error("Cannot resolve function", e); } return Collections.unmodifiableMap(map); } /** Check Method signature for compatibility. */ private static void checkFunction(MethodHandle method) { Supplier methodNameSupplier = method::toString; // try to crack the handle and check if it is a static call: int refKind; try { MethodHandleInfo cracked = LOOKUP.revealDirect(method); refKind = cracked.getReferenceKind(); // we have a much better name for the method so display it instead: methodNameSupplier = () -> cracked.getDeclaringClass().getName() + "#" + cracked.getName() + cracked.getMethodType(); } catch (@SuppressWarnings("unused") IllegalArgumentException | SecurityException iae) { // can't check for static, we assume it is static // (it does not matter as we call the MethodHandle directly if it is compatible): refKind = MethodHandleInfo.REF_invokeStatic; } if (refKind != MethodHandleInfo.REF_invokeStatic && refKind != MethodHandleInfo.REF_getStatic) { throw new IllegalArgumentException(methodNameSupplier.get() + " is not static."); } // do some checks if the signature is "compatible": final MethodType type = method.type(); for (int arg = 0, arity = type.parameterCount(); arg < arity; arg++) { if (type.parameterType(arg) != double.class) { throw new IllegalArgumentException( methodNameSupplier.get() + " must take only double parameters."); } } if (type.returnType() != double.class) { throw new IllegalArgumentException(methodNameSupplier.get() + " does not return a double."); } } /** * Bootstrap method for dynamic constants. This returns a {@link MethodHandle} for the {@code * functionName} from the class data passed via {@link Lookup#defineHiddenClassWithClassData}. The * {@code constantName} is ignored. */ static MethodHandle dynamicConstantBootstrap( Lookup lookup, String constantName, Class type, String functionName) throws IllegalAccessException { if (type != MethodHandle.class) { throw new IllegalArgumentException("Invalid type of constant: " + type.getName()); } final var classData = Objects.requireNonNull( MethodHandles.classData(lookup, ConstantDescs.DEFAULT_NAME, Map.class), "Missing class data for " + lookup); return (MethodHandle) Objects.requireNonNull( classData.get(functionName), "Function does not exist: " + functionName); } /** * Method called from try/catch handler in compiled expression. This patches the stack trace and * adds back a hidden frame (including the source code of script as filename). */ static Throwable patchStackTrace(Throwable t, Expression impl) { var extra = new StackTraceElement(impl.getClass().getName(), "evaluate", impl.sourceText, -1); var origStack = t.getStackTrace(); var myStack = new Throwable().getStackTrace(); var top = Arrays.stream(origStack).limit(Math.max(0, origStack.length - myStack.length + 1)); var tail = Arrays.stream(myStack).skip(1); t.setStackTrace( Stream.of(top, Stream.of(extra), tail) .flatMap(Function.identity()) .toArray(StackTraceElement[]::new)); return t; } }





© 2015 - 2025 Weber Informatics LLC | Privacy Policy