diff --git a/README.md b/README.md index 23730e4..0e780a1 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,7 @@ git clone git@github.com:mkantor/please-lang-prototype.git cd please-lang-prototype npm install npm run build -echo '{@runtime, context => :context.program.start_time}' | ./please --output-format=json +echo '@runtime { context => :context.program.start_time }' | ./please --output-format=json ``` There are more example programs in [`./examples`](./examples). @@ -45,7 +45,7 @@ data representation implied by the fact that a value is an atom (e.g. the atom `2` may be an integer in memory). Bare words not containing any -[reserved character sequences](./src/language/parsing/atom.ts#L33-L55) are +[reserved character sequences](./src/language/parsing/atom.ts#L34-L57) are atoms: ``` @@ -178,13 +178,18 @@ expressions_. Most of the interesting stuff that Please does involves evaluating keyword expressions. Under the hood, keyword expressions are modeled as objects. For example, `:foo` -desugars to `{ @lookup, key: foo }`. All such expressions have a key `0` -referring to a value that is an `@`-prefixed atom (the keyword). Keywords -include `@apply`, `@check`, `@function`, `@if`, `@index`, `@lookup`, `@panic`, -and `@runtime`. +desugars to `{ 0: "@lookup", 1: { key: foo } }`. All such expressions have a +property named `0` referring to a value that is an `@`-prefixed atom (the +keyword). Most keyword expressions also require a property named `1` to pass an +argument to the expression. Keywords include `@apply`, `@check`, `@function`, +`@if`, `@index`, `@lookup`, `@panic`, and `@runtime`. -Currently only `@function`, `@lookup`, `@index`, and `@apply` have syntax -sugars. +In addition to the specific syntax sugars shown above, any keyword expression +can be written using a generalized sugar: + +``` +@keyword { … } // desugars to `{ 0: "@keyword", 1: { … } }` +``` ### Semantics @@ -210,7 +215,7 @@ function from other programming languages, except there can be any number of `@runtime` expressions in a given program. Here's an example: ``` -{@runtime, context => :context.program.start_time} +@runtime { context => :context.program.start_time } ``` Unsurprisingly, this program outputs the current time when run. @@ -249,7 +254,7 @@ Take this example `plz` program: { language: Please message: :atom.prepend("Welcome to ")(:language) - now: {@runtime, context => :context.program.start_time} + now: @runtime { context => :context.program.start_time } } ``` @@ -259,40 +264,58 @@ It desugars to the following `plo` program: { language: Please message: { - 0: @apply - function: { - 0: @apply + 0: "@apply" + 1: { function: { - 0: @index - object: { - 0: @lookup - key: atom + 0: "@apply" + 1: { + function: { + 0: "@index" + 1: { + object: { + 0: "@lookup" + 1: { + key: atom + } + } + query: { + 0: prepend + } + } + } + argument: "Welcome to " } - query: { - 0: prepend + } + argument: { + 0: "@lookup" + 1: { + key: language } } - argument: "Welcome to " - } - argument: { - 0: @lookup - key: language } } now: { - 0: @runtime + 0: "@runtime" 1: { - 0: @function - parameter: context - body: { - 0: @index - object: { - 0: @lookup - key: context - } - query: { - 0: program - 1: start_time + 0: { + 0: "@function" + 1: { + parameter: context + body: { + 0: "@index" + 1: { + object: { + 0: "@lookup" + 1: { + key: context + } + } + query: { + 0: program + 1: start_time + } + } + } } } } @@ -307,19 +330,27 @@ Which in turn compiles to the following `plt` program: language: Please message: "Welcome to Please" now: { - 0: @runtime - function: { - 0: @function - parameter: context - body: { - 0: @index - object: { - 0: @lookup - key: context - } - query: { - 0: program - 1: start_time + 0: "@runtime" + 1: { + function: { + 0: "@function" + 1: { + parameter: context + body: { + 0: "@index" + 1: { + object: { + 0: "@lookup" + 1: { + key: context + } + } + query: { + 0: program + 1: start_time + } + } + } } } } @@ -333,7 +364,7 @@ Which produces the following runtime output: { language: Please message: "Welcome to Please" - now: "2025-02-14T18:45:14.168Z" + now: "2025-05-13T22:47:50.802Z" } ``` diff --git a/examples/fibonacci.plz b/examples/fibonacci.plz index 6c90536..0080f70 100644 --- a/examples/fibonacci.plz +++ b/examples/fibonacci.plz @@ -1,20 +1,18 @@ { - fibonacci: n => { - @if - condition: :n < 2 - then: :n - else: :fibonacci(:n - 1) + :fibonacci(:n - 2) - } + fibonacci: n => + @if { + condition: :n < 2 + then: :n + else: :fibonacci(:n - 1) + :fibonacci(:n - 2) + } - input: { - @runtime - context => :context.arguments.lookup(input) + input: @runtime { context => + :context.arguments.lookup(input) } output: :input match { none: _ => "missing input argument" - some: input => { - @if + some: input => @if { condition: :natural_number.is(:input) then: :fibonacci(:input) else: "input must be a natural number" diff --git a/examples/kitchen-sink.plz b/examples/kitchen-sink.plz index 7548407..1daf3cb 100644 --- a/examples/kitchen-sink.plz +++ b/examples/kitchen-sink.plz @@ -8,6 +8,6 @@ add_one: :integer.add(1) three: :add_one(:two) function: x => { value: :x } - conditional_value: :function({ @if, :sky_is_blue, :two, :three }) - side_effect: { @runtime, context => :context.log("this goes to stderr") } + conditional_value: :function(@if { :sky_is_blue, :two, :three }) + side_effect: @runtime { context => :context.log("this goes to stderr") } } diff --git a/examples/lookup-environment-variable.plz b/examples/lookup-environment-variable.plz index 0db7fa2..1afec8d 100644 --- a/examples/lookup-environment-variable.plz +++ b/examples/lookup-environment-variable.plz @@ -2,14 +2,12 @@ * Given CLI arguments like `--variable=FOO`, looks up the environment * variable named `FOO`. */ -{ - @runtime - context => - :context.arguments.lookup(variable) match { +@runtime { context => + :context.arguments.lookup(variable) match { + none: {} + some: :context.environment.lookup >> :match({ none: {} - some: :context.environment.lookup >> :match({ - none: {} - some: :identity - }) - } + some: :identity + }) + } } diff --git a/src/end-to-end.test.ts b/src/end-to-end.test.ts index 1be77eb..686f2a1 100644 --- a/src/end-to-end.test.ts +++ b/src/end-to-end.test.ts @@ -28,21 +28,21 @@ testCases(endToEnd, code => code)('end-to-end tests', [ ['{,a,b,c,}', either.makeRight({ 0: 'a', 1: 'b', 2: 'c' })], ['{a,1:overwritten,c}', either.makeRight({ 0: 'a', 1: 'c' })], ['{overwritten,0:a,c}', either.makeRight({ 0: 'a', 1: 'c' })], - ['{@check, type:true, value:true}', either.makeRight('true')], + ['@check {type:true, value:true}', either.makeRight('true')], [ - '{@panic}', + '@panic', result => { assert(either.isLeft(result)) assert('kind' in result.value) assert.deepEqual(result.value.kind, 'panic') }, ], - ['{a:A, b:{@lookup, a}}', either.makeRight({ a: 'A', b: 'A' })], - ['{a:A, {@lookup, a}}', either.makeRight({ a: 'A', 0: 'A' })], + ['{a:A, b:{"@lookup", {a}}}', either.makeRight({ a: 'A', b: 'A' })], + ['{a:A, {"@lookup", {a}}}', either.makeRight({ a: 'A', 0: 'A' })], ['{a:A, b: :a}', either.makeRight({ a: 'A', b: 'A' })], ['{a:A, :a}', either.makeRight({ a: 'A', 0: 'A' })], [ - '{@runtime, _ => {@panic}}', + '@runtime {_ => @panic}', result => { assert(either.isLeft(result)) assert('kind' in result.value) @@ -53,16 +53,20 @@ testCases(endToEnd, code => code)('end-to-end tests', [ 'a => :a', either.makeRight({ 0: '@function', - parameter: 'a', - body: { 0: '@lookup', key: 'a' }, + 1: { + parameter: 'a', + body: { 0: '@lookup', 1: { key: 'a' } }, + }, }), ], [ '(a => :a)', either.makeRight({ 0: '@function', - parameter: 'a', - body: { 0: '@lookup', key: 'a' }, + 1: { + parameter: 'a', + body: { 0: '@lookup', 1: { key: 'a' } }, + }, }), ], ['{ a: ({ A }) }', either.makeRight({ a: { 0: 'A' } })], @@ -86,11 +90,15 @@ testCases(endToEnd, code => code)('end-to-end tests', [ // foo: bar "static data":"blah blah blah" "evaluated data": { - 0:@runtime - function:{ - 0:@apply - function:{0:@index, object:{0:@lookup, key:object}, query:{0:lookup}} - argument:"key which does not exist in runtime context" + 0:"@runtime" + 1:{ + function:{ + 0:"@apply" + 1:{ + function:{0:"@index", 1:{object:{0:"@lookup", 1:{key:object}}, query:{0:lookup}}} + argument:"key which does not exist in runtime context" + } + } } } }`, @@ -145,7 +153,7 @@ testCases(endToEnd, code => code)('end-to-end tests', [ ], [':match({ a: A })({ tag: a, value: {} })', either.makeRight('A')], [ - `{@runtime, context => + `@runtime { context => :identity(:context).program.start_time }`, output => { @@ -173,9 +181,7 @@ testCases(endToEnd, code => code)('end-to-end tests', [ [`(1 - 2) - 3`, either.makeRight('-4')], [':flow(:atom.append(b))(:atom.append(a))(z)', either.makeRight('zab')], [ - `{@runtime - :object.lookup("key which does not exist in runtime context") - }`, + `@runtime { :object.lookup("key which does not exist in runtime context") }`, either.makeRight({ tag: 'none', value: {} }), ], [ @@ -205,21 +211,23 @@ testCases(endToEnd, code => code)('end-to-end tests', [ either.makeRight({ true: 'true', false: 'false' }), ], [ - `{@runtime, :flow( - :match({ - none: "environment does not exist" - some: :flow( - :match({ - none: "environment.lookup does not exist" - some: :apply(PATH) - }) - )( - :object.lookup(lookup) - ) - }) - )( - :object.lookup(environment) - )}`, + `@runtime { + :flow( + :match({ + none: "environment does not exist" + some: :flow( + :match({ + none: "environment.lookup does not exist" + some: :apply(PATH) + }) + )( + :object.lookup(lookup) + ) + }) + )( + :object.lookup(environment) + ) + }`, output => { if (either.isLeft(output)) { assert.fail(output.value.message) @@ -250,7 +258,7 @@ testCases(endToEnd, code => code)('end-to-end tests', [ either.makeRight({ 0: 'a', 1: 'b', 2: 'c', 3: 'd' }), ], [ - `{@runtime, context => + `@runtime { context => :context.environment.lookup(PATH) }`, output => { @@ -263,9 +271,10 @@ testCases(endToEnd, code => code)('end-to-end tests', [ }, ], [ - `{@if, true + `@if { + true "it works!" - {@panic} + @panic }`, either.makeRight('it works!'), ], @@ -278,21 +287,23 @@ testCases(endToEnd, code => code)('end-to-end tests', [ either.makeRight({ 0: 'a', 1: 'b', 2: 'c' }), ], [ - `{@runtime, context => - {@if, :boolean.not(:boolean.is(:context)) + `@runtime { context => + @if { + :boolean.not(:boolean.is(:context)) "it works!" - {@panic} + @panic } }`, either.makeRight('it works!'), ], [ `{ - fibonacci: n => { - @if, :integer.less_than(2)(:n) - then: :n - else: :fibonacci(:n - 1) + :fibonacci(:n - 2) - } + fibonacci: n => + @if { + :integer.less_than(2)(:n) + then: :n + else: :fibonacci(:n - 1) + :fibonacci(:n - 2) + } result: :fibonacci(10) }.result`, either.makeRight('55'), @@ -348,7 +359,7 @@ testCases(endToEnd, code => code)('end-to-end tests', [ either.makeRight('2'), ], [ - `{@runtime, context => + `@runtime { context => ( PATH |> :context.environment.lookup diff --git a/src/language/compiling/compiler.test.ts b/src/language/compiling/compiler.test.ts index db271d3..9bfbc2b 100644 --- a/src/language/compiling/compiler.test.ts +++ b/src/language/compiling/compiler.test.ts @@ -16,16 +16,21 @@ testCases(compile, input => `compiling \`${JSON.stringify(input)}\``)( 'compiler', [ ['Hello, world!', success('Hello, world!')], - [['@check', true, ['@lookup', 'identity']], success('true')], + [['@check', [true, ['@lookup', ['identity']]]], success('true')], [ { - true1: ['@check', true, ['@lookup', 'identity']], - true2: ['@apply', ['@index', ['@lookup', 'boolean'], ['not']], false], - false1: ['@check', false, ['@index', ['@lookup', 'boolean'], ['is']]], + true1: ['@check', [true, ['@lookup', ['identity']]]], + true2: [ + '@apply', + [['@index', [['@lookup', ['boolean']], ['not']]], false], + ], + false1: [ + '@check', + [false, ['@index', [['@lookup', ['boolean']], ['is']]]], + ], false2: [ '@apply', - ['@index', ['@lookup', 'boolean'], ['is']], - 'not a boolean', + [['@index', [['@lookup', ['boolean']], ['is']]], 'not a boolean'], ], }, success({ @@ -36,29 +41,40 @@ testCases(compile, input => `compiling \`${JSON.stringify(input)}\``)( }), ], [ - ['@runtime', ['@lookup', 'identity']], + ['@runtime', [['@lookup', ['identity']]]], success({ 0: '@runtime', - function: { 0: '@lookup', key: 'identity' }, + 1: { function: { 0: '@lookup', 1: { key: 'identity' } } }, }), ], [ [ '@runtime', - ['@apply', ['@lookup', 'identity'], ['@lookup', 'identity']], + [ + [ + '@apply', + [ + ['@lookup', ['identity']], + ['@lookup', ['identity']], + ], + ], + ], ], success({ 0: '@runtime', - function: { 0: '@lookup', key: 'identity' }, + 1: { function: { 0: '@lookup', 1: { key: 'identity' } } }, }), ], [ - ['@check', 'not a boolean', ['@index', ['@lookup', 'boolean'], ['is']]], + [ + '@check', + ['not a boolean', ['@index', [['@lookup', ['boolean']], ['is']]]], + ], output => assert(either.isLeft(output)), ], - [['@lookup', 'compose'], output => assert(either.isLeft(output))], + [[['@lookup', ['compose']]], output => assert(either.isLeft(output))], [ - ['@runtime', ['@index', ['@lookup', 'boolean'], ['not']]], + ['@runtime', [['@index', [['@lookup', ['boolean']], ['not']]]]], output => { assert(either.isLeft(output)) assert(output.value.kind === 'typeMismatch') @@ -68,9 +84,13 @@ testCases(compile, input => `compiling \`${JSON.stringify(input)}\``)( [ '@runtime', [ - '@apply', - ['@lookup', 'identity'], - ['@index', ['@lookup', 'boolean'], ['not']], + [ + '@apply', + [ + ['@lookup', ['identity']], + ['@index', [['@lookup', ['boolean']], ['not']]], + ], + ], ], ], output => { @@ -82,26 +102,42 @@ testCases(compile, input => `compiling \`${JSON.stringify(input)}\``)( [ '@runtime', [ - '@apply', - ['@apply', ['@lookup', 'flow'], ['@lookup', 'identity']], - ['@lookup', 'identity'], + [ + '@apply', + [ + [ + '@apply', + [ + ['@lookup', ['flow']], + ['@lookup', ['identity']], + ], + ], + ['@lookup', ['identity']], + ], + ], ], ], success({ 0: '@runtime', - function: { - 0: '@apply', + 1: { function: { 0: '@apply', - function: { 0: '@lookup', key: 'flow' }, - argument: { 0: '@lookup', key: 'identity' }, + 1: { + function: { + 0: '@apply', + 1: { + function: { 0: '@lookup', 1: { key: 'flow' } }, + argument: { 0: '@lookup', 1: { key: 'identity' } }, + }, + }, + argument: { 0: '@lookup', 1: { key: 'identity' } }, + }, }, - argument: { 0: '@lookup', key: 'identity' }, }, }), ], [ - ['@runtime', ['@index', ['@lookup', 'boolean'], ['not']]], + ['@runtime', [['@index', [['@lookup', ['boolean']], ['not']]]]], output => { assert(either.isLeft(output)) assert(output.value.kind === 'typeMismatch') @@ -110,26 +146,38 @@ testCases(compile, input => `compiling \`${JSON.stringify(input)}\``)( [ { 0: '@runtime', - function: { - 0: '@apply', + 1: { function: { - 0: '@index', - object: { 0: '@lookup', key: 'object' }, - query: { 0: 'lookup' }, + 0: '@apply', + 1: { + function: { + 0: '@index', + 1: { + object: { 0: '@lookup', 1: { key: 'object' } }, + query: { 0: 'lookup' }, + }, + }, + argument: 'key which does not exist in runtime context', + }, }, - argument: 'key which does not exist in runtime context', }, }, success({ 0: '@runtime', - function: { - 0: '@apply', + 1: { function: { - 0: '@index', - object: { 0: '@lookup', key: 'object' }, - query: { 0: 'lookup' }, + 0: '@apply', + 1: { + function: { + 0: '@index', + 1: { + object: { 0: '@lookup', 1: { key: 'object' } }, + query: { 0: 'lookup' }, + }, + }, + argument: 'key which does not exist in runtime context', + }, }, - argument: 'key which does not exist in runtime context', }, }), ], diff --git a/src/language/compiling/semantics/keyword-handlers/apply-handler.test.ts b/src/language/compiling/semantics/keyword-handlers/apply-handler.test.ts index 2d72f39..8041725 100644 --- a/src/language/compiling/semantics/keyword-handlers/apply-handler.test.ts +++ b/src/language/compiling/semantics/keyword-handlers/apply-handler.test.ts @@ -3,91 +3,125 @@ import assert from 'node:assert' import { elaborationSuite, success } from '../test-utilities.test.js' elaborationSuite('@apply', [ - [{ 0: '@apply', 1: { 0: '@lookup', key: 'identity' }, 2: 'a' }, success('a')], + [ + { 0: '@apply', 1: { 0: { 0: '@lookup', 1: { key: 'identity' } }, 1: 'a' } }, + success('a'), + ], [ { 0: '@apply', - function: { 0: '@lookup', key: 'identity' }, - argument: 'a', + 1: { + function: { 0: '@lookup', 1: { key: 'identity' } }, + argument: 'a', + }, }, success('a'), ], [ { 0: '@apply', - function: { 0: '@lookup', key: 'identity' }, - argument: { foo: 'bar' }, + 1: { + function: { 0: '@lookup', 1: { key: 'identity' } }, + argument: { foo: 'bar' }, + }, }, success({ foo: 'bar' }), ], [ - { 0: '@apply', function: 'not a function', argument: 'a' }, + { 0: '@apply', 1: { function: 'not a function', argument: 'a' } }, output => assert(either.isLeft(output)), ], [ { 0: '@apply', - function: { 0: '@function', 1: 'x', 2: { 0: '@lookup', 1: 'x' } }, - argument: 'identity is identical', + 1: { + function: { + 0: '@function', + 1: { 0: 'x', 1: { 0: '@lookup', 1: { 0: 'x' } } }, + }, + argument: 'identity is identical', + }, }, success('identity is identical'), ], [ { 0: '@apply', - function: { - 0: '@function', - parameter: 'a', - body: { - 0: '@apply', - function: { - 0: '@function', - parameter: 'b', + 1: { + function: { + 0: '@function', + 1: { + parameter: 'a', body: { - A: { 0: '@lookup', key: 'a' }, - B: { 0: '@lookup', key: 'b' }, + 0: '@apply', + 1: { + function: { + 0: '@function', + 1: { + parameter: 'b', + body: { + A: { 0: '@lookup', 1: { key: 'a' } }, + B: { 0: '@lookup', 1: { key: 'b' } }, + }, + }, + }, + argument: 'b', + }, }, }, - argument: 'b', }, + argument: 'a', }, - argument: 'a', }, success({ A: 'a', B: 'b' }), ], [ { 0: '@apply', - function: { - 0: '@function', - 1: 'x', - 2: { - 0: '@apply', - function: { - 0: '@index', - 1: { 0: '@lookup', 1: 'boolean' }, - 2: { 0: 'not' }, + 1: { + function: { + 0: '@function', + 1: { + 0: 'x', + 1: { + 0: '@apply', + 1: { + function: { + 0: '@index', + 1: { + 0: { 0: '@lookup', 1: { 0: 'boolean' } }, + 1: { 0: 'not' }, + }, + }, + argument: { 0: '@lookup', 1: { 0: 'x' } }, + }, + }, }, - argument: { 0: '@lookup', 1: 'x' }, }, + argument: 'false', }, - argument: 'false', }, success('true'), ], [ { 0: '@apply', - function: { - 0: '@function', - 1: 'x', - 2: { - 0: '@index', - 1: { 0: '@lookup', 1: 'x' }, - 2: { 0: 'a' }, + 1: { + function: { + 0: '@function', + 1: { + 0: 'x', + 1: { + 0: '@index', + 1: { + 0: { 0: '@lookup', 1: { 0: 'x' } }, + 1: { 0: 'a' }, + }, + }, + }, }, + argument: { a: 'it works' }, }, - argument: { a: 'it works' }, }, success('it works'), ], @@ -103,26 +137,34 @@ elaborationSuite('@apply', [ a: 'a', b: { 0: '@apply', - function: { - 0: '@function', - parameter: 'a', - body: { - a: 'b', - b: { - 0: '@apply', - function: { - 0: '@function', - parameter: 'a', - body: { - 0: '@lookup', - key: 'a', + 1: { + function: { + 0: '@function', + 1: { + parameter: 'a', + body: { + a: 'b', + b: { + 0: '@apply', + 1: { + function: { + 0: '@function', + 1: { + parameter: 'a', + body: { + 0: '@lookup', + 1: { key: 'a' }, + }, + }, + }, + argument: 'it works', + }, }, }, - argument: 'it works', }, }, + argument: 'unused', }, - argument: 'unused', }, }, success({ @@ -145,23 +187,31 @@ elaborationSuite('@apply', [ a: 'a', b: { 0: '@apply', - function: { - 0: '@function', - parameter: 'a', - body: { - a: 'it works', - b: { - 0: '@apply', - function: { - 0: '@function', - parameter: 'a', - body: { 0: '@lookup', key: 'a' }, + 1: { + function: { + 0: '@function', + 1: { + parameter: 'a', + body: { + a: 'it works', + b: { + 0: '@apply', + 1: { + function: { + 0: '@function', + 1: { + parameter: 'a', + body: { 0: '@lookup', 1: { key: 'a' } }, + }, + }, + argument: { 0: '@lookup', 1: { key: 'a' } }, + }, + }, }, - argument: { 0: '@lookup', key: 'a' }, }, }, + argument: 'unused', }, - argument: 'unused', }, }, success({ @@ -183,22 +233,30 @@ elaborationSuite('@apply', [ a: 'it works', b: { 0: '@apply', - function: { - 0: '@function', - parameter: 'a', - body: { - b: { - 0: '@apply', - function: { - 0: '@function', - parameter: 'a', - body: { 0: '@lookup', key: 'a' }, + 1: { + function: { + 0: '@function', + 1: { + parameter: 'a', + body: { + b: { + 0: '@apply', + 1: { + function: { + 0: '@function', + 1: { + parameter: 'a', + body: { 0: '@lookup', 1: { key: 'a' } }, + }, + }, + argument: { 0: '@lookup', 1: { key: 'a' } }, + }, + }, }, - argument: { 0: '@lookup', key: 'a' }, }, }, + argument: { 0: '@lookup', 1: { key: 'a' } }, }, - argument: { 0: '@lookup', key: 'a' }, }, }, success({ @@ -220,23 +278,31 @@ elaborationSuite('@apply', [ a: 'a', b: { 0: '@apply', - function: { - 0: '@function', - parameter: 'a', - body: { - a: 'it works', - b: { - 0: '@apply', - function: { - 0: '@function', - parameter: 'b', - body: { 0: '@lookup', key: 'a' }, + 1: { + function: { + 0: '@function', + 1: { + parameter: 'a', + body: { + a: 'it works', + b: { + 0: '@apply', + 1: { + function: { + 0: '@function', + 1: { + parameter: 'b', + body: { 0: '@lookup', 1: { key: 'a' } }, + }, + }, + argument: 'unused', + }, + }, }, - argument: 'unused', }, }, + argument: 'unused', }, - argument: 'unused', }, }, success({ @@ -259,23 +325,31 @@ elaborationSuite('@apply', [ a: 'it works', b: { 0: '@apply', - function: { - 0: '@function', - parameter: 'b', - body: { - b: 'b', - c: { - 0: '@apply', - function: { - 0: '@function', - parameter: 'b', - body: { 0: '@lookup', key: 'a' }, + 1: { + function: { + 0: '@function', + 1: { + parameter: 'b', + body: { + b: 'b', + c: { + 0: '@apply', + 1: { + function: { + 0: '@function', + 1: { + parameter: 'b', + body: { 0: '@lookup', 1: { key: 'a' } }, + }, + }, + argument: 'unused', + }, + }, }, - argument: 'unused', }, }, + argument: 'unused', }, - argument: 'unused', }, }, success({ diff --git a/src/language/compiling/semantics/keyword-handlers/apply-handler.ts b/src/language/compiling/semantics/keyword-handlers/apply-handler.ts index 2ff3208..bb6a59b 100644 --- a/src/language/compiling/semantics/keyword-handlers/apply-handler.ts +++ b/src/language/compiling/semantics/keyword-handlers/apply-handler.ts @@ -18,14 +18,14 @@ export const applyKeywordHandler: KeywordHandler = ( either.flatMap( readApplyExpression(expression), (applyExpression): Either => { - if (containsAnyUnelaboratedNodes(applyExpression.argument)) { + if (containsAnyUnelaboratedNodes(applyExpression[1].argument)) { // The argument isn't ready, so keep the @apply unelaborated. return either.makeRight(applyExpression) } else { - const functionToApply = asSemanticGraph(applyExpression.function) + const functionToApply = asSemanticGraph(applyExpression[1].function) if (isFunctionNode(functionToApply)) { const result = functionToApply( - asSemanticGraph(applyExpression.argument), + asSemanticGraph(applyExpression[1].argument), ) if (either.isLeft(result)) { if (result.value.kind === 'dependencyUnavailable') { diff --git a/src/language/compiling/semantics/keyword-handlers/check-handler.test.ts b/src/language/compiling/semantics/keyword-handlers/check-handler.test.ts index 457d883..9ecb300 100644 --- a/src/language/compiling/semantics/keyword-handlers/check-handler.test.ts +++ b/src/language/compiling/semantics/keyword-handlers/check-handler.test.ts @@ -3,45 +3,54 @@ import assert from 'node:assert' import { elaborationSuite, success } from '../test-utilities.test.js' elaborationSuite('@check', [ - [{ 0: '@check', 1: 'a', 2: 'a' }, success('a')], - [{ 0: '@check', type: 'a', value: 'a' }, success('a')], - [{ 0: '@check', type: '', value: '' }, success('')], - [{ 0: '@check', type: '@@a', value: '@@a' }, success('@a')], - [{ 0: '@check', 1: 'a', 2: 'B' }, output => assert(either.isLeft(output))], + [{ 0: '@check', 1: { 0: 'a', 1: 'a' } }, success('a')], + [{ 0: '@check', 1: { type: 'a', value: 'a' } }, success('a')], + [{ 0: '@check', 1: { type: '', value: '' } }, success('')], + [{ 0: '@check', 1: { type: '@@a', value: '@@a' } }, success('@a')], [ - { 0: '@check', type: 'a', value: 'B' }, + { 0: '@check', 1: { 0: 'a', 1: 'B' } }, output => assert(either.isLeft(output)), ], [ - { 0: '@check', type: 'a', value: {} }, + { 0: '@check', 1: { type: 'a', value: 'B' } }, output => assert(either.isLeft(output)), ], [ - { 0: '@check', type: {}, value: 'a' }, + { 0: '@check', 1: { type: 'a', value: {} } }, + output => assert(either.isLeft(output)), + ], + [ + { 0: '@check', 1: { type: {}, value: 'a' } }, output => assert(either.isLeft(output)), ], [ { 0: '@check', - type: { a: 'b' }, - value: { a: 'not b' }, + 1: { + type: { a: 'b' }, + value: { a: 'not b' }, + }, }, output => assert(either.isLeft(output)), ], [ { 0: '@check', - type: { something: { more: 'complicated' } }, - value: { something: { more: 'complicated' } }, + 1: { + type: { something: { more: 'complicated' } }, + value: { something: { more: 'complicated' } }, + }, }, success({ something: { more: 'complicated' } }), ], [ { 0: '@check', - type: { something: { more: 'complicated' } }, - value: { - something: { more: 'complicated, which also does not typecheck' }, + 1: { + type: { something: { more: 'complicated' } }, + value: { + something: { more: 'complicated, which also does not typecheck' }, + }, }, }, output => assert(either.isLeft(output)), @@ -49,16 +58,20 @@ elaborationSuite('@check', [ [ { 0: '@check', - type: { a: 'b' }, - value: {}, + 1: { + type: { a: 'b' }, + value: {}, + }, }, output => assert(either.isLeft(output)), ], [ { 0: '@check', - type: { a: { b: 'c' } }, - value: { a: {} }, + 1: { + type: { a: { b: 'c' } }, + value: { a: {} }, + }, }, output => assert(either.isLeft(output)), ], @@ -66,24 +79,30 @@ elaborationSuite('@check', [ [ { 0: '@check', - type: { a: 'b' }, - value: { a: 'b', c: 'd' }, + 1: { + type: { a: 'b' }, + value: { a: 'b', c: 'd' }, + }, }, success({ a: 'b', c: 'd' }), ], [ { 0: '@check', - type: {}, - value: { a: 'b' }, + 1: { + type: {}, + value: { a: 'b' }, + }, }, success({ a: 'b' }), ], [ { 0: '@check', - type: { a: {} }, - value: { a: { b: 'c' } }, + 1: { + type: { a: {} }, + value: { a: { b: 'c' } }, + }, }, success({ a: { b: 'c' } }), ], diff --git a/src/language/compiling/semantics/keyword-handlers/check-handler.ts b/src/language/compiling/semantics/keyword-handlers/check-handler.ts index 3c3c93d..109cedd 100644 --- a/src/language/compiling/semantics/keyword-handlers/check-handler.ts +++ b/src/language/compiling/semantics/keyword-handlers/check-handler.ts @@ -17,7 +17,7 @@ export const checkKeywordHandler: KeywordHandler = ( expression: Expression, context: ExpressionContext, ): Either => - either.flatMap(readCheckExpression(expression), ({ value, type }) => + either.flatMap(readCheckExpression(expression), ({ 1: { value, type } }) => check({ value: asSemanticGraph(value), type: asSemanticGraph(type), diff --git a/src/language/compiling/semantics/keyword-handlers/function-handler.test.ts b/src/language/compiling/semantics/keyword-handlers/function-handler.test.ts index 9e7daba..19326e4 100644 --- a/src/language/compiling/semantics/keyword-handlers/function-handler.test.ts +++ b/src/language/compiling/semantics/keyword-handlers/function-handler.test.ts @@ -6,11 +6,11 @@ import { elaborationSuite } from '../test-utilities.test.js' elaborationSuite('@function', [ [ - { 0: '@function', 1: 'not a function' }, + { 0: '@function', 1: { 0: 'not a function' } }, output => assert(either.isLeft(output)), ], [ - { 0: '@function', 1: 'x', 2: { 0: '@lookup', 1: 'x' } }, + { 0: '@function', 1: { 0: 'x', 1: { 0: '@lookup', 1: { 0: 'x' } } } }, elaboratedFunction => { assert(!either.isLeft(elaboratedFunction)) assert(isFunctionNode(elaboratedFunction.value)) @@ -22,8 +22,7 @@ elaborationSuite('@function', [ elaboratedFunction.value.serialize(), either.makeRight({ 0: '@function', - parameter: 'x', - body: { 0: '@lookup', key: 'x' }, + 1: { parameter: 'x', body: { 0: '@lookup', 1: { key: 'x' } } }, }), ) }, diff --git a/src/language/compiling/semantics/keyword-handlers/function-handler.ts b/src/language/compiling/semantics/keyword-handlers/function-handler.ts index 9fef0c0..700833f 100644 --- a/src/language/compiling/semantics/keyword-handlers/function-handler.ts +++ b/src/language/compiling/semantics/keyword-handlers/function-handler.ts @@ -30,7 +30,7 @@ export const functionKeywordHandler: KeywordHandler = ( return: types.something, }, () => either.makeRight(functionExpression), - option.makeSome(functionExpression.parameter), + option.makeSome(functionExpression[1].parameter), argument => apply(functionExpression, argument, context), ), ) @@ -40,8 +40,8 @@ const apply = ( argument: SemanticGraph, context: ExpressionContext, ): ReturnType => { - const parameter = expression.parameter - const body = asSemanticGraph(expression.body) + const parameter = expression[1].parameter + const body = asSemanticGraph(expression[1].body) const ownKey = context.location[context.location.length - 1] if (ownKey === undefined) { @@ -76,7 +76,7 @@ const apply = ( argument => apply(expression, argument, context), ), // Put the argument in scope. - [expression.parameter]: argument, + [parameter]: argument, [returnKey]: body, }), ), diff --git a/src/language/compiling/semantics/keyword-handlers/if-handler.test.ts b/src/language/compiling/semantics/keyword-handlers/if-handler.test.ts index c789e92..4a1e1e6 100644 --- a/src/language/compiling/semantics/keyword-handlers/if-handler.test.ts +++ b/src/language/compiling/semantics/keyword-handlers/if-handler.test.ts @@ -1,14 +1,22 @@ import { elaborationSuite, success } from '../test-utilities.test.js' elaborationSuite('@if', [ - [{ 0: '@if', condition: 'false', then: 'no', else: 'yes' }, success('yes')], - [{ 0: '@if', condition: 'true', then: 'yes', else: 'no' }, success('yes')], + [ + { 0: '@if', 1: { condition: 'false', then: 'no', else: 'yes' } }, + success('yes'), + ], + [ + { 0: '@if', 1: { condition: 'true', then: 'yes', else: 'no' } }, + success('yes'), + ], [ { 0: '@if', - condition: 'true', - then: 'it works!', - else: { 0: '@panic' }, + 1: { + condition: 'true', + then: 'it works!', + else: { 0: '@panic' }, + }, }, success('it works!'), ], @@ -17,9 +25,11 @@ elaborationSuite('@if', [ a: 'it works!', b: { 0: '@if', - condition: 'true', - then: { 0: '@lookup', key: 'a' }, - else: { 0: '@panic' }, + 1: { + condition: 'true', + then: { 0: '@lookup', 1: { key: 'a' } }, + else: { 0: '@panic' }, + }, }, }, success({ a: 'it works!', b: 'it works!' }), @@ -27,26 +37,34 @@ elaborationSuite('@if', [ [ { 0: '@if', - condition: 'false', - then: { 0: '@panic' }, - else: 'it works!', + 1: { + condition: 'false', + then: { 0: '@panic' }, + else: 'it works!', + }, }, success('it works!'), ], [ { 0: '@if', - condition: { - 0: '@apply', - function: { - 0: '@index', - object: { 0: '@lookup', key: 'boolean' }, - query: { 0: 'not' }, + 1: { + condition: { + 0: '@apply', + 1: { + function: { + 0: '@index', + 1: { + object: { 0: '@lookup', 1: { key: 'boolean' } }, + query: { 0: 'not' }, + }, + }, + argument: 'false', + }, }, - argument: 'false', + then: 'it works!', + else: { 0: '@panic' }, }, - then: 'it works!', - else: { 0: '@panic' }, }, success('it works!'), ], @@ -54,16 +72,22 @@ elaborationSuite('@if', [ { 0: '@if', 1: { - 0: '@apply', - function: { - 0: '@index', - object: { 0: '@lookup', key: 'boolean' }, - query: { 0: 'not' }, + 0: { + 0: '@apply', + 1: { + function: { + 0: '@index', + 1: { + object: { 0: '@lookup', 1: { key: 'boolean' } }, + query: { 0: 'not' }, + }, + }, + argument: 'false', + }, }, - argument: 'false', + 1: 'it works!', + 2: { 0: '@panic' }, }, - 2: 'it works!', - 3: { 0: '@panic' }, }, success('it works!'), ], @@ -71,9 +95,11 @@ elaborationSuite('@if', [ { a: { 0: '@if', - condition: { 0: '@lookup', key: 'b' }, - then: 'it works!', - else: { 0: '@panic' }, + 1: { + condition: { 0: '@lookup', 1: { key: 'b' } }, + then: 'it works!', + else: { 0: '@panic' }, + }, }, b: 'true', }, diff --git a/src/language/compiling/semantics/keyword-handlers/if-handler.ts b/src/language/compiling/semantics/keyword-handlers/if-handler.ts index 47aa711..83ab5c2 100644 --- a/src/language/compiling/semantics/keyword-handlers/if-handler.ts +++ b/src/language/compiling/semantics/keyword-handlers/if-handler.ts @@ -10,6 +10,7 @@ import { serialize, type Expression, type ExpressionContext, + type KeyPath, type KeywordHandler, type SemanticGraph, } from '../../../semantics.js' @@ -19,35 +20,45 @@ export const ifKeywordHandler: KeywordHandler = ( context: ExpressionContext, ): Either => either.flatMap(readIfExpression(expression), ifExpression => { - const expressionKeys = { + // TODO: Make this less ad-hoc. + if ( + !('1' in expression) || + typeof expression[1] !== 'object' || + expression[1] === null + ) { + throw new Error( + '`@if` expression was invalid after being validated. This is a bug!', + ) + } + const subexpressionKeyPaths = { // Note: this must be kept in alignment with `readIfExpression`. - condition: 'condition' in expression ? 'condition' : '1', - then: 'then' in expression ? 'then' : '2', - else: 'else' in expression ? 'else' : '3', + condition: ['1', 'condition' in expression[1] ? 'condition' : '0'], + then: ['1', 'then' in expression[1] ? 'then' : '1'], + else: ['1', 'else' in expression[1] ? 'else' : '2'], } const elaboratedCondition = evaluateSubexpression( - expressionKeys.condition, + subexpressionKeyPaths.condition, context, - ifExpression.condition, + ifExpression[1].condition, ) return either.flatMap(elaboratedCondition, elaboratedCondition => { if (elaboratedCondition === 'true') { return either.map( evaluateSubexpression( - expressionKeys.then, + subexpressionKeyPaths.then, context, - ifExpression.then, + ifExpression[1].then, ), asSemanticGraph, ) } else if (elaboratedCondition === 'false') { return either.map( evaluateSubexpression( - expressionKeys.else, + subexpressionKeyPaths.else, context, - ifExpression.else, + ifExpression[1].else, ), asSemanticGraph, ) @@ -59,7 +70,7 @@ export const ifKeywordHandler: KeywordHandler = ( // Return an unelaborated `@if` expression. return either.makeRight( makeIfExpression({ - ...ifExpression, + ...ifExpression[1], condition: elaboratedCondition, }), ) @@ -76,7 +87,7 @@ export const ifKeywordHandler: KeywordHandler = ( }) const evaluateSubexpression = ( - key: string, + subKeyPath: KeyPath, context: ExpressionContext, subexpression: SemanticGraph | Molecule, ) => @@ -86,6 +97,6 @@ const evaluateSubexpression = ( elaborateWithContext(serializedSubexpression, { keywordHandlers: context.keywordHandlers, program: context.program, - location: [...context.location, key], + location: [...context.location, ...subKeyPath], }), ) diff --git a/src/language/compiling/semantics/keyword-handlers/index-handler.test.ts b/src/language/compiling/semantics/keyword-handlers/index-handler.test.ts index a5a58f7..b8e1db4 100644 --- a/src/language/compiling/semantics/keyword-handlers/index-handler.test.ts +++ b/src/language/compiling/semantics/keyword-handlers/index-handler.test.ts @@ -3,29 +3,36 @@ import assert from 'node:assert' import { elaborationSuite, success } from '../test-utilities.test.js' elaborationSuite('@index', [ - [{ 0: '@index', 1: { foo: 'bar' }, 2: { 0: 'foo' } }, success('bar')], + [{ 0: '@index', 1: { 0: { foo: 'bar' }, 1: { 0: 'foo' } } }, success('bar')], [ - { 0: '@index', object: { foo: 'bar' }, query: { 0: 'foo' } }, + { 0: '@index', 1: { object: { foo: 'bar' }, query: { 0: 'foo' } } }, success('bar'), ], [ { 0: '@index', - object: { a: { b: { c: 'it works' } } }, - query: { 0: 'a', 1: 'b', 2: 'c' }, + 1: { + object: { a: { b: { c: 'it works' } } }, + query: { 0: 'a', 1: 'b', 2: 'c' }, + }, }, success('it works'), ], [ { 0: '@index', - object: { a: { b: { c: 'it works' } } }, - query: { 0: 'a', 1: 'b' }, + 1: { + object: { a: { b: { c: 'it works' } } }, + query: { 0: 'a', 1: 'b' }, + }, }, success({ c: 'it works' }), ], [ - { 0: '@index', object: {}, query: { 0: 'thisPropertyDoesNotExist' } }, + { + 0: '@index', + 1: { object: {}, query: { 0: 'thisPropertyDoesNotExist' } }, + }, output => assert(either.isLeft(output)), ], ]) diff --git a/src/language/compiling/semantics/keyword-handlers/index-handler.ts b/src/language/compiling/semantics/keyword-handlers/index-handler.ts index e4b4f60..9c76821 100644 --- a/src/language/compiling/semantics/keyword-handlers/index-handler.ts +++ b/src/language/compiling/semantics/keyword-handlers/index-handler.ts @@ -20,15 +20,15 @@ export const indexKeywordHandler: KeywordHandler = ( ): Either => either.flatMap(readIndexExpression(expression), indexExpression => either.flatMap( - keyPathFromObjectNodeOrMolecule(indexExpression.query), + keyPathFromObjectNodeOrMolecule(indexExpression[1].query), keyPath => { - if (containsAnyUnelaboratedNodes(indexExpression.object)) { + if (containsAnyUnelaboratedNodes(indexExpression[1].object)) { // The object isn't ready, so keep the @index unelaborated. return either.makeRight(indexExpression) } else { return option.match( applyKeyPathToSemanticGraph( - asSemanticGraph(indexExpression.object), + asSemanticGraph(indexExpression[1].object), keyPath, ), { diff --git a/src/language/compiling/semantics/keyword-handlers/lookup-handler.test.ts b/src/language/compiling/semantics/keyword-handlers/lookup-handler.test.ts index 088118f..b145cd8 100644 --- a/src/language/compiling/semantics/keyword-handlers/lookup-handler.test.ts +++ b/src/language/compiling/semantics/keyword-handlers/lookup-handler.test.ts @@ -6,21 +6,21 @@ elaborationSuite('@lookup', [ [ { foo: 'bar', - bar: { 0: '@lookup', 1: 'foo' }, + bar: { 0: '@lookup', 1: { 0: 'foo' } }, }, success({ foo: 'bar', bar: 'bar' }), ], [ { foo: 'bar', - bar: { 0: '@lookup', key: 'foo' }, + bar: { 0: '@lookup', 1: { key: 'foo' } }, }, success({ foo: 'bar', bar: 'bar' }), ], [ { foo: 'bar', - bar: { 0: '@lookup', 1: 'foo' }, + bar: { 0: '@lookup', 1: { 0: 'foo' } }, }, success({ foo: 'bar', bar: 'bar' }), ], @@ -29,7 +29,7 @@ elaborationSuite('@lookup', [ a: 'A', b: { a: 'different A', - b: { 0: '@lookup', key: 'a' }, + b: { 0: '@lookup', 1: { key: 'a' } }, }, }, success({ @@ -43,17 +43,17 @@ elaborationSuite('@lookup', [ [ { foo: 'bar', - bar: { 0: '@lookup', 1: 'foo' }, - baz: { 0: '@lookup', 1: 'bar' }, + bar: { 0: '@lookup', 1: { 0: 'foo' } }, + baz: { 0: '@lookup', 1: { 0: 'bar' } }, }, success({ foo: 'bar', bar: 'bar', baz: 'bar' }), ], [ - { a: { 0: '@lookup', _: 'missing key' } }, + { a: { 0: '@lookup', 1: { _: 'missing key' } } }, output => assert(either.isLeft(output)), ], [ - { a: { 0: '@lookup', key: 'thisPropertyDoesNotExist' } }, + { a: { 0: '@lookup', 1: { key: 'thisPropertyDoesNotExist' } } }, output => assert(either.isLeft(output)), ], @@ -62,7 +62,7 @@ elaborationSuite('@lookup', [ { a: 'C', b: { - c: { 0: '@lookup', key: 'a' }, + c: { 0: '@lookup', 1: { key: 'a' } }, }, }, success({ @@ -77,7 +77,7 @@ elaborationSuite('@lookup', [ a: 'C', b: { a: 'other C', // this `a` should be referenced - c: { 0: '@lookup', key: 'a' }, + c: { 0: '@lookup', 1: { key: 'a' } }, }, }, success({ diff --git a/src/language/compiling/semantics/keyword-handlers/lookup-handler.ts b/src/language/compiling/semantics/keyword-handlers/lookup-handler.ts index 2ca8f60..34bad41 100644 --- a/src/language/compiling/semantics/keyword-handlers/lookup-handler.ts +++ b/src/language/compiling/semantics/keyword-handlers/lookup-handler.ts @@ -22,7 +22,7 @@ export const lookupKeywordHandler: KeywordHandler = ( expression: Expression, context: ExpressionContext, ): Either => - either.flatMap(readLookupExpression(expression), ({ key }) => { + either.flatMap(readLookupExpression(expression), ({ 1: { key } }) => { if (isObjectNode(context.program)) { return either.flatMap(lookup({ context, key }), possibleValue => option.match(possibleValue, { @@ -61,19 +61,26 @@ const lookup = ({ } else { const pathToCurrentScope = context.location.slice(0, -1) + // TODO: This is sketchy, or at least confusingly-written. Improve test coverage to weed out + // potential bugginess, and consider refactoring to make it easier to follow. + const pathToPossibleExpression = + pathToCurrentScope[pathToCurrentScope.length - 1] === '1' + ? pathToCurrentScope.slice(0, -1) + : pathToCurrentScope + const possibleLookedUpValue = option.flatMap( - applyKeyPathToSemanticGraph(context.program, pathToCurrentScope), + applyKeyPathToSemanticGraph(context.program, pathToPossibleExpression), scope => either.match(readFunctionExpression(scope), { left: _ => // Lookups should not resolve to expression properties. - // For example the value of the lookup expression in `a => :parameter` (which desugars - // to `{@function, parameter: a, body: {@lookup, key: parameter}}`) should not be `a`. + // For example the value of the lookup expression in `a => :parameter` (desugared: + // `{@function, {parameter: a, body: {@lookup, {key: parameter}}}}`) should not be `a`. isExpression(scope) ? option.none : applyKeyPathToSemanticGraph(scope, [key]), right: functionExpression => - functionExpression.parameter === key + functionExpression[1].parameter === key ? // Keep an unelaborated `@lookup` around for resolution when the `@function` is called. option.makeSome(makeLookupExpression(key)) : option.none, diff --git a/src/language/compiling/semantics/keyword-handlers/runtime-handler.test.ts b/src/language/compiling/semantics/keyword-handlers/runtime-handler.test.ts index 70b0645..07a0c45 100644 --- a/src/language/compiling/semantics/keyword-handlers/runtime-handler.test.ts +++ b/src/language/compiling/semantics/keyword-handlers/runtime-handler.test.ts @@ -6,15 +6,18 @@ import { elaborationSuite } from '../test-utilities.test.js' elaborationSuite('@runtime', [ [ - { 0: '@runtime', 1: { 0: '@lookup', key: 'identity' } }, + { 0: '@runtime', 1: { 0: { 0: '@lookup', 1: { key: 'identity' } } } }, either.makeRight( withPhantomData()( - makeObjectNode({ 0: '@runtime', function: prelude['identity']! }), + makeObjectNode({ + 0: '@runtime', + 1: makeObjectNode({ function: prelude['identity']! }), + }), ), ), ], [ - { 0: '@runtime', 1: 'not a function' }, + { 0: '@runtime', 1: { 0: 'not a function' } }, output => assert(either.isLeft(output)), ], ]) diff --git a/src/language/compiling/semantics/keyword-handlers/runtime-handler.ts b/src/language/compiling/semantics/keyword-handlers/runtime-handler.ts index cca10a2..5507ef7 100644 --- a/src/language/compiling/semantics/keyword-handlers/runtime-handler.ts +++ b/src/language/compiling/semantics/keyword-handlers/runtime-handler.ts @@ -1,7 +1,6 @@ import either, { type Either } from '@matt.kantor/either' import type { ElaborationError } from '../../../errors.js' import { - asSemanticGraph, isAssignable, isFunctionNode, makeRuntimeExpression, @@ -18,23 +17,26 @@ export const runtimeKeywordHandler: KeywordHandler = ( expression: Expression, _context: ExpressionContext, ): Either => - either.flatMap(readRuntimeExpression(expression), ({ function: f }) => { - const runtimeFunction = asSemanticGraph(f) - if (isFunctionNode(runtimeFunction)) { - const runtimeFunctionSignature = runtimeFunction.signature - return !isAssignable({ - source: types.runtimeContext, - target: replaceAllTypeParametersWithTheirConstraints( - runtimeFunctionSignature.parameter, - ), - }) - ? either.makeLeft({ - kind: 'typeMismatch', - message: '@runtime function must accept a runtime context argument', - }) - : either.makeRight(makeRuntimeExpression(f)) - } else { - // TODO: Type-check unelaborated nodes. - return either.makeRight(makeRuntimeExpression(f)) - } - }) + either.flatMap( + readRuntimeExpression(expression), + ({ 1: { function: runtimeFunction } }) => { + if (isFunctionNode(runtimeFunction)) { + const runtimeFunctionSignature = runtimeFunction.signature + return !isAssignable({ + source: types.runtimeContext, + target: replaceAllTypeParametersWithTheirConstraints( + runtimeFunctionSignature.parameter, + ), + }) + ? either.makeLeft({ + kind: 'typeMismatch', + message: + '@runtime function must accept a runtime context argument', + }) + : either.makeRight(makeRuntimeExpression(runtimeFunction)) + } else { + // TODO: Type-check unelaborated nodes. + return either.makeRight(makeRuntimeExpression(runtimeFunction)) + } + }, + ) diff --git a/src/language/compiling/unparsing.test.ts b/src/language/compiling/unparsing.test.ts index 8721119..4ff64bf 100644 --- a/src/language/compiling/unparsing.test.ts +++ b/src/language/compiling/unparsing.test.ts @@ -24,7 +24,7 @@ testCases( [{}, either.makeRight('{}')], ['a', either.makeRight('a')], ['Hello, world!', either.makeRight('"Hello, world!"')], - ['@test', either.makeRight('@test')], + ['@test', either.makeRight('"@test"')], [{ 0: 'a' }, either.makeRight('{ a }')], [{ 1: 'a' }, either.makeRight('{ 1: a }')], [ @@ -36,13 +36,17 @@ testCases( { identity: { 0: '@function', - parameter: 'a', - body: { 0: '@lookup', 1: 'a' }, + 1: { + parameter: 'a', + body: { 0: '@lookup', 1: { 0: 'a' } }, + }, }, test: { 0: '@apply', - function: { 0: '@lookup', 1: 'identity' }, - argument: 'it works!', + 1: { + function: { 0: '@lookup', 1: { 0: 'identity' } }, + argument: 'it works!', + }, }, }, either.makeRight('{ identity: a => :a, test: :identity("it works!") }'), @@ -50,12 +54,16 @@ testCases( [ { 0: '@apply', - function: { - 0: '@function', - parameter: 'a', - body: { 0: '@lookup', 1: 'a' }, + 1: { + function: { + 0: '@function', + 1: { + parameter: 'a', + body: { 0: '@lookup', 1: { 0: 'a' } }, + }, + }, + argument: 'it works!', }, - argument: 'it works!', }, either.makeRight('(a => :a)("it works!")'), ], @@ -63,16 +71,22 @@ testCases( { 0: '@runtime', 1: { - 0: '@function', - parameter: 'context', - body: { - 0: '@index', - object: { 0: '@lookup', key: 'context' }, - query: { 0: 'program', 1: 'start_time' }, + 0: { + 0: '@function', + 1: { + parameter: 'context', + body: { + 0: '@index', + 1: { + object: { 0: '@lookup', 1: { key: 'context' } }, + query: { 0: 'program', 1: 'start_time' }, + }, + }, + }, }, }, }, - either.makeRight('{ @runtime, context => :context.program.start_time }'), + either.makeRight('@runtime { context => :context.program.start_time }'), ], [ { @@ -83,8 +97,10 @@ testCases( }, test: { 0: '@index', - object: { 0: '@lookup', 1: 'a.b' }, - query: { 0: 'c "d"', 1: 'e.f' }, + 1: { + object: { 0: '@lookup', 1: { 0: 'a.b' } }, + query: { 0: 'c "d"', 1: 'e.f' }, + }, }, }, either.makeRight( @@ -100,7 +116,7 @@ testCases( [{}, either.makeRight('{}')], ['a', either.makeRight('a')], ['Hello, world!', either.makeRight('"Hello, world!"')], - ['@test', either.makeRight('@test')], + ['@test', either.makeRight('"@test"')], [{ 0: 'a' }, either.makeRight('{\n a\n}')], [{ 1: 'a' }, either.makeRight('{\n 1: a\n}')], [ @@ -115,13 +131,14 @@ testCases( { identity: { 0: '@function', - parameter: 'a', - body: { 0: '@lookup', 1: 'a' }, + 1: { parameter: 'a', body: { 0: '@lookup', 1: { 0: 'a' } } }, }, test: { 0: '@apply', - function: { 0: '@lookup', 1: 'identity' }, - argument: 'it works!', + 1: { + function: { 0: '@lookup', 1: { 0: 'identity' } }, + argument: 'it works!', + }, }, }, either.makeRight( @@ -131,12 +148,16 @@ testCases( [ { 0: '@apply', - function: { - 0: '@function', - parameter: 'a', - body: { 0: '@lookup', 1: 'a' }, + 1: { + function: { + 0: '@function', + 1: { + parameter: 'a', + body: { 0: '@lookup', 1: { 0: 'a' } }, + }, + }, + argument: 'it works!', }, - argument: 'it works!', }, either.makeRight('(a => :a)("it works!")'), ], @@ -144,18 +165,22 @@ testCases( { 0: '@runtime', 1: { - 0: '@function', - parameter: 'context', - body: { - 0: '@index', - object: { 0: '@lookup', key: 'context' }, - query: { 0: 'program', 1: 'start_time' }, + 0: { + 0: '@function', + 1: { + parameter: 'context', + body: { + 0: '@index', + 1: { + object: { 0: '@lookup', 1: { key: 'context' } }, + query: { 0: 'program', 1: 'start_time' }, + }, + }, + }, }, }, }, - either.makeRight( - '{\n @runtime\n context => :context.program.start_time\n}', - ), + either.makeRight('@runtime {\n context => :context.program.start_time\n}'), ], [ { @@ -166,8 +191,10 @@ testCases( }, test: { 0: '@index', - object: { 0: '@lookup', 1: 'a.b' }, - query: { 0: 'c "d"', 1: 'e.f' }, + 1: { + object: { 0: '@lookup', 1: { 0: 'a.b' } }, + query: { 0: 'c "d"', 1: 'e.f' }, + }, }, }, either.makeRight( @@ -200,17 +227,18 @@ testCases( { identity: { 0: '@function', - parameter: 'a', - body: { 0: '@lookup', 1: 'a' }, + 1: { parameter: 'a', body: { 0: '@lookup', 1: { 0: 'a' } } }, }, test: { 0: '@apply', - function: { 0: '@lookup', 1: 'identity' }, - argument: 'it works!', + 1: { + function: { 0: '@lookup', 1: { 0: 'identity' } }, + argument: 'it works!', + }, }, }, either.makeRight( - '{\n "identity": {\n "0": "@function",\n "parameter": "a",\n "body": {\n "0": "@lookup",\n "1": "a"\n }\n },\n "test": {\n "0": "@apply",\n "function": {\n "0": "@lookup",\n "1": "identity"\n },\n "argument": "it works!"\n }\n}', + '{\n "identity": {\n "0": "@function",\n "1": {\n "parameter": "a",\n "body": {\n "0": "@lookup",\n "1": {\n "0": "a"\n }\n }\n }\n },\n "test": {\n "0": "@apply",\n "1": {\n "function": {\n "0": "@lookup",\n "1": {\n "0": "identity"\n }\n },\n "argument": "it works!"\n }\n }\n}', ), ], [ @@ -218,13 +246,15 @@ testCases( 0: '@apply', function: { 0: '@function', - parameter: 'a', - body: { 0: '@lookup', 1: 'a' }, + 1: { + parameter: 'a', + body: { 0: '@lookup', 1: { 0: 'a' } }, + }, }, argument: 'it works!', }, either.makeRight( - '{\n "0": "@apply",\n "function": {\n "0": "@function",\n "parameter": "a",\n "body": {\n "0": "@lookup",\n "1": "a"\n }\n },\n "argument": "it works!"\n}', + '{\n "0": "@apply",\n "function": {\n "0": "@function",\n "1": {\n "parameter": "a",\n "body": {\n "0": "@lookup",\n "1": {\n "0": "a"\n }\n }\n }\n },\n "argument": "it works!"\n}', ), ], ]) diff --git a/src/language/parsing/atom.ts b/src/language/parsing/atom.ts index 802e749..adf87a7 100644 --- a/src/language/parsing/atom.ts +++ b/src/language/parsing/atom.ts @@ -11,6 +11,7 @@ import { zeroOrMore, } from '@matt.kantor/parsing' import { + atSign, backslash, closingBlockCommentDelimiter, closingBrace, @@ -31,6 +32,7 @@ import { whitespace } from './trivia.js' export type Atom = string const atomComponentsRequiringQuotation = [ + atSign, backslash, closingBlockCommentDelimiter, closingBrace, diff --git a/src/language/parsing/expression.ts b/src/language/parsing/expression.ts index 6815f83..8e7273a 100644 --- a/src/language/parsing/expression.ts +++ b/src/language/parsing/expression.ts @@ -13,10 +13,12 @@ import { keyPathToMolecule, type KeyPath } from '../semantics.js' import { atom, atomWithAdditionalQuotationRequirements, + unquotedAtomParser, type Atom, } from './atom.js' import { arrow, + atSign, closingBrace, colon, comma, @@ -57,14 +59,18 @@ const trailingIndexesAndArgumentsToExpression = ( case 'argument': return { 0: '@apply', - function: expression, - argument: indexOrArgument.argument, + 1: { + function: expression, + argument: indexOrArgument.argument, + }, } case 'index': return { 0: '@index', - object: expression, - query: keyPathToMolecule(indexOrArgument.query), + 1: { + object: expression, + query: keyPathToMolecule(indexOrArgument.query), + }, } } }, root) @@ -119,18 +125,22 @@ const infixTokensToExpression = ( } const leftmostFunction = trailingIndexesAndArgumentsToExpression( - { 0: '@lookup', key: leftmostOperator[0] }, + { 0: '@lookup', 1: { key: leftmostOperator[0] } }, leftmostOperator[1], ) const reducedLeftmostOperation: Molecule = { 0: '@apply', - function: { - 0: '@apply', - function: leftmostFunction, - argument: leftmostOperationRHS, + 1: { + function: { + 0: '@apply', + 1: { + function: leftmostFunction, + argument: leftmostOperationRHS, + }, + }, + argument: leftmostOperationLHS, }, - argument: leftmostOperationLHS, } return infixTokensToExpression([ @@ -345,20 +355,40 @@ const precededByAtomThenArrow = map( ] const initialFunction = { 0: '@function', - parameter: lastParameter, - body: body, + 1: { + parameter: lastParameter, + body: body, + }, } return additionalParameters.reduce( (expression, additionalParameter) => ({ 0: '@function', - parameter: additionalParameter, - body: expression, + 1: { + parameter: additionalParameter, + body: expression, + }, }), initialFunction, ) }, ) +// @runtime { context => … } +// @panic +// @todo blah +const precededByAtSign = map( + sequence([ + atSign, + unquotedAtomParser, + optionalTrivia, + optional(lazy(() => expression)), + ]), + ([_atSign, keyword, _trivia, argument]) => ({ + 0: `@${keyword}`, + 1: argument === undefined ? {} : argument, + }), +) + // :a // :a.b // :a.b(1).c @@ -368,7 +398,7 @@ const precededByColonThenAtom = map( sequence([colon, atomRequiringDotQuotation, trailingIndexesAndArguments]), ([_colon, key, trailingIndexesAndArguments]) => trailingIndexesAndArgumentsToExpression( - { 0: '@lookup', key }, + { 0: '@lookup', 1: { key } }, trailingIndexesAndArguments, ), ) @@ -409,6 +439,7 @@ export const expression: Parser = map( oneOf([ precededByOpeningParenthesis, precededByOpeningBrace, + precededByAtSign, precededByColonThenAtom, precededByAtomThenArrow, atom, diff --git a/src/language/parsing/literals.ts b/src/language/parsing/literals.ts index 677d105..0d975fc 100644 --- a/src/language/parsing/literals.ts +++ b/src/language/parsing/literals.ts @@ -2,6 +2,7 @@ import { literal } from '@matt.kantor/parsing' export const arrow = literal('=>') export const asterisk = literal('*') +export const atSign = literal('@') export const backslash = literal('\\') export const closingBlockCommentDelimiter = literal('*/') export const closingBrace = literal('}') diff --git a/src/language/runtime/evaluator.test.ts b/src/language/runtime/evaluator.test.ts index 9e134c4..1d5d9c5 100644 --- a/src/language/runtime/evaluator.test.ts +++ b/src/language/runtime/evaluator.test.ts @@ -16,9 +16,12 @@ testCases(evaluate, input => `evaluating \`${JSON.stringify(input)}\``)( 'evaluator', [ ['Hello, world!', success('Hello, world!')], - [['@check', true, ['@lookup', 'identity']], success('true')], + [['@check', [true, ['@lookup', ['identity']]]], success('true')], [ - ['@check', 'not a boolean', ['@index', ['@lookup', 'boolean'], 'is']], + [ + '@check', + ['not a boolean', ['@index', [['@lookup', ['boolean']], 'is']]], + ], output => assert(either.isLeft(output)), ], ], diff --git a/src/language/runtime/keywords.ts b/src/language/runtime/keywords.ts index ac3e499..b8d2790 100644 --- a/src/language/runtime/keywords.ts +++ b/src/language/runtime/keywords.ts @@ -1,20 +1,17 @@ import either from '@matt.kantor/either' -import option, { type Option } from '@matt.kantor/option' +import option from '@matt.kantor/option' import { parseArgs } from 'node:util' import { writeOutput } from '../cli/output.js' import { keywordHandlers as compilerKeywordHandlers } from '../compiling.js' -import type { Atom } from '../parsing.js' import { isFunctionNode, makeFunctionNode, makeObjectNode, + readRuntimeExpression, serialize, types, - type Expression, type KeywordHandlers, - type SemanticGraph, } from '../semantics.js' -import { lookupPropertyOfObjectNode } from '../semantics/object-node.js' import { prettyJson } from '../unparsing.js' const unserializableFunction = () => @@ -131,45 +128,29 @@ export const keywordHandlers: KeywordHandlers = { /** * Evaluates the given function, passing runtime context captured in `world`. */ - '@runtime': expression => { - const runtimeFunction = lookupWithinExpression( - ['function', '1'], - expression, - ) - if ( - option.isNone(runtimeFunction) || - !isFunctionNode(runtimeFunction.value) - ) { - return either.makeLeft({ - kind: 'invalidExpression', - message: - 'a function must be provided via the property `function` or `1`', - }) - } else { - const result = runtimeFunction.value(runtimeContext) - if (either.isLeft(result)) { - // The runtime function panicked or had an unavailable dependency (which results in a panic - // anyway in this context). - return either.makeLeft({ - kind: 'panic', - message: result.value.message, - }) - } else { - return result - } - } - }, -} - -const lookupWithinExpression = ( - keyAliases: [Atom, ...(readonly Atom[])], - expression: Expression, -): Option => { - for (const key of keyAliases) { - const result = lookupPropertyOfObjectNode(key, makeObjectNode(expression)) - if (!option.isNone(result)) { - return result - } - } - return option.none + '@runtime': expression => + either.flatMap( + readRuntimeExpression(expression), + ({ 1: { function: runtimeFunction } }) => { + if (!isFunctionNode(runtimeFunction)) { + return either.makeLeft({ + kind: 'panic', + message: + 'a function must be provided via the property `function` or `0`', + }) + } else { + const result = runtimeFunction(runtimeContext) + if (either.isLeft(result)) { + // The runtime function panicked or had an unavailable dependency (which results in a panic + // anyway in this context). + return either.makeLeft({ + kind: 'panic', + message: result.value.message, + }) + } else { + return result + } + } + }, + ), } diff --git a/src/language/semantics.ts b/src/language/semantics.ts index 4f53825..7c1afa7 100644 --- a/src/language/semantics.ts +++ b/src/language/semantics.ts @@ -18,10 +18,7 @@ export { readCheckExpression, type CheckExpression, } from './semantics/expressions/check-expression.js' -export { - asSemanticGraph, - readArgumentsFromExpression, -} from './semantics/expressions/expression-utilities.js' +export { asSemanticGraph } from './semantics/expressions/expression-utilities.js' export { makeFunctionExpression, readFunctionExpression, diff --git a/src/language/semantics/expression.ts b/src/language/semantics/expression.ts index 4573b28..69b7e86 100644 --- a/src/language/semantics/expression.ts +++ b/src/language/semantics/expression.ts @@ -1,19 +1,27 @@ -import type { Molecule } from '../parsing.js' +import type { Atom, Molecule } from '../parsing.js' import type { SemanticGraph } from './semantic-graph.js' export type Expression = { readonly 0: `@${string}` + readonly 1?: Atom | Molecule } export const isExpression = ( node: SemanticGraph | Molecule, ): node is Expression => - typeof node === 'object' && typeof node[0] === 'string' && node[0][0] === '@' + typeof node === 'object' && + typeof node[0] === 'string' && + /^@[^@]/.test(node['0']) && + (!('1' in node) || typeof node[1] === 'object' || typeof node[1] === 'string') -export const isSpecificExpression = ( +export const isExpressionWithArgument = ( keyword: Keyword, - node: SemanticGraph | Molecule, + node: Molecule | SemanticGraph, ): node is { readonly 0: Keyword + readonly 1: Molecule } => - typeof node === 'object' && typeof node[0] === 'string' && node[0] === keyword + typeof node === 'object' && + typeof node[0] === 'string' && + node[0] === keyword && + (typeof node[1] === 'string' || typeof node[1] === 'object') diff --git a/src/language/semantics/expressions/apply-expression.ts b/src/language/semantics/expressions/apply-expression.ts index 87d91b5..b4b5f4a 100644 --- a/src/language/semantics/expressions/apply-expression.ts +++ b/src/language/semantics/expressions/apply-expression.ts @@ -1,26 +1,25 @@ import either, { type Either } from '@matt.kantor/either' import type { ElaborationError } from '../../errors.js' import type { Molecule } from '../../parsing.js' -import { isSpecificExpression } from '../expression.js' +import { isExpressionWithArgument } from '../expression.js' import { makeObjectNode, type ObjectNode } from '../object-node.js' import { type SemanticGraph } from '../semantic-graph.js' import { readArgumentsFromExpression } from './expression-utilities.js' export type ApplyExpression = ObjectNode & { readonly 0: '@apply' - readonly function: SemanticGraph | Molecule - readonly argument: SemanticGraph | Molecule + readonly 1: { + readonly function: SemanticGraph | Molecule + readonly argument: SemanticGraph | Molecule + } } export const readApplyExpression = ( node: SemanticGraph | Molecule, ): Either => - isSpecificExpression('@apply', node) + isExpressionWithArgument('@apply', node) ? either.map( - readArgumentsFromExpression(node, [ - ['function', '1'], - ['argument', '2'], - ]), + readArgumentsFromExpression(node, ['function', 'argument']), ([f, argument]) => makeApplyExpression({ function: f, argument }), ) : either.makeLeft({ @@ -37,6 +36,8 @@ export const makeApplyExpression = ({ }): ApplyExpression => makeObjectNode({ 0: '@apply', - function: f, - argument, + 1: makeObjectNode({ + function: f, + argument, + }), }) diff --git a/src/language/semantics/expressions/check-expression.ts b/src/language/semantics/expressions/check-expression.ts index 39b234a..51f91bb 100644 --- a/src/language/semantics/expressions/check-expression.ts +++ b/src/language/semantics/expressions/check-expression.ts @@ -1,26 +1,25 @@ import either, { type Either } from '@matt.kantor/either' import type { ElaborationError } from '../../errors.js' import type { Molecule } from '../../parsing.js' -import { isSpecificExpression } from '../expression.js' +import { isExpressionWithArgument } from '../expression.js' import { makeObjectNode, type ObjectNode } from '../object-node.js' import { type SemanticGraph } from '../semantic-graph.js' import { readArgumentsFromExpression } from './expression-utilities.js' export type CheckExpression = ObjectNode & { readonly 0: '@check' - readonly value: SemanticGraph | Molecule - readonly type: SemanticGraph | Molecule + readonly 1: { + readonly value: SemanticGraph | Molecule + readonly type: SemanticGraph | Molecule + } } export const readCheckExpression = ( node: SemanticGraph | Molecule, ): Either => - isSpecificExpression('@check', node) + isExpressionWithArgument('@check', node) ? either.map( - readArgumentsFromExpression(node, [ - ['value', '1'], - ['type', '2'], - ]), + readArgumentsFromExpression(node, ['value', 'type']), ([value, type]) => makeCheckExpression({ value, type }), ) : either.makeLeft({ @@ -37,6 +36,5 @@ export const makeCheckExpression = ({ }): CheckExpression => makeObjectNode({ 0: '@check', - value, - type, + 1: makeObjectNode({ value, type }), }) diff --git a/src/language/semantics/expressions/expression-utilities.ts b/src/language/semantics/expressions/expression-utilities.ts index a34b97a..7963e38 100644 --- a/src/language/semantics/expressions/expression-utilities.ts +++ b/src/language/semantics/expressions/expression-utilities.ts @@ -4,6 +4,7 @@ import type { ElaborationError } from '../../errors.js' import type { Atom, Molecule } from '../../parsing.js' import type { ExpressionContext } from '../expression-elaboration.js' import type { Expression } from '../expression.js' +import { isFunctionNode } from '../function-node.js' import { stringifyKeyPathForEndUser } from '../key-path.js' import { lookupPropertyOfObjectNode, @@ -33,46 +34,59 @@ export const locateSelf = (context: ExpressionContext) => }) export const readArgumentsFromExpression = < - const Specification extends readonly (readonly [ - string, - ...(readonly string[]), - ])[], + const Specification extends readonly string[], >( expression: Expression, specification: Specification, ): Either> => { - const expressionArguments: ObjectNode[string][] = [] - for (const aliases of specification) { - const argument = lookupWithinExpression(aliases, expression) - if (option.isNone(argument)) { - const requiredKeySummary = aliases - .map(alias => `\`${alias}\``) - .join(' or ') - return either.makeLeft({ - kind: 'invalidExpression', - message: `missing required property ${requiredKeySummary}`, - }) - } else { - expressionArguments.push(argument.value) + if (expression[1] === undefined) { + return either.makeLeft({ + kind: 'invalidExpression', + message: `missing arguments object`, + }) + } else if (typeof expression[1] === 'string') { + return either.makeLeft({ + kind: 'invalidExpression', + message: `found an atom instead of an arguments object`, + }) + } else if (isFunctionNode(expression[1])) { + return either.makeLeft({ + kind: 'invalidExpression', + message: `found a function instead of an arguments object`, + }) + } else { + const expressionArguments: ObjectNode[string][] = [] + for (const [position, keyword] of specification.entries()) { + const argument = lookupWithinMolecule( + [keyword, String(position)], + expression[1], + ) + if (option.isNone(argument)) { + const requiredKeySummary = `\`${keyword}\` or \`${position}\`` + return either.makeLeft({ + kind: 'invalidExpression', + message: `missing required property ${requiredKeySummary}`, + }) + } else { + expressionArguments.push(argument.value) + } } + return either.makeRight( + // This is correct since the above loop pushes one argument for each `specification` element. + expressionArguments as ParsedExpressionArguments, + ) } - return either.makeRight( - // This is correct since the above loop pushes one argument for each `specification` element. - expressionArguments as ParsedExpressionArguments, - ) } -type ParsedExpressionArguments< - Specification extends readonly (readonly [string, ...(readonly string[])])[], -> = { +type ParsedExpressionArguments = { [Index in keyof Specification]: ObjectNode[string] } -const lookupWithinExpression = ( +const lookupWithinMolecule = ( keyAliases: readonly [Atom, ...(readonly Atom[])], - expression: Expression, + molecule: Molecule | ObjectNode, ): Option => { for (const key of keyAliases) { - const result = lookupPropertyOfObjectNode(key, makeObjectNode(expression)) + const result = lookupPropertyOfObjectNode(key, makeObjectNode(molecule)) if (!option.isNone(result)) { return result } diff --git a/src/language/semantics/expressions/function-expression.ts b/src/language/semantics/expressions/function-expression.ts index 218d369..49506c9 100644 --- a/src/language/semantics/expressions/function-expression.ts +++ b/src/language/semantics/expressions/function-expression.ts @@ -1,7 +1,7 @@ import either, { type Either } from '@matt.kantor/either' import type { ElaborationError } from '../../errors.js' import type { Atom, Molecule } from '../../parsing.js' -import { isSpecificExpression } from '../expression.js' +import { isExpressionWithArgument } from '../expression.js' import { makeObjectNode, type ObjectNode } from '../object-node.js' import { serialize, type SemanticGraph } from '../semantic-graph.js' import { @@ -11,19 +11,18 @@ import { export type FunctionExpression = ObjectNode & { readonly 0: '@function' - readonly parameter: Atom - readonly body: SemanticGraph | Molecule + readonly 1: { + readonly parameter: Atom + readonly body: SemanticGraph | Molecule + } } export const readFunctionExpression = ( node: SemanticGraph | Molecule, ): Either => - isSpecificExpression('@function', node) + isExpressionWithArgument('@function', node) ? either.flatMap( - readArgumentsFromExpression(node, [ - ['parameter', '1'], - ['body', '2'], - ]), + readArgumentsFromExpression(node, ['parameter', 'body']), ([parameter, body]): Either => typeof parameter !== 'string' ? either.makeLeft({ @@ -45,6 +44,8 @@ export const makeFunctionExpression = ( ): FunctionExpression => makeObjectNode({ 0: '@function', - parameter, - body, + 1: makeObjectNode({ + parameter, + body, + }), }) diff --git a/src/language/semantics/expressions/if-expression.ts b/src/language/semantics/expressions/if-expression.ts index 353df72..04072b1 100644 --- a/src/language/semantics/expressions/if-expression.ts +++ b/src/language/semantics/expressions/if-expression.ts @@ -1,7 +1,7 @@ import either, { type Either } from '@matt.kantor/either' import type { ElaborationError } from '../../errors.js' import type { Molecule } from '../../parsing.js' -import { isSpecificExpression } from '../expression.js' +import { isExpressionWithArgument } from '../expression.js' import { makeObjectNode, type ObjectNode } from '../object-node.js' import { type SemanticGraph } from '../semantic-graph.js' import { readArgumentsFromExpression } from './expression-utilities.js' @@ -9,21 +9,19 @@ import { readArgumentsFromExpression } from './expression-utilities.js' // TODO: Evolve this into pattern matching/destructuring. export type IfExpression = ObjectNode & { readonly 0: '@if' - readonly condition: SemanticGraph | Molecule - readonly then: SemanticGraph | Molecule - readonly else: SemanticGraph | Molecule + readonly 1: { + readonly condition: SemanticGraph | Molecule + readonly then: SemanticGraph | Molecule + readonly else: SemanticGraph | Molecule + } } export const readIfExpression = ( node: SemanticGraph | Molecule, ): Either => - isSpecificExpression('@if', node) + isExpressionWithArgument('@if', node) ? either.map( - readArgumentsFromExpression(node, [ - ['condition', '1'], - ['then', '2'], - ['else', '3'], - ]), + readArgumentsFromExpression(node, ['condition', 'then', 'else']), ([condition, then, otherwise]) => makeIfExpression({ condition, then, else: otherwise }), ) @@ -43,7 +41,9 @@ export const makeIfExpression = ({ }): IfExpression => makeObjectNode({ 0: '@if', - condition, - then, - else: otherwise, + 1: makeObjectNode({ + condition, + then, + else: otherwise, + }), }) diff --git a/src/language/semantics/expressions/index-expression.ts b/src/language/semantics/expressions/index-expression.ts index 3e2b574..3d3ab13 100644 --- a/src/language/semantics/expressions/index-expression.ts +++ b/src/language/semantics/expressions/index-expression.ts @@ -1,7 +1,7 @@ import either, { type Either } from '@matt.kantor/either' import type { ElaborationError } from '../../errors.js' import type { Molecule } from '../../parsing.js' -import { isSpecificExpression } from '../expression.js' +import { isExpressionWithArgument } from '../expression.js' import { keyPathFromObjectNodeOrMolecule } from '../key-path.js' import { isObjectNode, @@ -16,19 +16,18 @@ import { export type IndexExpression = ObjectNode & { readonly 0: '@index' - readonly object: ObjectNode | Molecule - readonly query: ObjectNode | Molecule + readonly 1: { + readonly object: ObjectNode | Molecule + readonly query: ObjectNode | Molecule + } } export const readIndexExpression = ( node: SemanticGraph | Molecule, ): Either => - isSpecificExpression('@index', node) + isExpressionWithArgument('@index', node) ? either.flatMap( - readArgumentsFromExpression(node, [ - ['object', '1'], - ['query', '2'], - ]), + readArgumentsFromExpression(node, ['object', 'query']), ([o, q]) => { const object = asSemanticGraph(o) const query = asSemanticGraph(q) @@ -64,6 +63,8 @@ export const makeIndexExpression = ({ }): IndexExpression => makeObjectNode({ 0: '@index', - object, - query, + 1: makeObjectNode({ + object, + query, + }), }) diff --git a/src/language/semantics/expressions/lookup-expression.ts b/src/language/semantics/expressions/lookup-expression.ts index 9085c0f..6970663 100644 --- a/src/language/semantics/expressions/lookup-expression.ts +++ b/src/language/semantics/expressions/lookup-expression.ts @@ -1,7 +1,7 @@ import either, { type Either } from '@matt.kantor/either' import type { ElaborationError } from '../../errors.js' import type { Atom, Molecule } from '../../parsing.js' -import { isSpecificExpression } from '../expression.js' +import { isExpressionWithArgument } from '../expression.js' import { makeObjectNode, type ObjectNode } from '../object-node.js' import { stringifySemanticGraphForEndUser, @@ -14,28 +14,27 @@ import { export type LookupExpression = ObjectNode & { readonly 0: '@lookup' - readonly key: Atom + readonly 1: { + readonly key: Atom + } } export const readLookupExpression = ( node: SemanticGraph | Molecule, ): Either => - isSpecificExpression('@lookup', node) - ? either.flatMap( - readArgumentsFromExpression(node, [['key', '1']]), - ([key]) => { - if (typeof key !== 'string') { - return either.makeLeft({ - kind: 'invalidExpression', - message: `lookup key must be an atom, got \`${stringifySemanticGraphForEndUser( - asSemanticGraph(key), - )}\``, - }) - } else { - return either.makeRight(makeLookupExpression(key)) - } - }, - ) + isExpressionWithArgument('@lookup', node) + ? either.flatMap(readArgumentsFromExpression(node, ['key']), ([key]) => { + if (typeof key !== 'string') { + return either.makeLeft({ + kind: 'invalidExpression', + message: `lookup key must be an atom, got \`${stringifySemanticGraphForEndUser( + asSemanticGraph(key), + )}\``, + }) + } else { + return either.makeRight(makeLookupExpression(key)) + } + }) : either.makeLeft({ kind: 'invalidExpression', message: 'not an expression', @@ -44,5 +43,5 @@ export const readLookupExpression = ( export const makeLookupExpression = (key: Atom): LookupExpression => makeObjectNode({ 0: '@lookup', - key, + 1: makeObjectNode({ key }), }) diff --git a/src/language/semantics/expressions/runtime-expression.ts b/src/language/semantics/expressions/runtime-expression.ts index a70af86..bfb0703 100644 --- a/src/language/semantics/expressions/runtime-expression.ts +++ b/src/language/semantics/expressions/runtime-expression.ts @@ -1,7 +1,7 @@ import either, { type Either } from '@matt.kantor/either' import type { ElaborationError } from '../../errors.js' import type { Molecule } from '../../parsing.js' -import { isSpecificExpression } from '../expression.js' +import { isExpressionWithArgument } from '../expression.js' import { isFunctionNode } from '../function-node.js' import { makeObjectNode, type ObjectNode } from '../object-node.js' import { @@ -15,31 +15,28 @@ import { export type RuntimeExpression = ObjectNode & { readonly 0: '@runtime' - readonly function: SemanticGraph + readonly 1: { + readonly function: SemanticGraph + } } export const readRuntimeExpression = ( node: SemanticGraph | Molecule, ): Either => - isSpecificExpression('@runtime', node) - ? either.flatMap( - readArgumentsFromExpression(node, [['function', '1']]), - ([f]) => { - const runtimeFunction = asSemanticGraph(f) - if ( - !( - isFunctionNode(runtimeFunction) || containsAnyUnelaboratedNodes(f) - ) - ) { - return either.makeLeft({ - kind: 'invalidExpression', - message: 'runtime functions must compute something', - }) - } else { - return either.makeRight(makeRuntimeExpression(runtimeFunction)) - } - }, - ) + isExpressionWithArgument('@runtime', node) + ? either.flatMap(readArgumentsFromExpression(node, ['function']), ([f]) => { + const runtimeFunction = asSemanticGraph(f) + if ( + !(isFunctionNode(runtimeFunction) || containsAnyUnelaboratedNodes(f)) + ) { + return either.makeLeft({ + kind: 'invalidExpression', + message: 'runtime functions must compute something', + }) + } else { + return either.makeRight(makeRuntimeExpression(runtimeFunction)) + } + }) : either.makeLeft({ kind: 'invalidExpression', message: 'not an expression', @@ -48,5 +45,5 @@ export const readRuntimeExpression = ( export const makeRuntimeExpression = (f: SemanticGraph): RuntimeExpression => makeObjectNode({ 0: '@runtime', - function: f, + 1: makeObjectNode({ function: f }), }) diff --git a/src/language/semantics/function-node.ts b/src/language/semantics/function-node.ts index 179bae3..274eb19 100644 --- a/src/language/semantics/function-node.ts +++ b/src/language/semantics/function-node.ts @@ -6,7 +6,7 @@ import type { Panic, UnserializableValueError, } from '../errors.js' -import type { Atom } from '../parsing.js' +import type { Atom, Molecule } from '../parsing.js' import type { ObjectNode } from './object-node.js' import { nodeTag, @@ -25,7 +25,7 @@ export type FunctionNode = (( readonly serialize: () => Either } -export const isFunctionNode = (node: SemanticGraph) => +export const isFunctionNode = (node: Molecule | SemanticGraph) => typeof node === 'function' && node[nodeTag] === 'function' export const makeFunctionNode = ( diff --git a/src/language/unparsing/plz-utilities.ts b/src/language/unparsing/plz-utilities.ts index 6e7703d..3363e36 100644 --- a/src/language/unparsing/plz-utilities.ts +++ b/src/language/unparsing/plz-utilities.ts @@ -5,6 +5,7 @@ import type { UnserializableValueError } from '../errors.js' import type { Atom, Molecule } from '../parsing.js' import { unquotedAtomParser } from '../parsing/atom.js' import { + isExpression, isSemanticGraph, readApplyExpression, readFunctionExpression, @@ -12,6 +13,7 @@ import { readLookupExpression, serialize, type ApplyExpression, + type Expression, type FunctionExpression, type IndexExpression, type KeyPath, @@ -55,7 +57,19 @@ export const moleculeUnparser = unparseSugaredLookup(lookupExpression, unparseAtomOrMolecule), }) default: - return unparseSugarFreeMolecule(value, unparseAtomOrMolecule) + if (isExpression(value)) { + const result = unparseSugaredGeneralizedKeywordExpression( + value, + unparseAtomOrMolecule, + ) + if (either.isLeft(result)) { + return unparseSugarFreeMolecule(value, unparseAtomOrMolecule) + } else { + return result + } + } else { + return unparseSugarFreeMolecule(value, unparseAtomOrMolecule) + } } } @@ -101,10 +115,12 @@ export const unparseAtom = (atom: string): Right => : quoteAtomIfNecessary(atom), ) +const requiresQuotation = (atom: string): boolean => + either.isLeft(parsing.parse(unquotedAtomParser, atom)) + const quoteAtomIfNecessary = (value: string): string => { const { quote } = punctuation(kleur) - const unquotedAtomResult = parsing.parse(unquotedAtomParser, value) - if (either.isLeft(unquotedAtomResult)) { + if (requiresQuotation(value)) { return quote.concat(escapeStringContents(value)).concat(quote) } else { return value @@ -142,11 +158,11 @@ const unparseSugaredApply = ( const { closeParenthesis, openParenthesis } = punctuation(kleur) const functionUnparseResult = either.map( either.flatMap( - serializeIfNeeded(expression.function), + serializeIfNeeded(expression[1].function), unparseAtomOrMolecule, ), unparsedFunction => - either.isRight(readFunctionExpression(expression.function)) + either.isRight(readFunctionExpression(expression[1].function)) ? // Immediately-applied function expressions need parentheses. openParenthesis.concat(unparsedFunction).concat(closeParenthesis) : unparsedFunction, @@ -156,7 +172,7 @@ const unparseSugaredApply = ( } const argumentUnparseResult = either.flatMap( - serializeIfNeeded(expression.argument), + serializeIfNeeded(expression[1].argument), unparseAtomOrMolecule, ) if (either.isLeft(argumentUnparseResult)) { @@ -175,10 +191,10 @@ const unparseSugaredFunction = ( expression: FunctionExpression, unparseAtomOrMolecule: UnparseAtomOrMolecule, ) => - either.flatMap(serializeIfNeeded(expression.body), serializedBody => + either.flatMap(serializeIfNeeded(expression[1].body), serializedBody => either.map(unparseAtomOrMolecule(serializedBody), bodyAsString => [ - kleur.cyan(expression.parameter), + kleur.cyan(expression[1].parameter), punctuation(kleur).arrow, bodyAsString, ].join(' '), @@ -190,20 +206,20 @@ const unparseSugaredIndex = ( unparseAtomOrMolecule: UnparseAtomOrMolecule, ) => { const objectUnparseResult = either.flatMap( - serializeIfNeeded(expression.object), + serializeIfNeeded(expression[1].object), unparseAtomOrMolecule, ) if (either.isLeft(objectUnparseResult)) { return objectUnparseResult } else { - if (typeof expression.query !== 'object') { + if (typeof expression[1].query !== 'object') { // TODO: It would be nice if this were provably impossible. return either.makeLeft({ kind: 'unserializableValue', message: 'Invalid index expression', }) } else { - const keyPath = Object.entries(expression.query).reduce( + const keyPath = Object.entries(expression[1].query).reduce( (accumulator: KeyPath | 'invalid', [key, value]) => { if (accumulator === 'invalid') { return accumulator @@ -223,7 +239,7 @@ const unparseSugaredIndex = ( if ( keyPath === 'invalid' || - Object.keys(expression.query).length !== keyPath.length + Object.keys(expression[1].query).length !== keyPath.length ) { return either.makeLeft({ kind: 'unserializableValue', @@ -248,7 +264,42 @@ const unparseSugaredLookup = ( either.makeRight( kleur.cyan( punctuation(kleur).colon.concat( - quoteKeyPathComponentIfNecessary(expression.key), + quoteKeyPathComponentIfNecessary(expression[1].key), ), ), ) + +const unparseSugaredGeneralizedKeywordExpression = ( + expression: Expression, + unparseAtomOrMolecule: UnparseAtomOrMolecule, +) => { + if ( + // Not every valid keyword expression can be expressed with the + // generalized sugar, e.g. if there are any additional properties + // besides the keyword and its argument, or if the keyword requires + // quotation (which won't be the case for any built-in keywords, but + // maybe eventually users will be able to create custom keywords). + requiresQuotation(expression['0'].substring(1)) || + Object.keys(expression).some(key => key !== '0' && key !== '1') + ) { + return either.makeLeft({ + kind: 'unserializableValue', + message: + 'expression cannot be faithfully represented using generalized keyword expression sugar', + }) + } else { + const unparsedKeyword = kleur.bold(kleur.underline(expression['0'])) + if ('1' in expression) { + return either.map( + either.flatMap( + serializeIfNeeded(expression['1']), + unparseAtomOrMolecule, + ), + unparsedArgument => + unparsedKeyword.concat(' ').concat(unparsedArgument), + ) + } else { + return either.makeRight(unparsedKeyword) + } + } +}