JFXG Language Description

JFXG (Java Functor eXpression Grammar) is a language intended to make it easy to create arbitrarily complex functors.

JFXG is, as its name implies, based on the subset of Java that describes expressions. The easy way to think of JFXG is that it supports (or will support) anything that can appear on the right side of a Java assignment statement. Currently, there are a few exceptions to this simple description, but the features that are supported are compelling enough to be of considerable value.

JFXG has no standalone form: it is intended to be used as an embedded language within Java source code, XML-based markup text, key/value properties files, etc. JFXG is not intended or expected to scale up to the level of anything that is typically considered a scripting language: there are no keywords in the language for flow-control, no provisions for subprograms, no provisions for lexical scoping, no declaration of variables(*FUTURE*), no classes/types/interfaces/objects, and no distinction between primitives and objects.

For want of a better term, we'll use the word 'scriptlet' to refer to the entire body of a stream that is passed to the JFXG parser. A scriptlet contains one or more expressions, each of which is translated into a jga functor. The complexity of the functor is largely a function of the complexity of the expression(s): for very simple expressions, it is generally as easy to work directly with the jga functor api as it is to write, parse, and store the scriptlet. For expressions involving long math expressions, conditional expressions, method calls, etc, it is far easier to write the expression than it is to describe the corresponding functor. Performance of jga functors should scale very closely with the performance of similarly structured java expressions: the code path through the evaluation of a typical functor is the same as the execution of the java expression, with the addition of very slight method call overhead for each functor in the resulting structure (of course, calling methods and accessing members of an Object in a JFXG expression incurrs the overhead associated with reflection).

  1. Lexical Structure

  2. The character set is necessarily constrained to that supported by the environment within which the scriptlet is embedded. In general, the characters allowed within JFXG are the same as java: outside of String and Character literals, everything is pretty much ASCII, but within String and Character literals, embedded unicode escapes will be interpreted.

    Whitespace characters are interpreted as described in ( JLS3.6 and JLS3.4)

    Both familiar forms of Java commenting are supported (TODO: verify): ie, end of line comments starting with '//' and embedded comments between '/*' '*/' delimiters.

    The structure of words is the same as java, described in ( JLS3.8). The following java keywords are supported by JFXG:

    true
    boolean literal value
    false
    boolean literal value
    null
    object literal value
    this
    object literal value
    new
    object creation operator
    instanceof
    object type operator
    class
    class literal (ie, Foo.class)

    Additionally, two variable names are reserved: x and y, depending on which entry point in the JFXG parser is used to interpret the input string. These act as place holder in the expression for one or two runtime arguments. (NOTE: in some future revision, this will likely be changed to a more java-typical {n} syntax, and grow to support an arbitrary number of arguments. For now, up to two runtime arguments are allowed).

    (*DIFF*)The this keyword is a placeholder for a simple binding of the parser to its enclosing environment: the object to which 'this' refers must be set via the parser API call bindThis(Object) prior to the parser's invocation.

    Words found in the input that are not reserved in some way are interpreted as class, method, or field names. As in java, the names of all classes in the java.lang package are imported by default, and require no additional qualification. Additional classnames, static public fieldnames, and static public methodnames may be imported via the parser API: if not so imported, then classes must be fully qualified (ie, "java.util.Date") to be used, and static methods and fields must be qualified with a valid class reference. Object fields and methods may be used when qualified with an object reference.

    The parser API also defines several methods that are invoked during when an unrecognized word is encountered, allowing the parser to be extended to support specialized syntax.

  3. Types & Literals

    1. Object

    2. Everything in JFXG is a Java Object -- there are no primitive values. All of the methods associated with java.lang.Object can be called directly (although wait() and notify() should be used rarely, if at all).

      The keyword 'null' describes the null value.

      All Objects can be compared for equality. (*DIFF*) Unlike Java, there is no notion of reference equality -- essentially, all use of the '==' and '!=' operators work via the left operand's .equals() method with one additional cavaet: null values are always equal to each other and never equal to non-null values. Thus, the expression "null == 0" is false, while "null == null" is true.

      All Object can be tested with the instanceof operator, using the same syntax as Java (JLS15.20.2). (*DIFF*) Unlike Java, there is no parse-time evaluation of the exression: a syntactically correct instanceof expression will not result in a parse-time exception. For example, the expression:

      "foo" instanceof Integer;
            

      ... would be rejected by the javac compiler (returning a compile-time error) while it is accepted the JFXG parser (its value is, of course, false) (*TEST*)

      As with Java, all Objects may be used within String concatenations: non-String Objects will be converted to String via the toString() method, either at parse-time (in the case of literal values) or at run-time (in the case of non-literal values).

    3. Boolean

    4. The keywords 'true' and 'false' are Boolean literals (JLS3.10.3)

      In addition to operations provided by Object, Boolean values may be used with the standard boolean operators (! & && | || ^). Also, a boolean value is required as the first operand of the conditional operator (?:).

    5. Numbers

    6. All Java implementations of Number are (or can be) supported to varying degrees by JFXG. In most cases, Java's numeric literals, as defined in (JLS3.10.1 and JLS3.10.2) are supported directly. By default, built-in Java Numbers are supported within expressions, even when there is no literal equivalent. Thus, it is possible to use Numbers within arithmetic expressions, but they may have to be created using a constructor or returned from an expression

      new BigDecimal("1") + new BigDecimal("2")
            

      ... is perfectly acceptible (so long as BigDecimal has been imported via the Parser API).

      The base JFXG parser can be configured to alter numeric literals slightly: the method setUndecoratedDecimal(boolean) controls the interpretation of decimal strings without the 'D' (or 'd') suffix. By default (or when set to false), the parser interprets these strings as they are defined in Java: as Double values. When set to true, the parser interprets such decimal strings as literal instances of java.math.BigDecimal. For example, if this setting is true, the value of ...

      1.0 + 2.0
            

      ... is the BigDecimal value 3.0.

      (*DIFF*) There is no implicit numeric coercion within an expression (*FUTURE*), and no provision for mixed type arithmetic. However, casting of Numbers is allowed between any supported Number implementation (even in cases not allowed in Java, eg, casting between Integer and BigInteger). Both of the operands of any standard binary operation must be of the same type.

      The underlying jga library ("Generic Algorithms for Java") provides a means by which arbitrary Number implementations can be supported in functor expressions. If support has been configured in jga, that support is available to expressions parsed with the JFXG parser.

      In addition to operations provided by Object, Number values may be used with the standard relational (< > <= >=) and arithmetic operators (+ - * /). Integral types (ie, Integer, Long, BigInteger) may be used with modulus (%) and bitwise operators (~ & | << >> >>>).

    7. Characters

    8. Character literals are enclosed in single quotes ('), as in Java (JLS3.10.4). Unicode character literals are supported using the syntax described in (JLS3.3). Escape sequences using the leading backslash syntax are supported, as described in (JLS3.10.6).

      In addition to operations provided by Object, Character values may be used with the standard relational (< > <= >=) and addition and subtraction operators (+ -). Characters in addition and subtraction expressions will be converted to Integer for evaluation of the expression, and the result of the expression is an Integer.

    9. Strings

    10. Strings are sequences of characters enclosed in double quotes ("), as described in (JLS3.10.5). Concatenation of string literals will be performed during the parsing, and concatenation expressions using both string and non-string results are directly supported as described in (JLS3.10.5 & JLS5.4).

  4. Expressions

  5. A single expression may be followed by an optional ';'. Multiple expressions must be separated by ';'. When multiple expressions are given, the value of the last expression is the value of the scriptlet.

    The following operators are supported by JFXG, and their order of precedence is the same as defined in (JLS15)

    =	>	<	!	~	?:
    ==	<=	>=	!=	&&	||
    +	-	*	/	&	|	^	%	<<	>>	>>>        
         

    With respect to (JLS15), it is easier to list what is not supported than to reproduce the list of what is supported by JFXG. Missing from (JLS15) are:

    • Qualified this (JLS15.8.4)
    • Definition of nested classes (including anonymous inner classes)(large parts of JLS15.9: new instances of pre-defined classes may be built using constructor or static factory method calls)
    • Arrays (JLS15.10 & JLS15.13)
    • Field or Method access using the 'super' keyword ( JLS15.11.2 & one case in JLS15.12.1)
    • Postfix expressions (*FUTURE*) (JLS15.14)
    • Prefix increment & decrement operators (*FUTURE*) (JLS15.15.1 & JLS15.15.2)
    • Assignment operators (*FUTURE*) (JLS15.26 & JLS15.27) (re: JLS15.27 -- the semicolon in JFXG behaves much like the comma operator in C and C++)

  6. JFXG Parser API

  7. The last published javadoc version is here

    Much of the public interface of the JFXG Parser API is produced by the JavaCC tool used to convert the grammar file into a working parser class. Many of the public methods are not intended for use by general users. The following is a summary of those methods that are intended for general use.

    Configuration methods

    importClass

    public void importClass(java.lang.Class clasz)
    Imports the given class into the parser. The class will not need to be fully qualified for use in expressions. This will replace any existing imported class that happens to have the same name.

    importClass

    public void importClass(java.lang.String alias,
                            java.lang.Class clasz)
    Imports the given class into the parser under the given alias. The alias may be used in place of the fully qualified name of the class hereafter. This will replace any existing imported class associated with the alias.

    deportClass

    public void deportClass(java.lang.String alias)
    Removes information about the class imported under the given name.

    importStatics

    public void importStatics(java.lang.Class clasz)
    Imports all of the static public methods and members in the given class.

    importField

    public void importField(java.lang.Class clasz,
                            java.lang.String name)
                     throws java.lang.NoSuchFieldException
    Imports the given static member. The member will not need to be qualified with its classname for use in expressions.

    Throws:
    java.lang.NoSuchFieldException - if the named field does not exist
    java.lang.IllegalArgumentException - if the named field is not static

    importField

    public void importField(java.lang.reflect.Field field)
                     throws java.lang.IllegalArgumentException
    Imports the given static member. The member will not need to be qualified with its classname for use in expressions.

    Throws:
    java.lang.IllegalArgumentException - if the field is not static

    getImportedField

    public java.lang.reflect.Field getImportedField(java.lang.String name)
    Returns the imported field with the given name, or null if no such field has been imported.

    importMethod

    public void importMethod(java.lang.Class clasz,
                             java.lang.String name)
                      throws java.lang.NoSuchMethodException
    Imports the given static method(s).

    Throws:
    java.lang.NoSuchMethodException - if the named method does not exist, or if it/they is/are not static

    importMethod

    public void importMethod(java.lang.reflect.Method meth)
    Imports a static method into the parser. The method will not need to be qualified with its classname for use in expressions. Multiple methods may be imported with the same name: the first best fit will be used when the method name is used.

    Throws:
    java.lang.NoSuchMethodException - if the named method does not exist
    java.lang.IllegalArgumentException - if the named method is not static

    importMethod

    public void importMethod(java.lang.String name,
                             java.lang.reflect.Method meth)
    Imports a static method into the parser under the given name. The method will not need to be qualified with its classname for use in expressions. Multiple methods may be imported with the same name: the first best fit will be used when the method name is used.

    Throws:
    java.lang.IllegalArgumentException - if the method is not static

    getImportedMethods

    public java.lang.reflect.Method[] getImportedMethods(java.lang.String name)
    Returns a list of methods with the given name

    bindThis

    public void bindThis(java.lang.Object thisBinding)
    Binds the object to which 'this' refers

    getBoundObject

    protected java.lang.Object getBoundObject()
    Returns the current object to which 'this' refers

    setUndecoratedDecimal

    public void setUndecoratedDecimal(boolean flag)
    Enables/Disables the interpretation of undecorated decimal literals as BigDecimals. When true, an undecorated number containing a decimal, for example 1.50 is interpreted as a BigDecimal literal. When false, the same token is interpreted as a Double, as is the case in standard java

    isUndecoratedDecimal

    public boolean isUndecoratedDecimal()
    When true, an undecorated number containing a decimal, for example 1.50 is interpreted as a BigDecimal literal. When false, the same token is interpreted as a Double, as is the case in standard java

    Parser Entry Points

    parse

    public static Generator parse(java.lang.String str)
                           throws UncheckedParseException
    Parses the string to create a Generator.

    Throws:
    UncheckedParseException

    parseGenerator

    public Generator parseGenerator(java.lang.String str) 
                             throws ParseException
    Parses the string to create a Generator.

    Throws:
    ParseException

    parseGeneratorRef

    protected GeneratorRef parseGeneratorRef()
                                      throws ParseException
    Parses the contents of the current input to create a Generator

    Throws:
    ParseException

    parse

    public static UnaryFunctor parse(java.lang.String str,
                                     java.lang.Class argType)
                              throws UncheckedParseException
    Parses the string to create a UnaryFunctor that takes an argument of the given type.

    Throws:
    UncheckedParseException

    parseUnary

    public UnaryFunctor parseUnary(java.lang.String str,
                                   java.lang.Class argType)
                            throws ParseException
    Parses the string to create a UnaryFunctor that takes an argument of the given type.

    Throws:
    ParseException

    parseUnaryRef

    protected UnaryFunctorRef parseUnaryRef(java.lang.Class argType)
                                     throws ParseException
    Parses the current input to create a UnaryFunctor that takes an argument of the given type.

    Throws:
    ParseException

    parse

    public static BinaryFunctor parse(java.lang.String str,
                                      java.lang.Class arg1Type,
                                      java.lang.Class arg2Type)
                               throws UncheckedParseException
    Parses the string to create a BinaryFunctor that takes arguments of the given types.

    Throws:
    UncheckedParseException

    parseBinary

    public BinaryFunctor parseBinary(java.lang.String str,
                                     java.lang.Class arg1Type,
                                     java.lang.Class arg2Type)
                              throws ParseException
    Parses the string to create a BinaryFunctor that takes arguments of the given types.

    Throws:
    ParseException

    parseBinaryRef

    protected BinaryFunctorRef parseBinaryRef(java.lang.Class arg1Type,
                                              java.lang.Class arg2Type)
                                       throws ParseException
    Parses the current input to create a BinaryFunctor that takes arguments of the given types.

    Throws:
    ParseException

    getReturnType

    public java.lang.Class getReturnType()
    Returns the type of object returned by the last functor parsed.

    Throws:
    java.lang.IllegalStateException - if the parser has not been used or if parsing the last functor resulted in an exception being thrown.

    Extension points

    reservedWord

    protected FunctorRef reservedWord(java.lang.String name)
                               throws ParseException
    Allows for (not necessarily constant) values to be added to the grammar

    Throws:
    ParseException

    reservedField

    protected FunctorRef reservedField(FunctorRef prefix,
                                       java.lang.String name)
                                throws ParseException
    Allows for (not necessarily constant) predefined fields to be added to the grammar

    Throws:
    ParseException

    reservedFunction

    protected FunctorRef reservedFunction(java.lang.String name,
                                          FunctorRef[] args)
                                   throws ParseException
    Allows for function-style names to be added to the grammar.

    Throws:
    ParseException