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:

# Literal null
{"literal": null}
>> null
# Literal false
{"literal": false}
>> false
# Literal true
{"literal": true}
>> true
# Literal number
{"literal": 1}
>> 1
# Literal string
{"literal": "foobar"}
>> "foobar"

Names

# Binding a name
{
    "defining": [["foo", {"literal": 42}]],
    "result": {"name": "foo"}
}
>> 42
# Binding multiple names
{
    "defining": [
        ["bar", {"literal": 42}],
        ["baz", {"name": "bar"}],
        ["foo", {"name": "baz"}]
    ],
    "result": {"name": "foo"}
}
>> 42
# Name used before assignment
{
    "defining": [
        ["foo", {"name": "baz"}],
        ["bar", {"literal": 42}],
        ["baz", {"name": "bar"}]
    ],
    "result": {"name": "foo"}
}
!! nameUsedBeforeAssignment {"name": "baz"}
# Name declared more than once in the same scope
{
    "defining": [
        ["foo", {"literal": 42}],
        ["foo", {"literal": 97}]
    ],
    "result": {"name": "foo"}
}
!! duplicateName {"name": "foo"}
# Scope
{
    "defining": [
        [
            "foo",
            {
                "defining": [
                    ["bar", {"literal": 42}]
                ],
                "result": {"literal": null}
            }
        ]
    ],
    "result": {"name": "bar"}
}
!! nameNotDefined {"name": "bar"}
# A name from an enclosing scope
{
    "defining": [
        ["foo", {"literal": 42}]
    ],
    "result": {
        "defining": [
            ["bar", {"literal": 73}]
        ],
        "result": {"name": "foo"}
    }
}
>> 42
# Shadowing
{
    "defining": [
        ["foo", {"literal": 42}]
    ],
    "result": {
        "defining": [
            ["foo", {"literal": 73}]
        ],
        "result": {"name": "foo"}
    }
}
>> 73
# Expression statements
{
    "defining": [
        [null, {"literal": 42}]
    ],
    "result": {"literal": 73}
}
>> 73

Arrays

# Empty array
{"array": []}
>> []
# Array of literals
{"array": [{"literal": 1}, {"literal": 2}, {"literal": 3}]}
>> [1, 2, 3]
# Array with elements of mixed types
{"array": [{"literal": null}, {"literal": 1}, {"literal": "foo"}]}
>> [null, 1, "foo"]
# Nested arrays
{"array": [{"array": [{"literal": 1}]}]}
>> [[1]]
# Array containing an expression to evaluate
{
    "defining": [["foo", {"literal": 42}]],
    "result": {"array": [{"name": "foo"}]}
}
>> [42]
# Array with spread
{
    "array": [
        {"literal": 42},
        {
            "spread": {
                "array": [
                    {"literal": 1},
                    {"literal": 2},
                    {"literal": 3}
                ]
            }
        },
        {"literal": 97}
    ]
}
>> [42, 1, 2, 3, 97]
# Array destructuring
{
    "defining": [
        [
            {"arrayPattern": ["foo", "bar"]},
            {
                "array": [
                    {"literal": 42},
                    {"literal": 97}
                ]
            }
        ]
    ],
    "result": {
        "array": [
            {"name": "bar"},
            {"name": "bar"},
            {"name": "foo"}
        ]
    }
}
>> [97, 97, 42]
# Array destructuring with rest
{
    "defining": [
        [
            {"arrayPattern": ["foo", {"rest": "bar"}, "baz"]},
            {
                "array": [
                    {"literal": 42},
                    {"literal": 1},
                    {"literal": 2},
                    {"literal": 3},
                    {"literal": 97}
                ]
            }
        ]
    ],
    "result": {
        "array": [
            {"name": "baz"},
            {"name": "foo"},
            {"name": "bar"}
        ]
    }
}
>> [97, 42, [1, 2, 3]]

Objects

