JX (JSON + Expressions)

Last edited: 14 June 2016

JX is a superset of JSON with additional syntax for dynamically generating data. The use case in mind when designing JX was writing job descriptions for a workflow manager accepting JSON input. For workflows with a large number of rules with similar structure, it is sometimes necessary to write a script in another language like Python to generate the JSON output. It would be desirable to have a special-purpose language that can dynamically generate rules while still bearing a resemblance to JSON, and more importantly, still being readable. JX is much lighter than a full-featured language like Python or JavaScript, and is suited to embedding in other systems like a workflow manager.

Standard JSON syntax is supported, with two important additions: operators and functions. Operators include the usual arithmetic, comparison, and logical operators (e.g. +, <=, &&) and are useful for simple computations. Note that JX is less forgiving than JavaScript with respect to type; values will not be coerced. JX syntax is styled after JavaScript, but does not strictly adhere to JS. While JX can often be interpreted as valid JS, the result is not necessarily the same between the two. This is especially apparent with the type coercions that JS performs. For all operators and functions, invalid invocation (wrong number of arguments, wrong arguments types, etc.) will return an error. The main exception is that integers will be promoted to doubles as needed. Thus JX is a superset of JSON, and (with care) mostly a subset of JS.

"123" + 4
=> Error{"source":"jx_eval","name":"mismatched types","message":"mismatched types for operator","operator":"123"+4,"code":2}

"123" + "4"
=> "1234"

123 + 4
=> 127

JX supports the same basic types as JSON

Note that unlike JSON, JX makes a distinction between integer and floating point values. Unevaluated JX may also contain

but evaluation will produce the plain JSON types above, assuming the evaluation succeeded. If an error occurs, evaluation stops and an error is returned to the caller. Error objects can include additional information indicating where the failure occurred and what the cause was. Details on the syntax and usage of Errors is given in a following section. If a non-error type is returned, then evaluation succeeded and the result contains only plain JSON types. JX expressions are evaluated in a context, which is an object mapping names to arbitrary JX values. On evaluating a symbol, the symbol's name is looked up in the current context. If found, the value from the context is substituted in place of the symbol. If the symbol is not found in the context, an error is returned.

Functions allow for more advanced manipulations on the data. The details of required arguments, return type, side effects, etc. vary, but in general,

In this example, foreach, range, and str are used to generate a list of filenames.

foreach(x, range(4), "input" + str(x) + ".dat")
=> ["input0.dat", "input1.dat", "input2.dat", "input3.dat"]

Functions may be nested or composed as needed, and will produce plain JSON when evaluated. Functions do not need to be given at the top level, and may be used to generate object keys, array elements, operands to JX operators, etc. The following sections give detailed descriptions of the currently supported operators and functions.

Unary Operators

Logical Complement

!Boolean -> Boolean

Computes the logical NOT of the given boolean. Unlike C, integers may not be used as truth values.

Negation

-A -> A

where A = Integer|Double

Computes the additive inverse of its operand.

Positive Prefix

+A -> A

where A = Integer|Double|String

Returns its operand unmodified.

Binary Operators

For complicated expressions, parentheses may be used as grouping symbols. In the absence of parentheses, operators are evaluated left to right in order of precedence. From highest to lowest precedence,

Lookup

A[B] -> C

where A,B = Array,Integer or A,B = Object,String

Gets the item at the given index/key in a collection type. If the key/index is not present, returns null.

Addition

A + A -> A

where A = Integer|Double|String|Array

The behaviour of the addition operator depends on the type of its operands.

Subtraction

A - A -> A

where A = Integer|Double

Computes the difference of its operands.

Multiplication

A * A -> A

where A = Integer|Double

Computes the product of its operands.

Division

A / A -> A

where A = Integer|Double

Computes the quotient of its operands. Division by zero is an error.

Modulo

A % A -> A

where A = Integer|Double

Computes the modulus of its operands. Division by zero is an error.

Conjunction

A && A -> A

where A = Boolean|Integer

The behaviour of the conjunction operator depends on the type of its operands.

Disjunction

Boolean || Boolean -> Boolean

where A = Boolean|Integer

The behaviour of the disjunction operator depends on the type of its operands.

Equality

A == B -> Boolean

where A,B = Null|Boolean|Integer|Double|String|Array

Returns true iff its operands have the same value. All instances of null are considered to be equal. For arrays and objects, equality is checked recursively. Note that if x and y are of incompatible types, x == y returns false.

Inequality

A != B -> Boolean

where A,B = Null|Boolean|Integer|Double|String|Array

Returns true iff its operands have different values. All instances of null are considered to be equal. For arrays and objects, equality is checked recursively. Note that if x and y are of incompatible types, x != y returns true.

