| using Antlr4.Runtime; |
| using Antlr4.Runtime.Tree; |
| using J2N; |
| using J2N.Text; |
| using Lucene.Net.Queries.Function; |
| using Lucene.Net.Support; |
| using System; |
| using System.Collections.Generic; |
| using System.Diagnostics.CodeAnalysis; |
| using System.Globalization; |
| using System.IO; |
| using System.Linq; |
| using System.Reflection; |
| using System.Reflection.Emit; |
| using System.Text; |
| using JCG = J2N.Collections.Generic; |
| |
| namespace Lucene.Net.Expressions.JS |
| { |
| /* |
| * 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. |
| */ |
| |
| /// <summary>An expression compiler for javascript expressions.</summary> |
| /// <remarks> |
| /// An expression compiler for javascript expressions. |
| /// <para/> |
| /// Example: |
| /// <code> |
| /// Expression foo = JavascriptCompiler.Compile("((0.3*popularity)/10.0)+(0.7*score)"); |
| /// </code> |
| /// <para/> |
| /// See the <see cref="Lucene.Net.Expressions.JS">package documentation</see> for |
| /// the supported syntax and default functions. |
| /// <para> |
| /// You can compile with an alternate set of functions via <see cref="Compile(string, IDictionary{string, MethodInfo})"/>. |
| /// For example: |
| /// <code> |
| /// // instantiate and add all the default functions |
| /// IDictionary<string, MethodInfo> functions = new Dictionary<string, MethodInfo>(JavascriptCompiler.DEFAULT_FUNCTIONS); |
| /// // add sqrt() |
| /// functions["sqrt"] = (typeof(Math)).GetMethod("Sqrt", new Type[] { typeof(double) }); |
| /// // call compile with customized function map |
| /// Expression foo = JavascriptCompiler.Compile("sqrt(score)+ln(popularity)", functions); |
| /// </code> |
| /// </para> |
| /// @lucene.experimental |
| /// </remarks> |
| public class JavascriptCompiler |
| { |
| |
| private static readonly string COMPILED_EXPRESSION_CLASS = typeof(Expression).Namespace + ".CompiledExpression"; |
| |
| //private static readonly string COMPILED_EXPRESSION_INTERNAL = COMPILED_EXPRESSION_CLASS.Replace('.', '/'); // LUCENENET: Not used |
| |
| private static readonly Type EXPRESSION_TYPE = Type.GetType(typeof(Expression).FullName); |
| |
| private static readonly Type FUNCTION_VALUES_TYPE = typeof(FunctionValues); |
| |
| private static readonly ConstructorInfo EXPRESSION_CTOR = typeof(Expression). |
| GetConstructor(new Type[] { typeof(string), typeof(string[]) }); |
| |
| private static readonly MethodInfo EVALUATE_METHOD = GetMethod(EXPRESSION_TYPE, "Evaluate", |
| new[] { typeof(int), typeof(FunctionValues[]) }); |
| |
| private static readonly MethodInfo DOUBLE_VAL_METHOD = GetMethod(FUNCTION_VALUES_TYPE, "DoubleVal", |
| new[] { typeof(int) }); |
| |
| // We use the same class name for all generated classes as they all have their own class loader. |
| // The source code is displayed as "source file name" in stack trace. |
| // to work around import clash: |
| private static MethodInfo GetMethod(Type type, string method, Type[] parms) |
| { |
| return type.GetMethod(method, parms); |
| } |
| |
| private readonly string sourceText; |
| |
| // LUCENENET specific: OrderedDictioary<TKey, TValue> is a replacement for LinkedHashMap<K, V> in the JDK |
| private readonly IDictionary<string, int> externalsMap = new JCG.OrderedDictionary<string, int>(); |
| |
| private TypeBuilder dynamicType; |
| |
| private readonly IDictionary<string, MethodInfo> functions; |
| |
| private ILGenerator gen; |
| private AssemblyBuilder asmBuilder; |
| private MethodBuilder evalMethod; |
| private ModuleBuilder modBuilder; |
| |
| // This maximum length is theoretically 65535 bytes, but as its CESU-8 encoded we dont know how large it is in bytes, so be safe |
| // rcmuir: "If your ranking function is that large you need to check yourself into a mental institution!" |
| /// <summary>Compiles the given expression.</summary> |
| /// <param name="sourceText">The expression to compile</param> |
| /// <returns>A new compiled expression</returns> |
| /// <exception cref="ParseException">on failure to compile</exception> |
| // LUCENENET TODO: ParseException not being thrown here - need to check |
| // where this is thrown in Java and throw the equivalent in .NET |
| public static Expression Compile(string sourceText) |
| { |
| return new JavascriptCompiler(sourceText).CompileExpression(); |
| } |
| |
| /// <summary>Compiles the given expression with the supplied custom functions.</summary> |
| /// <remarks> |
| /// Compiles the given expression with the supplied custom functions. |
| /// <para/> |
| /// Functions must be <c>public static</c>, return <see cref="double"/> and |
| /// can take from zero to 256 <see cref="double"/> parameters. |
| /// </remarks> |
| /// <param name="sourceText">The expression to compile</param> |
| /// <param name="functions">map of <see cref="string"/> names to functions</param> |
| /// <returns>A new compiled expression</returns> |
| /// <exception cref="ParseException">on failure to compile</exception> |
| public static Expression Compile(string sourceText, IDictionary<string, MethodInfo> functions) |
| { |
| foreach (MethodInfo m in functions.Values) |
| { |
| CheckFunction(m); |
| } |
| return new JavascriptCompiler(sourceText, functions).CompileExpression(); |
| } |
| |
| /// <summary>This method is unused, it is just here to make sure that the function signatures don't change.</summary> |
| /// <remarks> |
| /// This method is unused, it is just here to make sure that the function signatures don't change. |
| /// If this method fails to compile, you also have to change the byte code generator to correctly |
| /// use the <see cref="FunctionValues"/> class. |
| /// </remarks> |
| #pragma warning disable IDE0051 // Remove unused private members |
| private static void UnusedTestCompile() |
| #pragma warning restore IDE0051 // Remove unused private members |
| { |
| FunctionValues f = null; |
| /*double ret = */ |
| f.DoubleVal(2); // LUCENENET: IDE0059: Remove unnecessary value assignment |
| } |
| |
| /// <summary>Constructs a compiler for expressions.</summary> |
| /// <param name="sourceText">The expression to compile</param> |
| private JavascriptCompiler(string sourceText) |
| : this(sourceText, DEFAULT_FUNCTIONS) |
| { |
| } |
| |
| /// <summary>Constructs a compiler for expressions with specific set of functions</summary> |
| /// <param name="sourceText">The expression to compile</param> |
| /// <param name="functions">The set of functions to compile with</param> |
| private JavascriptCompiler(string sourceText, IDictionary<string, MethodInfo> functions) |
| { |
| this.sourceText = sourceText ?? throw new ArgumentNullException(nameof(sourceText)); // LUCENENET specific - changed from IllegalArgumentException to ArgumentNullException (.NET convention) |
| this.functions = functions; |
| } |
| |
| /// <summary>Compiles the given expression with the specified parent classloader</summary> |
| /// <returns>A new compiled expression</returns> |
| /// <exception cref="ParseException">on failure to compile</exception> |
| private Expression CompileExpression() |
| { |
| try |
| { |
| var antlrTree = GetAntlrComputedExpressionTree(); |
| BeginCompile(); |
| RecursiveCompile(antlrTree); |
| EndCompile(); |
| return |
| (Expression) |
| Activator.CreateInstance(dynamicType.CreateTypeInfo().AsType(), sourceText, externalsMap.Keys.ToArray()); |
| |
| } |
| catch (Exception exception) when (exception.IsInstantiationException() || exception.IsIllegalAccessException() || |
| exception.IsNoSuchMethodException() || exception.IsInvocationTargetException()) |
| { |
| throw IllegalStateException.Create("An internal error occurred attempting to compile the expression (" + sourceText + ").", exception); |
| } |
| } |
| |
| private void BeginCompile() |
| { |
| var assemblyName = new AssemblyName("Lucene.Net.Expressions.Dynamic" + Math.Abs(new J2N.Randomizer().Next())); |
| asmBuilder = AssemblyBuilder.DefineDynamicAssembly(assemblyName, AssemblyBuilderAccess.RunAndCollect); |
| |
| modBuilder = asmBuilder.DefineDynamicModule(assemblyName.Name + ".dll"); |
| |
| dynamicType = modBuilder.DefineType(COMPILED_EXPRESSION_CLASS, |
| TypeAttributes.AnsiClass | TypeAttributes.AutoClass | TypeAttributes.Public | TypeAttributes.Class | |
| TypeAttributes.BeforeFieldInit | TypeAttributes.AutoLayout, EXPRESSION_TYPE); |
| |
| ConstructorBuilder constructorBuilder = dynamicType.DefineConstructor(MethodAttributes.Public, |
| CallingConventions.HasThis, |
| new[] { typeof(string), typeof(string[]) }); |
| |
| ILGenerator ctorGen = constructorBuilder.GetILGenerator(); |
| ctorGen.Emit(OpCodes.Ldarg_0); |
| ctorGen.Emit(OpCodes.Ldarg_1); |
| ctorGen.Emit(OpCodes.Ldarg_2); |
| ctorGen.Emit(OpCodes.Call, EXPRESSION_CTOR); |
| ctorGen.Emit(OpCodes.Nop); |
| ctorGen.Emit(OpCodes.Nop); |
| ctorGen.Emit(OpCodes.Ret); |
| |
| evalMethod = dynamicType.DefineMethod("Evaluate", MethodAttributes.Public | MethodAttributes.Virtual, |
| typeof(double), new[] { typeof(int), typeof(FunctionValues[]) }); |
| gen = evalMethod.GetILGenerator(); |
| } |
| |
| private void RecursiveCompile(JavascriptParser.ExpressionContext context) |
| { |
| var listener = new JavascriptListener(this); |
| listener.EnterExpression(context); |
| } |
| |
| // LUCENENET-specific: these methods use the ANTLRv4 listener API to |
| // be equivalent to what RecursiveCompile was doing previously |
| private class JavascriptListener : JavascriptBaseListener |
| { |
| private readonly JavascriptCompiler compiler; |
| |
| public JavascriptListener(JavascriptCompiler compiler) |
| { |
| this.compiler = compiler; |
| } |
| |
| public override void EnterExpression(JavascriptParser.ExpressionContext context) |
| { |
| EnterConditional(context.conditional()); |
| } |
| |
| public override void EnterCall(JavascriptParser.CallContext context) |
| { |
| ITerminalNode identifier = context.NAMESPACE_ID(); |
| string call = identifier.GetText(); |
| // LUCENENET: logic changed to get arguments from parse context |
| var arguments = context.arguments().conditional(); |
| int argumentCount = arguments.Length; |
| if (!compiler.functions.TryGetValue(call, out MethodInfo method) || method is null) |
| { |
| throw new ArgumentException("Unrecognized method call (" + call + ")."); |
| } |
| int arity = method.GetParameters().Length; |
| if (argumentCount != arity) |
| { |
| throw new ArgumentException("Expected (" + arity + ") arguments for method call (" |
| + call + "), but found (" + argumentCount + ")."); |
| } |
| for (int argument = 0; argument < argumentCount; ++argument) // LUCENENET: was 1 to and including arguments |
| { |
| EnterConditional(arguments[argument]); |
| } |
| compiler.Emit(OpCodes.Call, method); |
| } |
| |
| public override void EnterPrimary(JavascriptParser.PrimaryContext context) |
| { |
| if (context.NAMESPACE_ID() is { } namespaceId) |
| { |
| string text = namespaceId.GetText(); |
| |
| if (!compiler.externalsMap.TryGetValue(text, out int index)) |
| { |
| compiler.externalsMap[text] = index = compiler.externalsMap.Count; |
| } |
| |
| compiler.Emit(OpCodes.Nop); |
| |
| compiler.Emit(OpCodes.Ldarg_2); |
| compiler.Emit(OpCodes.Ldc_I4, index); |
| |
| compiler.Emit(OpCodes.Ldelem_Ref); |
| compiler.Emit(OpCodes.Ldarg_1); |
| compiler.Emit(OpCodes.Callvirt, DOUBLE_VAL_METHOD); |
| } |
| else if (context.numeric() is { } numeric) |
| { |
| EnterNumeric(numeric); |
| } |
| else if (context.conditional() is { } conditional) |
| { |
| EnterConditional(conditional); |
| } |
| else |
| { |
| throw new ParseException("Unknown primary alternative", context.Start.StartIndex); |
| } |
| } |
| |
| public override void EnterNumeric(JavascriptParser.NumericContext context) |
| { |
| string text = context.GetText(); |
| |
| if (context.HEX() is not null) |
| { |
| compiler.PushInt64(Convert.ToInt64(text, 16)); |
| } |
| else if (context.OCTAL() is not null) |
| { |
| compiler.PushInt64(Convert.ToInt64(text, 8)); |
| } |
| else |
| { |
| compiler.Emit(OpCodes.Ldc_R8, double.Parse(text, CultureInfo.InvariantCulture)); |
| } |
| } |
| |
| public override void EnterPostfix(JavascriptParser.PostfixContext context) |
| { |
| if (context.primary() is { } primary) |
| { |
| EnterPrimary(primary); |
| } |
| else if (context.call() is { } call) |
| { |
| EnterCall(call); |
| } |
| else |
| { |
| throw new ParseException("Unknown postfix alternative", context.Start.StartIndex); |
| } |
| } |
| |
| public override void EnterUnary(JavascriptParser.UnaryContext context) |
| { |
| if (context.postfix() is { } postfix) |
| { |
| EnterPostfix(postfix); |
| } |
| else if (context.AT_ADD() is not null) |
| { |
| EnterUnary(context.unary()); |
| compiler.Emit(OpCodes.Conv_R8); |
| } |
| else if (context.unary_operator() is { } unaryOperator) |
| { |
| EnterUnary(context.unary()); |
| |
| if (unaryOperator.AT_SUBTRACT() is not null) |
| { |
| compiler.Emit(OpCodes.Neg); |
| } |
| else if (unaryOperator.AT_BIT_NOT() is not null) |
| { |
| compiler.Emit(OpCodes.Conv_I8); // cast to long (truncate) |
| compiler.Emit(OpCodes.Not); |
| compiler.Emit(OpCodes.Conv_R8); |
| } |
| else if (unaryOperator.AT_BOOL_NOT() is not null) |
| { |
| compiler.Emit(OpCodes.Ldc_I4_0); |
| compiler.Emit(OpCodes.Ceq); |
| compiler.Emit(OpCodes.Conv_R8); |
| } |
| else |
| { |
| throw new ParseException("Unknown unary_operator alternative", context.Start.StartIndex); |
| } |
| } |
| else |
| { |
| throw new ParseException("Unknown unary alternative", context.Start.StartIndex); |
| } |
| } |
| |
| public override void EnterAdditive(JavascriptParser.AdditiveContext context) |
| { |
| CompileBinary(context, context.multiplicative, EnterMultiplicative, terminalNode => |
| { |
| if (terminalNode.Symbol.Type == JavascriptParser.AT_ADD) |
| { |
| compiler.PushOpWithConvert(OpCodes.Add); |
| } |
| else if (terminalNode.Symbol.Type == JavascriptParser.AT_SUBTRACT) |
| { |
| compiler.PushOpWithConvert(OpCodes.Sub); |
| } |
| else |
| { |
| throw new ParseException("Unknown additive token", context.Start.StartIndex); |
| } |
| }); |
| } |
| |
| public override void EnterMultiplicative(JavascriptParser.MultiplicativeContext context) |
| { |
| CompileBinary(context, context.unary, EnterUnary, terminalNode => |
| { |
| if (terminalNode.Symbol.Type == JavascriptParser.AT_MULTIPLY) |
| { |
| compiler.PushOpWithConvert(OpCodes.Mul); |
| } |
| else if (terminalNode.Symbol.Type == JavascriptParser.AT_DIVIDE) |
| { |
| compiler.PushOpWithConvert(OpCodes.Div); |
| } |
| else if (terminalNode.Symbol.Type == JavascriptParser.AT_MODULO) |
| { |
| compiler.PushOpWithConvert(OpCodes.Rem); |
| } |
| else |
| { |
| throw new ParseException("Unknown multiplicative token", context.Start.StartIndex); |
| } |
| }); |
| } |
| |
| public override void EnterShift(JavascriptParser.ShiftContext context) |
| { |
| EnterAdditive(context.additive(0)); |
| |
| if (context.children.Count == 1) // if we don't have a shift token |
| { |
| return; |
| } |
| |
| compiler.Emit(OpCodes.Conv_I8); // cast to long (truncate) |
| int argIndex = 1; |
| |
| for (int i = 1; i < context.children.Count; i += 2) |
| { |
| if (context.children[i] is ITerminalNode terminalNode) |
| { |
| EnterAdditive(context.additive(argIndex++)); |
| compiler.Emit(OpCodes.Conv_I4); // cast to int (truncate) |
| |
| // mask off 63 to prevent overflow (fixes issue on x86 .NET Framework, #1034) |
| compiler.Emit(OpCodes.Ldc_I4, 0x3F); |
| compiler.Emit(OpCodes.And); |
| |
| if (terminalNode.Symbol.Type == JavascriptParser.AT_BIT_SHL) |
| { |
| compiler.PushOpWithConvert(OpCodes.Shl); |
| } |
| else if (terminalNode.Symbol.Type == JavascriptParser.AT_BIT_SHR) |
| { |
| compiler.PushOpWithConvert(OpCodes.Shr); |
| } |
| else if (terminalNode.Symbol.Type == JavascriptParser.AT_BIT_SHU) |
| { |
| compiler.PushOpWithConvert(OpCodes.Shr_Un); |
| } |
| else |
| { |
| throw new ParseException("Unknown shift token", context.Start.StartIndex); |
| } |
| } |
| else |
| { |
| throw new ParseException("Unexpected child", context.Start.StartIndex); |
| } |
| } |
| } |
| |
| public override void EnterRelational(JavascriptParser.RelationalContext context) |
| { |
| CompileBinary(context, context.shift, EnterShift, terminalNode => |
| { |
| if (terminalNode.Symbol.Type == JavascriptParser.AT_COMP_LT) |
| { |
| compiler.PushOpWithConvert(OpCodes.Clt); |
| } |
| else if (terminalNode.Symbol.Type == JavascriptParser.AT_COMP_GT) |
| { |
| compiler.PushOpWithConvert(OpCodes.Cgt); |
| } |
| else if (terminalNode.Symbol.Type == JavascriptParser.AT_COMP_LTE) |
| { |
| compiler.PushCondEq(OpCodes.Cgt); |
| } |
| else if (terminalNode.Symbol.Type == JavascriptParser.AT_COMP_GTE) |
| { |
| compiler.PushCondEq(OpCodes.Clt); |
| } |
| else |
| { |
| throw new ParseException("Unknown relational token", context.Start.StartIndex); |
| } |
| }); |
| } |
| |
| public override void EnterEquality(JavascriptParser.EqualityContext context) |
| { |
| CompileBinary(context, context.relational, EnterRelational, terminalNode => |
| { |
| if (terminalNode.Symbol.Type == JavascriptParser.AT_COMP_EQ) |
| { |
| compiler.PushOpWithConvert(OpCodes.Ceq); |
| } |
| else if (terminalNode.Symbol.Type == JavascriptParser.AT_COMP_NEQ) |
| { |
| compiler.PushCondEq(OpCodes.Ceq); |
| } |
| else |
| { |
| throw new ParseException("Unknown equality token", context.Start.StartIndex); |
| } |
| }); |
| } |
| |
| public override void EnterBitwise_and(JavascriptParser.Bitwise_andContext context) |
| { |
| CompileBinary(context, context.equality, equalityContext => |
| { |
| EnterEquality(equalityContext); |
| |
| if (context.children.Count > 1) // if we have a bitwise token |
| { |
| compiler.Emit(OpCodes.Conv_I8); // cast to long (truncate) |
| } |
| }, terminalNode => |
| { |
| if (terminalNode.Symbol.Type == JavascriptParser.AT_BIT_AND) |
| { |
| compiler.PushOpWithConvert(OpCodes.And); |
| } |
| else |
| { |
| throw new ParseException("Unknown bitwise_and token", context.Start.StartIndex); |
| } |
| }); |
| } |
| |
| public override void EnterBitwise_xor(JavascriptParser.Bitwise_xorContext context) |
| { |
| CompileBinary(context, context.bitwise_and, andContext => |
| { |
| EnterBitwise_and(andContext); |
| |
| if (context.children.Count > 1) // if we have a bitwise token |
| { |
| compiler.Emit(OpCodes.Conv_I8); // cast to long (truncate) |
| } |
| }, terminalNode => |
| { |
| if (terminalNode.Symbol.Type == JavascriptParser.AT_BIT_XOR) |
| { |
| compiler.PushOpWithConvert(OpCodes.Xor); |
| } |
| else |
| { |
| throw new ParseException("Unknown bitwise_xor token", context.Start.StartIndex); |
| } |
| }); |
| } |
| |
| public override void EnterBitwise_or(JavascriptParser.Bitwise_orContext context) |
| { |
| CompileBinary(context, context.bitwise_xor, xorContext => |
| { |
| EnterBitwise_xor(xorContext); |
| |
| if (context.children.Count > 1) // if we have a bitwise token |
| { |
| compiler.Emit(OpCodes.Conv_I8); // cast to long (truncate) |
| } |
| }, terminalNode => |
| { |
| if (terminalNode.Symbol.Type == JavascriptParser.AT_BIT_OR) |
| { |
| compiler.PushOpWithConvert(OpCodes.Or); |
| } |
| else |
| { |
| throw new ParseException("Unknown bitwise_or token", context.Start.StartIndex); |
| } |
| }); |
| } |
| |
| public override void EnterLogical_and(JavascriptParser.Logical_andContext context) |
| { |
| if (context.children.Count == 1) |
| { |
| EnterBitwise_or(context.bitwise_or(0)); |
| } |
| else |
| { |
| // Evaluate the first operand and check if it is false |
| EnterBitwise_or(context.bitwise_or(0)); |
| compiler.Emit(OpCodes.Ldc_I4_0); |
| compiler.Emit(OpCodes.Ceq); |
| |
| // Iterate over the remaining operands |
| for (int i = 2; i < context.children.Count; i += 2) |
| { |
| if (context.children[i] is not JavascriptParser.Bitwise_orContext bitwiseOrContext) |
| { |
| throw new ParseException("Unexpected child of logical_and", context.Start.StartIndex); |
| } |
| |
| // Evaluate the next operand and check if it is false |
| EnterBitwise_or(bitwiseOrContext); |
| compiler.Emit(OpCodes.Ldc_I4_0); |
| compiler.Emit(OpCodes.Ceq); |
| |
| // Combine the results using OR |
| compiler.Emit(OpCodes.Or); |
| } |
| |
| // Check if the combined result is false |
| compiler.Emit(OpCodes.Ldc_I4_0); |
| compiler.Emit(OpCodes.Ceq); |
| |
| // Convert the result to a double |
| compiler.Emit(OpCodes.Conv_R8); |
| } |
| } |
| |
| public override void EnterLogical_or(JavascriptParser.Logical_orContext context) |
| { |
| if (context.children.Count == 1) |
| { |
| EnterLogical_and(context.logical_and(0)); |
| } |
| else |
| { |
| // Evaluate the first operand and check if it is true |
| EnterLogical_and(context.logical_and(0)); |
| compiler.Emit(OpCodes.Ldc_I4_0); |
| compiler.Emit(OpCodes.Ceq); |
| compiler.Emit(OpCodes.Ldc_I4_1); |
| compiler.Emit(OpCodes.Xor); |
| |
| // Iterate over the remaining operands |
| for (int i = 2; i < context.children.Count; i += 2) |
| { |
| if (context.children[i] is not JavascriptParser.Logical_andContext logicalAndContext) |
| { |
| throw new ParseException("Unexpected child of logical_or", context.Start.StartIndex); |
| } |
| |
| // Evaluate the next operand and check if it is true |
| EnterLogical_and(logicalAndContext); |
| compiler.Emit(OpCodes.Ldc_I4_0); |
| compiler.Emit(OpCodes.Ceq); |
| compiler.Emit(OpCodes.Ldc_I4_1); |
| compiler.Emit(OpCodes.Xor); |
| |
| // Combine the results using OR |
| compiler.Emit(OpCodes.Or); |
| } |
| |
| // Check if the combined result is true |
| compiler.Emit(OpCodes.Ldc_I4_1); |
| compiler.Emit(OpCodes.Ceq); |
| |
| // Convert the result to a double |
| compiler.Emit(OpCodes.Conv_R8); |
| } |
| } |
| |
| public override void EnterConditional(JavascriptParser.ConditionalContext context) |
| { |
| if (context.children.Count == 1) |
| { |
| EnterLogical_or(context.logical_or()); |
| } |
| else |
| { |
| Label condFalse = compiler.gen.DefineLabel(); |
| Label condEnd = compiler.gen.DefineLabel(); |
| |
| // Evaluate the condition |
| EnterLogical_or(context.logical_or()); |
| compiler.Emit(OpCodes.Ldc_I4_0); |
| compiler.Emit(OpCodes.Beq, condFalse); |
| |
| // Evaluate the true branch |
| EnterConditional(context.conditional(0)); |
| compiler.Emit(OpCodes.Br_S, condEnd); |
| |
| // Evaluate the false branch |
| compiler.gen.MarkLabel(condFalse); |
| EnterConditional(context.conditional(1)); |
| |
| // Mark the end of the conditional |
| compiler.gen.MarkLabel(condEnd); |
| } |
| } |
| |
| private static void CompileBinary<TContext, TArg>( |
| TContext context, |
| Func<int, TArg> getArg, |
| Action<TArg> enterArg, |
| Action<ITerminalNode> enterOp) |
| where TContext : ParserRuleContext |
| where TArg : ParserRuleContext |
| { |
| int argIndex = 0; |
| enterArg(getArg(argIndex++)); |
| |
| for (int i = 1; i < context.children.Count; i += 2) |
| { |
| if (context.children[i] is ITerminalNode terminalNode) |
| { |
| enterArg(getArg(argIndex++)); |
| enterOp(terminalNode); |
| } |
| else |
| { |
| throw new ParseException("Unexpected child", context.Start.StartIndex); |
| } |
| } |
| } |
| } |
| |
| private void PushCondEq(OpCode opCode) |
| { |
| Emit(opCode); |
| Emit(OpCodes.Ldc_I4_1); |
| Emit(OpCodes.Xor); |
| Emit(OpCodes.Conv_R8); |
| } |
| |
| private void PushOpWithConvert(OpCode opCode) |
| { |
| Emit(opCode); |
| Emit(OpCodes.Conv_R8); |
| } |
| |
| /// <summary> |
| /// NOTE: This was pushLong() in Lucene |
| /// </summary> |
| private void PushInt64(long i) |
| { |
| Emit(OpCodes.Ldc_I8, i); |
| if (!sourceText.Contains("<<")) |
| { |
| Emit(OpCodes.Conv_R8); |
| } |
| } |
| |
| // LUCENENET-specific - wrapping Emit methods which is helpful for debugging |
| private void Emit(OpCode opcode) |
| { |
| // Console.WriteLine(opcode); |
| gen.Emit(opcode); |
| } |
| |
| private void Emit(OpCode opcode, Label label) |
| { |
| // Console.WriteLine(opcode + " " + label); |
| gen.Emit(opcode, label); |
| } |
| |
| private void Emit(OpCode opcode, double arg) |
| { |
| // Console.WriteLine(opcode + " " + arg); |
| gen.Emit(opcode, arg); |
| } |
| |
| private void Emit(OpCode opcode, long arg) |
| { |
| // Console.WriteLine(opcode + " " + arg); |
| gen.Emit(opcode, arg); |
| } |
| |
| private void Emit(OpCode opcode, MethodInfo arg) |
| { |
| // Console.WriteLine(opcode + " " + arg); |
| gen.Emit(opcode, arg); |
| } |
| |
| private void EndCompile() |
| { |
| gen.Emit(OpCodes.Ret); |
| dynamicType.DefineMethodOverride(evalMethod, EVALUATE_METHOD); |
| } |
| |
| private JavascriptParser.ExpressionContext GetAntlrComputedExpressionTree() |
| { |
| ICharStream input = new AntlrInputStream(sourceText); |
| JavascriptLexer lexer = new JavascriptLexer(input); |
| CommonTokenStream tokens = new CommonTokenStream(lexer); |
| JavascriptParser parser = new JavascriptParser(tokens); |
| |
| try |
| { |
| parser.ErrorHandler = new ParseExceptionErrorStrategy(); // LUCENENET-specific |
| return parser.expression(); |
| } |
| catch (RecognitionException re) |
| { |
| throw new ArgumentException(re.Message, re); |
| } |
| } |
| |
| // LUCENENET-specific: Throw a ParseException in the event of parsing errors |
| private class ParseExceptionErrorStrategy : DefaultErrorStrategy |
| { |
| public override void Recover(Parser recognizer, RecognitionException e) |
| { |
| int errorOffset = -1; |
| |
| for (ParserRuleContext parserRuleContext = recognizer.Context; parserRuleContext != null; parserRuleContext = (ParserRuleContext)parserRuleContext.Parent) |
| { |
| if (errorOffset < 0) |
| { |
| errorOffset = parserRuleContext.Start.StartIndex; |
| } |
| |
| parserRuleContext.exception = e; |
| } |
| |
| throw new ParseException(e.Message, errorOffset); |
| } |
| |
| public override IToken RecoverInline(Parser recognizer) |
| { |
| InputMismatchException cause = new InputMismatchException(recognizer); |
| int errorOffset = -1; |
| |
| for (ParserRuleContext parserRuleContext = recognizer.Context; parserRuleContext != null; parserRuleContext = (ParserRuleContext)parserRuleContext.Parent) |
| { |
| if (errorOffset < 0) |
| { |
| errorOffset = parserRuleContext.Start.StartIndex; |
| } |
| |
| parserRuleContext.exception = cause; |
| } |
| |
| throw new ParseException(cause.Message, errorOffset); |
| } |
| |
| public override void Sync(Parser recognizer) |
| { |
| } |
| } |
| |
| /// <summary>The default set of functions available to expressions.</summary> |
| /// <remarks> |
| /// The default set of functions available to expressions. |
| /// <para/> |
| /// See the <see cref="Lucene.Net.Expressions.JS">package documentation</see> for a list. |
| /// </remarks> |
| [SuppressMessage("CodeQuality", "IDE0079:Remove unnecessary suppression", Justification = "This is a SonarCloud issue")] |
| [SuppressMessage("Performance", "S3887:Use an immutable collection or reduce the accessibility of the non-private readonly field", Justification = "Collection is immutable")] |
| [SuppressMessage("Performance", "S2386:Use an immutable collection or reduce the accessibility of the public static field", Justification = "Collection is immutable")] |
| public static readonly IDictionary<string, MethodInfo> DEFAULT_FUNCTIONS = LoadDefaultFunctions(); |
| |
| private static IDictionary<string, MethodInfo> LoadDefaultFunctions() // LUCENENET: Avoid static constructors (see https://github.com/apache/lucenenet/pull/224#issuecomment-469284006) |
| { |
| IDictionary<string, MethodInfo> map = new Dictionary<string, MethodInfo>(); |
| try |
| { |
| foreach (var property in GetDefaultSettings()) |
| { |
| string[] vals = property.Value.Split(',').TrimEnd(); |
| if (vals.Length != 3) |
| { |
| throw Error.Create("Error reading Javascript functions from settings"); |
| } |
| string typeName = vals[0]; |
| |
| Type clazz; |
| |
| if (vals[0].Contains("Lucene.Net")) |
| { |
| clazz = GetType(vals[0] + ", Lucene.Net"); |
| } |
| else |
| { |
| clazz = GetType(typeName); |
| } |
| |
| string methodName = vals[1].Trim(); |
| int arity = int.Parse(vals[2], CultureInfo.InvariantCulture); |
| Type[] args = new Type[arity]; |
| Arrays.Fill(args, typeof(double)); |
| MethodInfo method = clazz.GetMethod(methodName, args); |
| CheckFunction(method); |
| map[property.Key] = method; |
| } |
| } |
| catch (Exception e) when (e.IsNoSuchMethodException() || e.IsClassNotFoundException() || e.IsIOException()) |
| { |
| throw Error.Create("Cannot resolve function", e); |
| } |
| return Collections.AsReadOnly(map); |
| } |
| |
| private static Type GetType(string typeName) |
| { |
| try |
| { |
| return Type.GetType(typeName, true); |
| } |
| catch |
| { |
| return null; |
| } |
| } |
| |
| private static IDictionary<string, string> GetDefaultSettings() |
| { |
| var settings = new Dictionary<string, string>(); |
| var type = typeof(JavascriptCompiler); |
| using var reader = new StreamReader(type.FindAndGetManifestResourceStream(type.Name + ".properties"), Encoding.UTF8); |
| settings.LoadProperties(reader); |
| return settings; |
| } |
| |
| private static void CheckFunction(MethodInfo method) |
| { |
| // do some checks if the signature is "compatible": |
| if (!method.IsStatic) |
| { |
| throw new ArgumentException(method + " is not static."); |
| } |
| if (!method.IsPublic) |
| { |
| throw new ArgumentException(method + " is not public."); |
| } |
| if (!method.DeclaringType.IsPublic) |
| { |
| //.NET Port. Inner class is being returned as not public even when declared public |
| if (method.DeclaringType.IsNestedAssembly) |
| { |
| throw new ArgumentException(method.DeclaringType.FullName + " is not public."); |
| } |
| } |
| if (method.GetParameters().Any(parmType => parmType.ParameterType != (typeof(double)))) |
| { |
| throw new ArgumentException(method + " must take only double parameters"); |
| } |
| if (method.ReturnType != typeof(double)) |
| { |
| throw new ArgumentException(method + " does not return a double."); |
| } |
| } |
| } |
| } |