See: Description
Interface | Description |
---|---|
CompilerConstants |
Constants used by the expression compilation support classes.
|
ExpressionFlags |
Expression flags that affect the compilation and runtime behavior of a
compiled expression.
|
ExpressionParserTokenTypes | |
Scope |
Objects of this type are used by the
SymbolResolver to partition
library objects and user variables registered by clients of the expression
engine. |
WritableData |
Classes which implement this interface must be able to write their
data to a
DataOutput stream. |
Class | Description |
---|---|
Argument |
This class represents an argument to a bytecode instruction.
|
Attribute |
This is the base class for all classes which represent attribute info
structures found within a class file.
|
BytecodeContainer |
This class represents any object which contains bytecode, such as a
bytecode instruction or a code unit containing multiple instructions.
|
ClassConstant |
This class represents the
CONSTANT_Class_info structure
defined by the Java Virtual Machine Specification. |
ClassFile |
This class encapsulates knowledge about the Java class file format as
defined in The Java Virtual Machine Specification, Second Edition
by Tim Lindholm and Frank Yellin.
|
CodeAttribute |
This class manages information necessary to create the
Code
attribute structure for a method. |
CodeUnit |
Instances of this class organize one or more bytecode instructions into
logical units of code.
|
CompiledExpression |
This is the base class for all custom, compiled expression classes.
|
Compiler |
Creates in-memory Java classes on the fly from expression strings written
in infix notation.
|
Compiler.LogicalData |
Helper class which stores information to assist when assembling branch
instructions for logical tests.
|
Compiler.PrimitiveInfo |
A helper class which stores information about the various primitive
data types supported by the expression compiler.
|
Constant |
This is the parent class to all of the various types of constants
supported by the Java class file format.
|
ConstantArgument |
This class represents an instruction argument which is a constant.
|
ConstantPool |
This class represents the constant pool found within the Java class
file format (see The Java Virtual Machine Specification, Second
Edition).
|
DoubleConstant |
This class represents a double constant.
|
ExceptionAttribute | |
Expression |
This class provides transparent access to compiled expressions.
|
Expression.CacheKey |
Definition of the key to the cache of
CompiledExpression
instances. |
ExpressionClassLoader |
This class extends
ClassLoader to enable the loading of
a class file stored as a byte array in memory. |
ExpressionInitializer |
Generates an initial value for a variable, using a contained
Expression . |
ExpressionLexer |
Tokenizes an arithmetic or logical expression in infix notation (input as
a stream of characters) into a stream of tokens suitable for the
ExpressionParser . |
ExpressionParser |
Creates an Abstract Syntax Tree (AST) representation of an expression
in infix notation from an input stream of tokens (provided by the
ExpressionLexer ). |
ExtraAst |
A modified abstract syntax tree node which provides an additional service
for storing a single object reference of the user's choosing.
|
FieldrefConstant | |
FloatConstant | |
FMIrefConstant | |
Function |
Support class which assists in parse-time resolution of method references
in user expressions.
|
Initializer |
Generates an initial value for a variable.
|
Instruction |
Instances of this class represent individual bytecode instructions.
|
IntegerConstant | |
InterfaceMethodrefConstant | |
LongConstant | |
MethodInfo | |
MethodrefConstant | |
NameAndTypeConstant | |
NumericArgument | |
SimpleInitializer |
Generates an initial value for a variable, using a contained instance
that is returned directly.
|
StringConstant | |
SymbolResolver |
The abstract base class of application-specific symbol resolvers.
|
SymbolResolver.CacheKey |
Helper class which is used to assist in lookups of methods when
introspecting invocation target classes.
|
SymbolResolver.TestResolver |
Concrete implementation of a
SymbolResolver for simple
testing and debug purposes. |
TestDriver |
Test driver and sample client code for the compiled expressions package.
|
UnknownType | |
Utf8Constant |
This class represents a string constant in UTF-8 format, which is used
by the JVM to reference strings.
|
Variable |
Implementation of a user variable which is accessible from expressions.
|
Verifier |
Provides inspection methods that search down the expression tree to
determine operator/operand types.
|
Exception | Description |
---|---|
AmbiguousSymbolException |
Exception thrown when a lookup of a function or variable is ambiguous.
|
CompilerException |
Exception thrown during
Compiler operations. |
ExpressionException |
Exception thrown during
ExpressionCompiler operations. |
SymbolException |
Exception indicating an error relating to symbol resolution processing.
|
UnresolvedSymbolException |
This exception should be thrown by classes which implement the
SymbolResolver interface, when a variable is encountered which cannot
be resolved. |
Author |
Eric Faulhaber |
Modification Date |
July 4, 2005 |
Access Control |
CONFIDENTIAL |
expr
package was created to address performance
issues with
often-evaluated runtime expressions. The expression engine implemented
in this package dynamically compiles a runtime
expression's logic directly into Java bytecode instructions to create a
Java object which is well suited to execute the expression repeatedly
in
performance critical situations. The object uses callback libraries and variables
to
interact with client code and to allow client code to provide extended
services to the user via the expression engine. Client code must
supply a symbol
resolver implementation for this purpose.Expression
class, and are transparent to client code. Client code simply
prepares an instance of this class using an expression string in infix
notation and a symbol resolver object, then invokes a method to execute
the expression. Internally, the infix expression is submitted to
the expression compiler for
"just-in-time" compilation the first time the expression is to be
executed. The compiler parses the expression, assembles the
proper bytecode instructions, compiles a Java class, loads it into the
current Java virtual machine, instantiates it, and executes it using a
well-known method invocation convention. The compiled expression
instance is cached
such that subsequent requests for an instance of that
expression object can skip the parsing, assembly, compilation, class
loading and instantiation steps entirely and simply return the existing
instance immediately.(A
+ B == C * 2) OR (A < D)
') into tokens.A B + C 2 * == A D < OR
'), or into a similarly
structured tree.AND
or OR
conjunction operators, the above approach does
not optimize out unnecessary processing if a result can be determined
early in the expression's execution. Every sub-operation of the
expression is evaluated to determine the final result. Consider
that
the right side of a compound expression using OR
can be ignored completely if the left side evaluates to true
,
and that
the right side of an compound expression using AND
can be
ignored if the left side evaluates to false
.java.lang.Integer
can be
assigned an int
value directly in an expression; a
method which requires a primitive boolean
parameter may
be invoked from an expression which passes a java.lang.Boolean
object as that parameter. The expression compiler detects these
conditions, determines that these fixups are required, and compiles
instructions accordingly to perform the proper wrapping and unwrapping
of data at expression execution time.null
before they are dereferenced by the expression, in order to avoid the
expression from throwing a NullPointerException
. If
an object reference involved in some boolean operation evaluates to null
,
that operation will always evaluate to false
. On
the other hand, if an object reference is passed as a parameter to a
method, it is not first
checked against null
, as the compiler can not presume in
this case that a null
parameter to a method is invalid.foo
has
methods, int getBar()
and void setBar(int)
,
they can be referred to respectively, in expressions as follows: foo.bar
> 10
and foo.bar = 55
.=
).
This expression always returns null
. Thus, inline
or nested assignment operations are not possible. Examples of an
assignment expression are:myVar = 10
myVar = a + b
myVar = null
myVar = foo.getBar()
myVar = a > b
7
true
myVar == a + b
a > b
a * b + 5
foo.getBar()
<class name>.<static
member name>
are not supported. For method invocation,
one must either have an object instance upon which to invoke a method
with the dot operator (.
), or one must be invoking a user
function in a registered callback library.#
). For example,
given a variable myInteger
of type java.lang.Integer
,
an object foo
, and a method of foo
with
signature Object getIntegerAsObject()
, which is known to
return a java.lang.Integer
(widened to a java.lang.Object
),
the following cast would be appropriate: myInteger =
#(java.lang.Integer) foo.integerAsObject
(myVar = a + b) > 24
, because the assignment to myVar
occurs inline, whereas the greater than comparison is made at the top
level of the expression.new
keyword.
There is no direct support in the expression engine for the
construction of new object instances. Object references are
available only from callback library methods (user
functions) and from variable references. Any support for the
construction of new object instances must be provided by the
application code which uses the expression engine.java.util.List
and its implementation variants.'Hello World'
) or double quotes (e.g., "Hello
World"
), so long as the same character is used on both ends of
the string. Note that it is possible to mix and match the
delimiter for different
strings within the same expression. However, this is bad form and
should be avoided unless it is necessary to escape one delimiter, and
then the other, from within the same expression.char
literals. Primitive characters are not supported within
expressions as literals; any single character inside single
quotes is instead interpreted as a string of length one. However,
values of type char
may be returned from method calls and
passed as parameters to other methods, just as any other primitive or
object. For instance, the following expression is valid: string1.indexOf(string2.charAt(7))
>= 0
"Hello World".length()
. Instead, intermediate
storage of a string literal in a variable is necessary to perform this
type of dereferencing.new
(see above)instanceof
+
)?:
)++
)--
)+=
, -=
,
*=
, /=
, %=
, |=
,
&=
, ^=
, >>=
, <<=
,
>>>=
)reset
method is invoked
(including during Variable
construction), and the
expression's result becomes the new referent of the variable. If
no initializer expression
is provided, the variable is initialized to null
.Variable
class. An instance of
this class is created only when a variable is registered with the symbol resolver; this class cannot
be instantiated directly. Variables optionally may be registered
as read-only. In this
mode, expressions which reference a variable may access the referent of
a variable, but may not change it. This restriction holds only
for variable access from within expressions, but not for programmatic
manipulation of a Variable
object. The latter is
always permitted, regardless of the read-only state of the variable.myVar
has been registered as
type Bar
, the expressionmyVar = foo.getBar()
myVar
. The expressionmyVar.doSomething()
doSomething()
on myVar
's
referent, an instance of Bar
. Note that any
assignment, access, comparison, invocation, etc. against a variable
always is applied against the variable's referent. Because of the
automatic null checking, auto-boxing, and automatic type conversion
features of the expression engine, variables which represent primitive
values using wrapper objects can interact naturally with primitive
literals. For example, given a variable num
,
registered as type java.lang.Integer
, the following
expression is perfectly valid, despite the apparent type mismatch:num >= 100.5
num
's referent is checked against null
(auto null checking), then unwrapped to an int
(auto-boxing), then widened to a double
(auto-conversion)
before the comparison takes place (in the event num
's
referent is null
, this expression would return false
).void
.someUserFunction(myVar, 39,
constant)
myLib.someUserFunction(myVar, 39,
constant)
AmbiguousSymbolException
is thrown at expression compile time.int
can in fact accept any
numeric primitive or numeric primitive wrapper object. Primitive
unwrapping operations and narrowing conversions will occur as necessary
to allow the user function to resolve at expression compile time.
Note that this behavior may result in ambiguity among user function
alternatives which may require a typecast to eliminate. Consider,
for instance two alternatives of a method within callback library myLib
which implement different user functions:public void foo(long num)
public void foo(java.lang.Integer num)
myLib.foo(25)
myLib.foo(#(long) 25)
25
to a
long
, ormyLib.foo(#(java.lang.Integer) 25)
25
to be wrapped into an
instance of java.lang.Integer
. Note that where
there is no such ambiguity, the typecast is unnecessary.java.lang.Object
s
as its final parameter. For instance:public void
myVarArgFunction(Object[] args)
{
// process variable arguments
for (int i = 0; i < args.length; i++)
{
...
}
}
public void
myOtherVarArgFunction(int len, String text, Object[] args)
{
// process required arguments 'len' and 'text'
...
// process variable arguments
for (int i = 0; i < args.length; i++)
{
...
}
}
NullPointerException
or ClassCastException
may be thrown by the backing method
if a null
argument is dereferenced or an argument is
assumed to be an incorrect type, respectively:
myLib.myVarArgFunction()
myLib.
myVarArgFunction(1, 2, 3)
myLib.
myVarArgFunction(1, myVar)
myLib.
myOtherVarArgFunction(10, 'Hello World')
myLib.
myOtherVarArgFunction(5, 'Some text',
vararg1, vararg2)
.
)
operator. This concept differs from user function invocation in
that it requires no explicit registration of a callback library of
backing methods with the symbol resolver, but it always requires an
object instance upon which to apply the invocation (even for static
methods). Thus, an unqualified method invocation will fail to
compile, since unlike Java, the expression engine provides is no
implicit this
reference. Likewise, a static
method invocation qualified by a class name will fail to compile, since
there is no implicit class resolution.<object
reference>.<method name>([param1 [, ...]])
string1
of type java.lang.String
,
initialized to "Hello World"
, the following represents a
valid invocation of a java.lang.String
method:string1.indexOf('He')
0
upon execution.
Supported OperatorsThe set of operators which may be used within expressions is listed in the table below. Operators in this table are listed in order of their precedence, from those evaluated first to those evaluated last. Operators which have the same precedence are grouped together. When evaluating operations whose operators have the same precedence, operations are performed in the order in which they appear, from left to right. Parentheses (()) may be used to group operations which must be evaluated in a different order.
Precedence | Symbol | Type | Unary/Binary | Operation Performed |
|
|
Logical | Unary | Logical complement |
|
|
Bitwise | Binary | Bitwise complement |
3 |
- |
Arithmetic |
Unary |
Negation |
|
|
Arithmetic | Binary | Multiplication |
|
Arithmetic | Binary | Division | |
|
Arithmetic | Binary | Remainder | |
|
|
Arithmetic | Binary | Addition |
|
Arithmetic | Binary | Subtraction | |
6 |
|
Bitwise | Binary | Left shift |
|
Bitwise | Binary | Right shift w/ sign extension | |
|
Bitwise | Binary | Right shift w/ zero extension | |
7 |
|
Logical | Binary | Is less than |
|
Logical | Binary | Is less than or equal to | |
|
Logical | Binary | Is greater than | |
|
Logical | Binary | Is greater than or equal to | |
8 |
|
Logical | Binary | Is equal to |
|
Logical | Binary | Is not equal to | |
|
|
Bitwise | Binary | Bitwise AND |
|
|
Bitwise | Binary | Bitwise XOR |
|
|
Bitwise | Binary | Bitwise OR |
|
|
Logical | Binary | Conditional AND |
|
|
Logical | Binary | Conditional OR |
SymbolResolver
interface.SymbolResolver
object. This
object will be
called:SymbolResolver
implementation supplied by client
code is responsible for providing the correct substitution values based
upon the context of the application at the time it executes an
expression; the expression object itself has no awareness of the
application's current state. The TestDriver
class is a sample implementation of the SymbolResolver
interface. It is discussed in greater detail below.stderr
, followed by an ExpressionException
thrown
at compile time. Client code generally should not throw an exception
from the SymbolResolver
resolveXXX
methods; the subclasses of CompiledExpression
do not
have exception handlers, as these are expensive constructs. Any
exception thrown by client code during symbol resolution will
propagate up the call stack back to the client code which called ArithmeticExpression.compute()
or LogicalExpression.evaluate()
.null
should be returned from a SymbolResolver
resolveXXX
method. A null
return is handled differently by ArithmeticExpression
and LogicalExpression
objects, as described below.null
is returned from a variable resolver
callback method, ArithmeticExpression
throws an UnresolvedSymbolException
.null
values
returned by the variable resolver. However, the implication of this
leniency is that unexpected results may occur. Consider the following
expression:
MYVAR == 10
MYVAR != null and MYVAR == 10
MYVAR != 10
MYVAR
cannot be
resolved at evaluation time. This may not be as immediately intuitive
as
the first example, but this behavior is consistent. This is because the
latter expression is interpreted internally by the expression engine as
MYVAR != null and MYVAR != 10
MYVAR != 10
!(MYVAR == 10)
!(MYVAR != null and MYVAR == 10)
MYVAR == null or MYVAR != 10
MYVAR
cannot be resolved. In this event, the right
side
of the expression is ignored and the overall expression returns true.
It is important to keep in mind the subtleties of how unresolved
variables
are handled when considering input expressions.TestDriver
class
represents a trivial application which loads records from a simple
database (implemented as a properties file) and allows expressions to
be executed against these records. Either arithmetic or logical
expressions can be evaluated against any valid range of records defined
in the properties.java.util.SimpleDateFormat
to process date and time string constants. Data and time string
constants are converted by the application to Long
values
(number of milliseconds since midnight, Jan. 1, 1970) for use in
expressions.TestDriver
class serves as an illustrative example of
a VariableResolver
implementation and of an ExpressionCompiler
client. It can be launched from the command line, or may be used
programmatically as a test harness. See TestDriver
's main
method for usage syntax and its class
decription for a sample properties format.Field
Name |
Application-Defined Data Type |
Compiled
Expression Data Type |
Notes |
name |
string |
string |
Employee name |
dob |
date |
long |
Employee date of birth (translates to #millis since 01/01/1970) |
overtime |
double |
double |
Overtime hours for the current
period |
city |
string |
string |
City in which employee works |
union |
boolean |
long |
Employee's union affiliation |
begin |
date |
long |
Time employee's regular work
shift begins (translates to #millis since 01/01/1970) |
end |
date |
long |
Time employee's regular work
shift ends (translates to #millis since 01/01/1970) |
union
union == true
[1] true
[2] true
[3] false
(@now() - dob) / 86400000 / 365.25
[1] 54.908003341413796
[2] 44.55904144317058
[3] 59.077750090215986
@now()
user function in the above
expression. From the compiled expression's point of view, this symbol
is simply a variable reference to be resolved to a numeric value. It is
recognized by the application code (in the implementation of the resolveToLong
method) as having special meaning. As a result, special processing is
invoked to resolve this variable to the current date and time (as a
millisecond value).name == 'Larry' or city == 'St.
Louis'
[1] true
[2] true
[3] false
ExpressionException
being thrown, or in a potentially
incorrect result. The unquoted symbol will not be recognized as a
string constant, but
will instead be treated as a variable, or as an unexpected token,
depending upon the contents of the string constant. For
instance, the results of leaving the quotes off 'Larry'
in the example above results in a valid expression, but Larry
is treated as a variable at runtime, and resolves to null
.
This results in a successful compilation and execution, but the results
are probably not what was intended:[1] false
[2] true
[3] false
'St.
Louis'
in the same expression, the compiler treats this
condition as a fatal error, since the string Louis
is now
interpreted as an unexpected, extra token, which invalidates the
expression.expr
package.TestDriver
example above,
the following expression in infix notation:union and begin < '08:00:00'
union begin '08:00:00' < andThe expression compiler creates a skeleton representation of a Java class file in memory. It generates a unique name for the new class, and sets its superclass to
(variable (variable (constant (binary (binary
operand) operand) operand) operator) operator)
ArithmeticExpression
for numeric
expressions and to LogicalExpression
for boolean
expressions. It creates a default constructor and a stub execution
method (compute()
for numeric, evaluate()
for boolean) for the new class. It is into this method which the
expression's logic will be distilled as Java bytecode instructions.<
)
operator, which pops the constant string '08:00:00'
and
the variable begin
. Once the operator has the needed
operand(s), it examines them to determine what Java bytecode
instructions are required to handle each at runtime.'08:00:00'
represents a
time, which this
application chooses to resolve to a number of milliseconds since
midnight. When the compiler encounters this token, it calls TestDriver
's
resolveConstant
method, which returns a Long
with an internal value of 46800000. It is this simpler numeric
representation which is compiled into the class as a constant, avoiding
the need for the application to interpret the string '08:00:00'
each time the expression is executed.null
check on the callback result are then appended. Finally, whether the
operand is a variable or a constant, instructions are appended to push
the resolved value onto the JVM's runtime operand stack.begin
< '08:00:00'
in this case) is then pushed back onto the
compile time operand stack.and
is encountered, the
compiler pops this code unit and the union
variable
operand off the stack. It recognizes that the code unit operand has
already been processed and only assembles bytecode instructions for the
union
operand and for the logical and
operation. As an optimization, the algorithm which assembles
instructions for the logical and
operation ensures that
if the union
variable resolves to false
at
runtime, it will jump directly to the end of the method to return false
,
skipping the evaluation of the begin < '08:00:00'
portion of the expression entirely.true
or false
) are finally
assembled for the end of the method, branching logic code fixups and
bytecode offset fixups are made, the class' constant pool is indexed,
and the finished class file is written to a byte array in memory for
further class loading and caching. If the expression was compiled in
debug mode, the byte array is written to the file system as a class
file, at a location specified during construction of the ExpressionCompiler
object.Class filename: LE0.class
Magic Number : 0xCAFEBABE
Version : 45.3
This Class : com/goldencode/expr/LE0
Super Class : com/goldencode/expr/LogicalExpression
Access Flags : ACC_PUBLIC
Constant Count: 0x24 (36 dec)
1: <String> begin
2: <String> union
3: <Class> com/goldencode/expr/CompiledExpression
4: <Class> com/goldencode/expr/LE0
5: <Class> com/goldencode/expr/LogicalExpression
6: <Class> com/goldencode/expr/VariableResolver
7: <Class> java/lang/Long
8: <Field> com/goldencode/expr/CompiledExpression.resolverLcom/goldencode/expr/VariableResolver;
9: <Method> com/goldencode/expr/LogicalExpression.<init>()V
A: <Method> java/lang/Long.longValue()J
B: <InterfaceMethod> com/goldencode/expr/VariableResolver.resolveToLong(Ljava/lang/String;)Ljava/lang/Long;
C: <Double> 4.68E7
E: <NameAndType> <init>()V
F: <NameAndType> longValue()J
10: <NameAndType> resolveToLong(Ljava/lang/String;)Ljava/lang/Long;
11: <NameAndType> resolverLcom/goldencode/expr/VariableResolver;
12: <Utf8> ()J
13: <Utf8> ()V
14: <Utf8> ()Z
15: <Utf8> (Ljava/lang/String;)Ljava/lang/Long;
16: <Utf8> <init>
17: <Utf8> Code
18: <Utf8> Lcom/goldencode/expr/VariableResolver;
19: <Utf8> begin
1A: <Utf8> com/goldencode/expr/CompiledExpression
1B: <Utf8> com/goldencode/expr/LE0
1C: <Utf8> com/goldencode/expr/LogicalExpression
1D: <Utf8> com/goldencode/expr/VariableResolver
1E: <Utf8> evaluate
1F: <Utf8> java/lang/Long
20: <Utf8> longValue
21: <Utf8> resolveToLong
22: <Utf8> resolver
23: <Utf8> union
Interface Count: 0
Field Count : 0
Method Count : 2
0: <Method> ACC_PUBLIC <init>()V
<Code> Max stack: 1, Max locals: 1
5 bytes of code:
0000 0x2A <aload_0 >
0001 0xB7 <invokespecial > 0009 [<Method> com/goldencode/expr/LogicalExpression.<init>()V]
0004 0xB1 <return >
1: <Method> ACC_PUBLIC evaluate()Z
<Code> Max stack: 4, Max locals: 4
65 bytes of code:
0000 0x2A <aload_0 >
0001 0xB4 <getfield > 0008 [<Field> com/goldencode/expr/CompiledExpression.resolverLcom/goldencode/expr/VariableResolver;]
0004 0x4C <astore_1 >
0005 0x2B <aload_1 >
0006 0x12 <ldc > 02 [<String> union]
0008 0x4D <astore_2 >
0009 0x2C <aload_2 >
000A 0xB9 <invokeinterface> 000B [<InterfaceMethod> com/goldencode/expr/VariableResolver.resolveToLong(Ljava/lang/String;)Ljava/lang/Long;] 02 00
000F 0x4E <astore_3 >
0010 0x2D <aload_3 >
0011 0xC7 <ifnonnull > 0006 [dest:0017]
0014 0xA7 <goto > 0029 [dest:003D]
0017 0x2D <aload_3 >
0018 0xB6 <invokevirtual > 000A [<Method> java/lang/Long.longValue()J]
001B 0x88 <l2i >
001C 0x99 <ifeq > 0021 [dest:003D]
001F 0x2B <aload_1 >
0020 0x12 <ldc > 01 [<String> begin]
0022 0x4D <astore_2 >
0023 0x2C <aload_2 >
0024 0xB9 <invokeinterface> 000B [<InterfaceMethod> com/goldencode/expr/VariableResolver.resolveToLong(Ljava/lang/String;)Ljava/lang/Long;] 02 00
0029 0x4E <astore_3 >
002A 0x2D <aload_3 >
002B 0xC7 <ifnonnull > 0006 [dest:0031]
002E 0xA7 <goto > 000F [dest:003D]
0031 0x2D <aload_3 >
0032 0xB6 <invokevirtual > 000A [<Method> java/lang/Long.longValue()J]
0035 0x8A <l2d >
0036 0x14 <ldc2_w > 000C [<Double> 4.68E7]
0039 0x98 <dcmpg >
003A 0x9B <iflt > 0005 [dest:003F]
003D 0x03 <iconst_0 >
003E 0xAC <ireturn >
003F 0x04 <iconst_1 >
0040 0xAC <ireturn >
Attribute Count: 0
expr
package has a number of known limitations. Some
are the result
of conscious design decisions, since the overriding concern in the
development of this package was high performance. Others are bugs or
unwelcome side effects of the implementation which may be addressed
with future development.ArithmeticExpression.compute()
and LogicalExpression.evaluate()
methods
are not synchronized, as synchronization is expensive and is not needed
in many use cases. In any case, synchronization only at the level of
these methods would not be enough to ensure thread safety, since the VariableResolver
itself is the most important component whose state must be synchronized
to ensure the integrity of an expression result. Where it is necessary
at all, synchronization must be done at the application level.