# Empty object
{"object": []}
>> {}
# Object with literal values
{
    "object": [
        ["foo", {"literal": "bar"}],
        ["spam", {"literal": "eggs"}]
    ]
}
>> {foo: "bar", spam: "eggs"}
# Object with explicit literal keys
{
    "object": [
        [{"literal": "foo"}, {"literal": "bar"}],
        [{"literal": "spam"}, {"literal": "eggs"}]
    ]
}
>> {foo: "bar", spam: "eggs"}
# Object with values of mixed types
{
    "object": [
        ["foo", {"literal": null}],
        ["bar", {"literal": 1}],
        ["baz", {"array": [{"literal": 2}]}]
    ]
}
>> {foo: null, bar: 1, baz: [2]}
# Nested objects
{
    "object": [
        [
            "foo",
            {
                "object": [["bar", {"literal": "baz"}]]
            }
        ]
    ]
}
>> {foo: {bar: "baz"}}
# Object with expression keys and values
{
    "defining": [
        ["key", {"literal": "foo"}],
        ["value", {"literal": 42}]
    ],
    "result": {
        "object": [
            [{"name": "key"}, {"name": "value"}]
        ]
    }
}
>> {foo: 42}
# Object with spread
{
    "object": [
        ["answer", {"literal": 42}],
        {
            "spread": {
                "object": [
                    ["bar", {"literal": 1}],
                    ["baz", {"literal": 2}]
                ]
            }
        },
        ["question", {"literal": 69}]
    ]
}
>> {answer: 42, bar: 1, baz: 2, question: 69}
# Object destructuring
{
    "defining": [
        [
            {"objectPattern": ["foo", "bar"]},
            {
                "object": [
                    ["bar", {"literal": 42}],
                    ["foo", {"literal": 97}]
                ]
            }
        ]
    ],
    "result": {
        "array": [
            {"name": "foo"},
            {"name": "foo"},
            {"name": "bar"}
        ]
    }
}
>> [97, 97, 42]
# Object destructuring with rest
{
    "defining": [
        [
            {"objectPattern": ["bar", {"rest": "others"}]},
            {
                "object": [
                    ["baz", {"literal": 216}],
                    ["bar", {"literal": 42}],
                    ["foo", {"literal": 97}]
                ]
            }
        ]
    ],
    "result": {
        "array": [
            {"name": "bar"},
            {"name": "others"}
        ]
    }
}
>> [42, {baz: 216, foo: 97}]
# Object destructuring with aliases
{
    "defining": [
        [
            {
                "objectPattern": [
                    {"name": "spam", "property": "foo"},
                    {"name": "eggs", "property": "bar"}
                ]
            },
            {
                "object": [
                    ["foo", {"literal": 42}],
                    ["bar", {"literal": 97}]
                ]
            }
        ]
    ],
    "result": {
        "array": [
            {"name": "eggs"},
            {"name": "eggs"},
            {"name": "spam"}
        ]
    }
}
>> [97, 97, 42]

Defining and Calling Functions

