Kenpali JSON Specification
Literals
A literal expression has the form {"literal": <value>}
. The value can be null
, true
, false
, or any valid JSON string or number.
Examples:
Names
Names are defined using a defining expression, which has the form {"defining": <definitions>, "result": <result>}
. The <definitions>
node must be an array of pairs, where the first element of each pair is a name pattern and the second element is any expression. The <result>
node can be any expression. The value of the defining expression is the value of the <result>
node.
The name pattern is usually a string, but other pattern types are possible—see Arrays and Objects for more complex types of name patterns.
A name expression evaluates to the value previously assigned to the specified name. It has the form {"name": <name>}
, where the <name>
is a string.
Examples:
Names are defined in the order they appear in the defining expression. It’s an error to reference a name before it is defined.
It’s an error to declare the same name more than once in the same defining expression.
A defining expression creates a scope; all names defined within it are only accessible within it.
A defining expression can define a name that duplicates one in an enclosing defining expression. References to that name evaluate to the innermost accessible version of the name—the inner name shadows the outer one.
The name pattern can be null
, which creates an expression statement. An expression statement evaluates the expression (usually for its side effects) and then discards the result.
Arrays
An array expression has the form {"array": <elements>}
. The <elements>
node is a JSON array containing expressions, which are evaluated to obtain the array’s elements.
Instead of an expression, any of the array’s elements can be a spread instead. A spread has the form {"spread": <value>}
, where <value>
is any expression. The <value>
expression is expected to evaluate to a sequence, whose elements are added to the array.
When defining names, an array pattern can be used to extract individual elements from an array and assign them to names. An array pattern has the form {"arrayPattern": <names>}
, where <names>
is an array of name patterns.
One of the elements of an array pattern can be a rest pattern, of the form {"rest": <name>}
. The <name>
is bound to an array containing whatever elements are left over after the other name patterns have taken theirs.
Objects
An object expression has the form {"object": <entries>}
. The <entries>
node is a JSON array containing name-value pairs. In each pair, the first element can be a string or an expression evaluating to a string, while the second element can be any expression.
Instead of a name-value pair, any of the object’s elements can be a spread instead. A spread has the form {"spread": <value>}
, where <value>
is any expression. The <value>
expression is expected to evaluate to an object, whose entries are added to the containing object.
When defining names, an object pattern can be used to extract property values from an object and assign them to names. An object pattern has the form {"objectPattern": <names>}
, where <names>
is an array of property names to extract.
One of the elements of an object pattern can be a rest pattern, of the form {"rest": <name>}
. The <name>
is bound to an object containing whatever properties aren’t mentioned explicitly in the object pattern.
Any of an object pattern’s elements can be an alias pattern, of the form {"name": <pattern>, "property": <property-name>}
. The property <property-name>
is extracted and bound to <pattern>
, which can be any name pattern.
Defining and Calling Functions
Functions are defined using a given expression, which has the form {"given": <param-spec>, "result": <body>}
. The <param-spec>
is an object with two optional properties: params
, giving the function’s positional parameters, with the same format as the value in an array pattern; and namedParams
, giving the function’s named parameters, with the same format as the value in an object pattern. The <body>
can be any expression, and it defines what the function returns.
Functions are called using a calling expression, which has the form {"calling": <function>, "args": <pos-arg-spec>, "namedArgs": <named-arg-spec>}
. Both args
and namedArgs
are optional. The <function>
must be an expression, and its result is the function to call. The <pos-arg-spec>
indicates the positional arguments to pass to the function, with the same format as the value in an array expression; the <named-arg-spec>
indicates the named arguments to pass to the function, with the same format as the value in an object expression.
A function can reference names defined in an enclosing scope.
A function can reference names that were in scope when the function was defined, even if those names are out of scope when the function is called.
On the other hand, names that are in scope when the function is called don’t leak into the function body.
Indexing
An indexing expression extracts the value at a specific index from a collection. It has the form {"indexing": <collection>, "at": <index>}
, where both <collection>
and <index>
are expressions.
Errors
If an expression throws an error, that error propagates outward through enclosing expressions, aborting further evaluation.
But if the error encounters a catching expression, the catching expression returns the error as a value, stopping its propagation. A catching expression has the form {"catching": <expression>}
.