Kenpali Semantic Specification

The Kenpali Code Specification describes the syntax of Kenpali code, while the Kenpali JSON Specification describes basic evaluation rules. This document gets much deeper into the behaviour of Kenpali programs, specifying exactly how a correct Kenpali implementation must handle various interactions and corner cases.

Names

It’s an error to declare the same name more than once in the same block.

A block creates a scope; all names defined within it are only accessible within it.

But expressions in a block can reference names defined in a containing block.

A block can define a name that duplicates one in an enclosing block. References to that name evaluate to the innermost accessible version of the name—the inner name shadows the outer one.

Name definitions are processed in the order in which they appear in the block. If a name reference is evaluated before the name’s definition has been processed, a runtime error occurs.

However, names are in scope across the entire block in which they are defined. A name still shadows an outer definition, even if it hasn’t been assigned yet.

Strings

Strings in Kenpali must be treated as sequences of Unicode code points, regardless of how they are actually stored. For example, the length of a string is the number of code points it contains, not the number of bytes or some other unit (like UTF-16 code units).

Similarly, string indexing extracts the code point at the given position.

This requirement usually makes string length and indexing linear-time operations, so they should generally be avoided on arbitrary input strings. For example, the following function works, but is very slow if the string is long.

To iterate over the characters, use toStream instead.

String literals can use any of the escape sequences that are valid in JSON.

If a string literal contains invalid escape sequences, a runtime error occurs.

Arrays

Arrays can freely mix different types of elements.

Arrays can be nested to arbitrary depth.

It’s an error to destructure an array with an object pattern.

Excess elements in an array pattern are ignored.

Missing elements in an array pattern cause a runtime error.

The _ pattern can be used to explicitly ignore elements.

Array destructuring can be nested.

Nested patterns can have default values.

A rest pattern can itself be destructured.

Objects

Objects can freely mix different types of values.

Objects can be nested to arbitrary depth.

It’s an error to destructure an object with an array pattern.

Object destructuring can be nested.

Nested patterns can have default values.

The keys in an object pattern can be arbitrary expressions evaluated at runtime. But if the expression happens to be a single name, it must be enclosed in parentheses to avoid being treated as a property name.

Property access can be done directly on an object expression.

Streams

Streams can be indexed like arrays.

Streams can be indexed with negative numbers if finite.

Streams can be destructured like arrays.

Streams can be destructured with rest patterns, and if the rest pattern is the last element, the name it binds contains a stream of the remaining values.

Streams can be destructured with a middle rest pattern if finite, with the name it binds containing an array of the remaining values.

Streams can be spread like arrays.

If a stream references mutable instances, each iteration sees the values of those instances when that iteration is first demanded by a consumer of the stream. These values are then locked in for subsequent traversals, even if the instances are subsequently mutated.

The stream mechanism must be implemented so that it can iterate over an arbitrarily large number of values without causing a stack overflow.

If a stream contains an instance with a custom display implementation, it uses that implementation for its own display.

Defining and Calling Functions

Only functions can be called. Calling any other value causes a runtime error.

A zero-parameter function can be defined and called with no arguments.

A function can have positional and named parameters, with corresponding arguments passed in the same way.

Excess positional arguments are ignored.

Excess named arguments are ignored.

Missing positional arguments cause a runtime error.

Missing named arguments cause a runtime error.

Arguments can be explicitly ignored using _, rather than having to be given a name that is never used.

Positional and named parameters are strictly separated; trying to pass a positional argument to a named parameter or vice versa causes a runtime error.

Interleaving named and positional parameters is unconventional but legal.

So is interleaving named and positional arguments.

If a positional parameter has a default value, but a corresponding argument is supplied, the default value is ignored.

If a positional parameter has a default value, but no corresponding argument is supplied, the default value is used.

Default values can reference names defined in an enclosing scope.

If the default value is a mutable instance, each invocation of the function sees a new instance—the default expression is evaluated anew each time.

If a function has multiple optional positional parameters, the default values fill in any missing arguments.

A positional rest parameter collects all remaining positional arguments into an array.

Ordinary positional parameters can go before and after a positional rest parameter.

Arrays can be spread to fill positional parameters.

A named rest parameter collects all remaining named arguments into an object.

Ordinary named parameters can go before and after a named rest parameter.

Objects can be spread to fill named parameters.

Defining multiple rest parameters of the same type is ambiguous and causes a runtime error.

A rest parameter can go between ordinary parameters with default values, with additional parameters filling in the optional parameters first and then the rest parameter.

Destructuring patterns can be used in parameter lists.

Destructuring patterns in parameter lists can have default values.

Destructuring patterns in parameter lists can have rest elements.

A function can reference names defined in an enclosing scope.

A function can capture a name defined below it, as long as the function isn’t called until after the definition has been processed.

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. In other words, functions must capture names that were in scope when they were defined.

In this example, x has already gone out of scope by the time baz is declared, so baz can’t just capture x at declaration time.

Names that are in scope when the function is called don’t leak into the function body.

Indexing

Strings must be indexed with numbers.

Arrays can be indexed with positive numbers, counting from the beginning of the array.

Arrays can be indexed with negative numbers, counting from the end of the array.

Arrays must be indexed with numbers.

Indexing an array with an out-of-bounds index causes a runtime error.

Objects can be indexed with strings to retrieve the value of a property.

Objects can only be indexed with strings.

If the property is not in the object, a runtime error occurs.

Indexing non-indexable values causes a runtime error.

Sets and Maps

Instances can be used as set members, but they are compared by identity, not by value.

A set’s has method can be passed as a callback.

Errors

If an expression throws an error, that error propagates outward through enclosing expressions, aborting further evaluation.

After an error is caught, new errors can be thrown by code outside the try function.

After an error is caught, new errors can be thrown by the onError handler.

After an error is caught, new errors can be thrown by the onSuccess handler.

Errors capture the names of the functions that are unwound as they propagate.

Errors that propagate through platform functions capture their names.

Variables

Instances created separately don’t share state.