# No parameters, no arguments
{
    "defining": [
        ["foo", {"given": {}, "result": {"literal": 42}}]
    ],
    "result": {"calling": {"name": "foo"}}
}
>> 42
# One positional parameter, one positional argument
{
    "defining": [
        ["foo", {"given": {"params": ["x"]}, "result": {"name": "x"}}]
    ],
    "result": {
        "calling": {"name": "foo"},
        "args": [{"literal": 42}]
    }
}
>> 42
# One positional parameter, no arguments
{
    "defining": [
        ["foo", {"given": {"params": ["x"]}, "result": {"name": "x"}}]
    ],
    "result": {"calling": {"name": "foo"}}
}
!! missingArgument {"name": "x"}
# One optional positional parameter, one positional argument
{
    "defining": [
        [
            "foo",
            {
                "given": {
                    "params": [
                        {
                            "name": "x",
                            "defaultValue": {"literal": 73}
                        }
                    ]
                },
                "result": {"name": "x"}
            }
        ]
    ],
    "result": {"calling": {"name": "foo"}, "args": [{"literal": 42}]}
}
>> 42
# One optional positional parameter, no arguments
{
    "defining": [
        [
            "foo",
            {
                "given": {
                    "params": [
                        {
                            "name": "x",
                            "defaultValue": {"literal": 73}
                        }
                    ]
                },
                "result": {"name": "x"}
            }
        ]
    ],
    "result": {"calling": {"name": "foo"}}
}
>> 73
# Default value referencing a name
{
    "defining": [
        ["foo", {"literal": 73}],
        [
            "bar",
            {
                "given": {
                    "params": [
                        {
                            "name": "x",
                            "defaultValue": {"name": "foo"}
                        }
                    ]
                },
                "result": {"name": "x"}
            }
        ]
    ],
    "result": {"calling": {"name": "bar"}}
}
>> 73
# Positional rest parameter
{
    "defining": [
        [
            "foo",
            {
                "given": {
                    "params": [{"rest": "args"}]
                },
                "result": {"name": "args"}
            }
        ]
    ],
    "result": {
        "calling": {"name": "foo"},
        "args": [{"literal": 42}, {"literal": 97}]
    }
}
>> [42, 97]
# Spread positional argument
{
    "defining": [
        [
            "foo",
            {
                "given": {
                    "params": ["bar", "baz"]
                },
                "result": {
                    "array": [
                        {"name": "baz"},
                        {"name": "baz"},
                        {"name": "bar"}
                    ]
                }
            }
        ]
    ],
    "result": {
        "calling": {"name": "foo"},
        "args": [
            {
                "spread": {
                    "array": [
                        {"literal": 42},
                        {"literal": 97}
                    ]
                }
            }
        ]
    }
}
>> [97, 97, 42]
# One named parameter, one argument with that name
{
    "defining": [
        ["foo", {"given": {"namedParams": ["x"]}, "result": {"name": "x"}}]
    ],
    "result": {
        "calling": {"name": "foo"},
        "namedArgs": [["x", {"literal": 42}]]
    }
}
>> 42
# One named parameter, no arguments
{
    "defining": [
        ["foo", {"given": {"namedParams": ["x"]}, "result": {"name": "x"}}]
    ],
    "result": {"calling": {"name": "foo"}}
}
!! missingArgument {"name": "x"}
# One named parameter, one positional argument
{
    "defining": [
        ["foo", {"given": {"namedParams": ["x"]}, "result": {"name": "x"}}]
    ],
    "result": {
        "calling": {"name": "foo"},
        "args": [{"literal": 42}]
    }
}
!! missingArgument {"name": "x"}
# Named rest parameter
{
    "defining": [
        [
            "foo",
            {
                "given": {
                    "namedParams": [{"rest": "namedArgs"}]
                },
                "result": {"name": "namedArgs"}
            }
        ]
    ],
    "result": {
        "calling": {"name": "foo"},
        "namedArgs": [
            ["bar", {"literal": 42}],
            ["baz", {"literal": 97}]
        ]
    }
}
>> {bar: 42, baz: 97}
# Spread named argument
{
    "defining": [
        [
            "foo",
            {
                "given": {
                    "namedParams": ["bar", "baz"]
                },
                "result": {
                    "array": [
                        {"name": "baz"},
                        {"name": "baz"},
                        {"name": "bar"}
                    ]
                }
            }
        ]
    ],
    "result": {
        "calling": {"name": "foo"},
        "namedArgs": [
            {
                "spread": {
                    "object": [
                        ["bar", {"literal": 42}],
                        ["baz", {"literal": 97}]
                    ]
                }
            }
        ]
    }
}
>> [97, 97, 42]
# Array destructuring in parameters
{
    "defining": [
        [
            "foo",
            {
                "given": {
                    "params": [
                        {"arrayPattern": ["foo", "bar"]}
                    ]
                },
                "result": {
                    "array": [
                        {"name": "bar"},
                        {"name": "bar"},
                        {"name": "foo"}
                    ]
                }
            }
        ]
    ],
    "result": {
        "calling": {"name": "foo"},
        "args": [
            {
                "array": [
                    {"literal": 42},
                    {"literal": 97}
                ]
            }
        ]
    }
}
>> [97, 97, 42]
# Calling a non-function
{
    "calling": {"literal": 42}
}
!! notCallable {"value": 42}
# A name from an enclosing function
{
    "defining": [
        ["x", {"literal": 73}],
        [
            "foo",
            {
                "given": {"params": ["y"]},
                "result": {
                    "array": [
                        {"name": "x"},
                        {"name": "y"}
                    ]
                }
            }
        ]
    ],
    "result": {"calling": {"name": "foo"}, "args": [{"literal": 42}]}

}
>> [73, 42]

A function body 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.

# Closure
{
    "calling": {
        "calling": {
            "given": {},
            "result": {
                "defining": [["x", {"literal": 73}]],
                "result": {
                    "given": {"params": ["y"]},
                    "result": {
                        "array": [
                            {"name": "x"},
                            {"name": "y"}
                        ]
                    }
                }
            }
        }
    },
    "args": [{"literal": 42}]
}
>> [73, 42]

On the other hand, names that are in scope when the function is called don’t leak into the function body.