Less than

A < A -> Boolean

where A = Integer|Double|String

The behaviour of the less than operator depends on the type of its arguments.

Less than or equal to

A <= A -> Boolean

where A = Integer|Double|String

The behaviour of the less than or equal to operator depends on the type of its arguments.

Greater than

A > A -> Boolean

where A = Integer|Double|String

The behaviour of the greater than operator depends on the type of its arguments.

Greater than or equal to

A >= A -> Boolean

where A = Integer|Double|String and B = Boolean

The behaviour of the greater than or equal to operator depends on the type of its arguments.

Functions

str

str(A) -> String

Returns a string representation of the given value.

range

range(A) -> Array
range(A, A[, A]) -> Array

where A = Integer

Returns an array of integers ranging over the given values. This function is a reimplementation of Python's range function. range has two forms,

range(stop)
range(start, stop[, step])

The first form returns an array of integers from zero to stop (exclusive).

range(10)
=> [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]

The second form returns an array of integers ranging from start (inclusive) to stop (exclusive).

range(3, 7)
=> [3, 4, 5, 6]

range(7, 3)
=> []

The second form also allows an optional third parameter, step. If not given, step defaults to one.

range(-1, 10, 2)
=> [-1,1,3,5,7,9]

range(5,0,-1)
=> [5, 4, 3, 2, 1]

Calling with step = 0 is an error.

foreach

foreach(Symbol, A, B) -> C

where and A = [_] and C = [B]

foreach provides similar functionality to the map function in other languages, but has reduced semantics to limit recursion. foreach requires three arguments.

foreach(var, items, body)

Evaluating foreach results in an Array of the same length as items, or null on invalid arguments. We bind var to each value in items in turn, and evaluate body with this extra binding.

foreach(x, range(4), x * 2)
=> [0, 2, 4, 6]

foreach(x, [-1, 2.2, "foo"], str(x))
=> ["-1", "2.2", "foo"]

join

join(A[, B]) -> String

where A = [String] and B = String

Takes an array of strings, and concatenates them into a single string. If the optional second argument is given, it is used as the separator, or " " otherwise.

join(["1", "2", "3"])
=> "1 2 3"

join(["a", "b", "c"], ", ")
=> "a, b, c"

let

let(A, B) -> C

where A = Object

Evaluates an expression with local name bindings. The first argument is an object whose keys will be bound variable names when evaluating the second argument. Names specified in a local scope may shadow names in the enclosing scope.

let({"x": 10}, 1 + x)
=> 11

let({"x": 10, "y": 20}, let({"x": 1}, x + y))
=> 21

dbg

dbg(A) -> A

This function simply returns its argument. As a side effect, it prints its argument before and after evaluation to stderr. This function might be useful to debug evaluation issues, as it lets you see the evaluation of arbitrary parts of an expression.

join(foreach(x, range(5), x))
=> Error{"source":"jx_eval","name":"invalid arguments","message":"joined items must be strings","func":join(foreach(x,range(5),x)),"code":6}

join(dbg(foreach(x, range(5), x)))
+ dbg  in: foreach(x,range(5),x)
+ dbg out: [0,1,2,3,4]

Recalling that join takes an array of Strings, not Integers, we should use

join(dbg(foreach(x, range(5), str(x))))
+ dbg  in: foreach(x,range(5),str(x))
+ dbg out: ["0","1","2","3","4"]
=> "0 1 2 3 4"

Errors

JX has a special type, Error, to indicate some kind of failure or exceptional condition. If a function or operator is misapplied, jx_eval will return an error indicating the cause of the failure. While errors are valid JX, they are special in that they have no JSON equivalent. Furthermore, errors do not evaluate in the same way as other types. The additional information in an error is protected from evaluation, so calling jx_eval on an Error simply returns a copy. It is therefore safe to directly include the invalid function/operator in the body of an error. If a function or operator encounters an error as an argument, evaluation is aborted and the Error is returned as the result of evaluation. Thus if an error occurs deeply nested within a structure, the error will be passed up as the result of the entire evaluation. Errors are defined using the keyword Error followed by a body enclosed in curly braces.

Error{"source":"jx_eval","name":"undefined symbol","message":"undefined symbol","context":{"outfile":"results","infile":"mydata","a":true,"b":false,"f":0.5,"g":3.14159,"x":10,"y":20,"list":[100,200,300],"object":{"house":"home"}},"symbol":c,"code":0}

All errors MUST include some special keys with string values.

Errors from "jx_eval" have some additional keys, described below, to aid in debugging. Other sources are free to use any structure for their additional error data.

JX Errors

Errors produced during evaluation of a JX structure all include some common keys.

The following codes and names are used by jx_eval.