# Leakage
{
    "defining": [
        [
            "leaky",
            {"given": {"params": ["x"]}, "result": {"name": "intruder"}}
        ]
    ],
    "result": {
        "defining": [
            ["intruder", {"literal": 42}]
        ],
        "result": {"calling": {"name": "leaky"}, "args": [{"literal": 73}]}
    }
}
!! nameNotDefined {"name": "intruder"}

Indexing

# Indexing strings
{
    "indexing": {"literal": "foobar"},
    "at": {"literal": 4}
}
>> "b"
# Indexing strings - wrong index type
{
    "indexing": {"literal": "foobar"},
    "at": {"literal": "baz"}
}
!! wrongType {"value": "baz", "expectedType": "number"}
# Indexing arrays
{
    "indexing": {
        "array": [
            {"literal": "foo"},
            {"literal": "bar"}
        ]
    },
    "at": {"literal": 2}
}
>> "bar"
# Indexing arrays - index from end
{
    "indexing": {
        "array": [
            {"literal": "foo"},
            {"literal": "bar"}
        ]
    },
    "at": {"literal": -2}
}
>> "foo"
# Indexing arrays - wrong index type
{
    "indexing": {
        "array": [
            {"literal": "foo"},
            {"literal": "bar"}
        ]
    },
    "at": {"literal": "baz"}
}
!! wrongType {"value": "baz", "expectedType": "number"}
# Indexing arrays - index less than minus length
{
    "indexing": {
        "array": [
            {"literal": "foo"},
            {"literal": "bar"}
        ]
    },
    "at": {"literal": -3}
}
!! indexOutOfBounds {"value": ["foo", "bar"], "length": 2, "index": -3}
# Indexing arrays - index 0
{
    "indexing": {
        "array": [
            {"literal": "foo"},
            {"literal": "bar"}
        ]
    },
    "at": {"literal": 0}
}
!! indexOutOfBounds {"value": ["foo", "bar"], "length": 2, "index": 0}
# Indexing arrays - index greater than length
{
    "indexing": {
        "array": [
            {"literal": "foo"},
            {"literal": "bar"}
        ]
    },
    "at": {"literal": 3}
}
!! indexOutOfBounds {"value": ["foo", "bar"], "length": 2, "index": 3}
# Indexing objects
{
    "indexing": {
        "object": [
            ["foo", {"literal": "bar"}],
            ["spam", {"literal": "eggs"}]
        ]
    },
    "at": {"literal": "spam"}
}
>> "eggs"
# Indexing objects - wrong index type
{
    "indexing": {
        "object": [
            ["foo", {"literal": "bar"}],
            ["spam", {"literal": "eggs"}]
        ]
    },
    "at": {"literal": 42}
}
!! wrongType {"value": 42, "expectedType": "string"}
{
    "indexing": {
        "object": [
            ["foo", {"literal": "bar"}],
            ["spam", {"literal": "eggs"}]
        ]
    },
    "at": {"literal": "baz"}
}
!! missingProperty {"value": {foo: "bar", spam: "eggs"}, "key": "baz"}
# Indexing something non-indexable
{
    "indexing": {"literal": 42},
    "at": {"literal": 2}
}
!! wrongType {"value": 42, "expectedType": {"either": ["string", "array", "object"]}}

Errors

# Error short-circuiting through function calls
{
    "calling": {
        "given": {},
        "result": {"literal": 42}
    },
    "args": [
        {
            "defining": [
                [{"arrayPattern": ["foo"]}, {"array": []}]
            ],
            "result": {"name": "foo"}
        }
    ]
}
!! missingElement {"name": "foo"}
# Error short-circuiting through arrays
{
    "array": [
        {
            "defining": [
                [{"arrayPattern": ["foo"]}, {"array": []}]
            ],
            "result": {"name": "foo"}
        }
    ]
}
!! missingElement {"name": "foo"}
# Error short-circuiting through objects
{
    "object": [
        [
            "foo",
            {
                "defining": [
                    [{"arrayPattern": ["foo"]}, {"array": []}]
                ],
                "result": {"name": "foo"}
            }
        ]
    ]
}
!! missingElement {"name": "foo"}
# Error catching
{
    "calling": {
        "given": {},
        "result": {"literal": 42}
    },
    "args": [
        {
            "catching": {
                "defining": [
                    [{"arrayPattern": ["foo"]}, {"array": []}]
                ],
                "result": {"name": "foo"}
            }
        }
    ]
}
>> 42