Skip to content

Commit 2ed205e

Browse files
authored
Merge pull request #32 from mkantor/index-keyword
Add `@index` keyword expression
2 parents e327345 + af19fd2 commit 2ed205e

File tree

10 files changed

+181
-56
lines changed

10 files changed

+181
-56
lines changed

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -153,7 +153,7 @@ keyword expressions.
153153
Under the hood, keyword expressions are modeled as objects. For example, `:foo`
154154
desugars to `{@lookup query: foo}`. All such expressions have a key `0`
155155
referring to a value that is an `@`-prefixed atom (the keyword). Keywords
156-
include `@function`, `@lookup`, `@apply`, `@check`, and `@runtime`.
156+
include `@function`, `@lookup`, `@apply`, `@check`, `@index`, and `@runtime`.
157157

158158
Currently only `@function`, `@lookup`, and `@apply` have syntax sugars.
159159

src/language/compiling/semantics.test.ts

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -173,6 +173,34 @@ elaborationSuite('@check', [
173173
],
174174
])
175175

176+
elaborationSuite('@index', [
177+
[{ 0: '@index', 1: { foo: 'bar' }, 2: { 0: 'foo' } }, success('bar')],
178+
[
179+
{ 0: '@index', object: { foo: 'bar' }, query: { 0: 'foo' } },
180+
success('bar'),
181+
],
182+
[
183+
{
184+
0: '@index',
185+
object: { a: { b: { c: 'it works' } } },
186+
query: { 0: 'a', 1: 'b', 2: 'c' },
187+
},
188+
success('it works'),
189+
],
190+
[
191+
{
192+
0: '@index',
193+
object: { a: { b: { c: 'it works' } } },
194+
query: { 0: 'a', 1: 'b' },
195+
},
196+
success({ c: 'it works' }),
197+
],
198+
[
199+
{ 0: '@index', object: {}, query: { 0: 'thisPropertyDoesNotExist' } },
200+
output => assert(either.isLeft(output)),
201+
],
202+
])
203+
176204
elaborationSuite('@lookup', [
177205
[
178206
{
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
import either, { type Either } from '@matt.kantor/either'
2+
import option from '@matt.kantor/option'
3+
import type { ElaborationError } from '../../../errors.js'
4+
import {
5+
applyKeyPathToSemanticGraph,
6+
asSemanticGraph,
7+
keyPathFromObjectNodeOrMolecule,
8+
readIndexExpression,
9+
stringifyKeyPathForEndUser,
10+
type Expression,
11+
type ExpressionContext,
12+
type KeywordHandler,
13+
type SemanticGraph,
14+
} from '../../../semantics.js'
15+
16+
export const indexKeywordHandler: KeywordHandler = (
17+
expression: Expression,
18+
_context: ExpressionContext,
19+
): Either<ElaborationError, SemanticGraph> =>
20+
either.flatMap(readIndexExpression(expression), ({ object, query }) =>
21+
either.flatMap(keyPathFromObjectNodeOrMolecule(query), keyPath =>
22+
option.match(
23+
applyKeyPathToSemanticGraph(asSemanticGraph(object), keyPath),
24+
{
25+
none: () =>
26+
either.makeLeft({
27+
kind: 'invalidExpression',
28+
message: `property \`${stringifyKeyPathForEndUser(
29+
keyPath,
30+
)}\` not found`,
31+
}),
32+
some: either.makeRight,
33+
},
34+
),
35+
),
36+
)

src/language/compiling/semantics/keyword-handlers/lookup-handler.ts

Lines changed: 3 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,10 @@
11
import either, { type Either } from '@matt.kantor/either'
22
import option, { type Option } from '@matt.kantor/option'
3-
import type {
4-
ElaborationError,
5-
InvalidExpressionError,
6-
} from '../../../errors.js'
7-
import type { Molecule } from '../../../parsing.js'
3+
import type { ElaborationError } from '../../../errors.js'
84
import {
95
applyKeyPathToSemanticGraph,
106
isObjectNode,
7+
keyPathFromObjectNodeOrMolecule,
118
keyPathToMolecule,
129
makeLookupExpression,
1310
makeObjectNode,
@@ -19,7 +16,6 @@ import {
1916
type ExpressionContext,
2017
type KeyPath,
2118
type KeywordHandler,
22-
type ObjectNode,
2319
type SemanticGraph,
2420
} from '../../../semantics.js'
2521

@@ -28,7 +24,7 @@ export const lookupKeywordHandler: KeywordHandler = (
2824
context: ExpressionContext,
2925
): Either<ElaborationError, SemanticGraph> =>
3026
either.flatMap(readLookupExpression(expression), ({ query }) =>
31-
either.flatMap(keyPathFromObject(query), relativePath => {
27+
either.flatMap(keyPathFromObjectNodeOrMolecule(query), relativePath => {
3228
if (isObjectNode(context.program)) {
3329
return either.flatMap(
3430
lookup({
@@ -56,28 +52,6 @@ export const lookupKeywordHandler: KeywordHandler = (
5652
}),
5753
)
5854

59-
const keyPathFromObject = (
60-
node: ObjectNode | Molecule,
61-
): Either<InvalidExpressionError, KeyPath> => {
62-
const relativePath: string[] = []
63-
let queryIndex = 0
64-
// Consume numeric indexes ("0", "1", …) until exhausted, validating that each is an atom.
65-
let key = node[queryIndex]
66-
while (key !== undefined) {
67-
if (typeof key !== 'string') {
68-
return either.makeLeft({
69-
kind: 'invalidExpression',
70-
message: 'query must be a key path composed of sequential atoms',
71-
})
72-
} else {
73-
relativePath.push(key)
74-
}
75-
queryIndex++
76-
key = node[queryIndex]
77-
}
78-
return either.makeRight(relativePath)
79-
}
80-
8155
const lookup = ({
8256
context,
8357
relativePath,

src/language/compiling/semantics/keywords.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { type KeywordHandlers } from '../../semantics.js'
22
import { applyKeywordHandler } from './keyword-handlers/apply-handler.js'
33
import { checkKeywordHandler } from './keyword-handlers/check-handler.js'
44
import { functionKeywordHandler } from './keyword-handlers/function-handler.js'
5+
import { indexKeywordHandler } from './keyword-handlers/index-handler.js'
56
import { lookupKeywordHandler } from './keyword-handlers/lookup-handler.js'
67
import { runtimeKeywordHandler } from './keyword-handlers/runtime-handler.js'
78
import { todoKeywordHandler } from './keyword-handlers/todo-handler.js'
@@ -22,6 +23,11 @@ export const keywordHandlers: KeywordHandlers = {
2223
*/
2324
'@function': functionKeywordHandler,
2425

26+
/**
27+
* Returns the value of a property within an object.
28+
*/
29+
'@index': indexKeywordHandler,
30+
2531
/**
2632
* Given a query, resolves the value of a property within the program.
2733
*/

src/language/semantics.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,11 @@ export {
2727
readFunctionExpression,
2828
type FunctionExpression,
2929
} from './semantics/expressions/function-expression.js'
30+
export {
31+
makeIndexExpression,
32+
readIndexExpression,
33+
type IndexExpression,
34+
} from './semantics/expressions/index-expression.js'
3035
export {
3136
makeLookupExpression,
3237
readLookupExpression,
@@ -44,6 +49,7 @@ export {
4449
type FunctionNode,
4550
} from './semantics/function-node.js'
4651
export {
52+
keyPathFromObjectNodeOrMolecule,
4753
keyPathToMolecule,
4854
stringifyKeyPathForEndUser,
4955
type KeyPath,
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
import either, { type Either } from '@matt.kantor/either'
2+
import type { ElaborationError } from '../../errors.js'
3+
import type { Molecule } from '../../parsing.js'
4+
import { isSpecificExpression } from '../expression.js'
5+
import { keyPathFromObjectNodeOrMolecule } from '../key-path.js'
6+
import {
7+
isObjectNode,
8+
makeUnelaboratedObjectNode,
9+
type ObjectNode,
10+
} from '../object-node.js'
11+
import { type SemanticGraph, type unelaboratedKey } from '../semantic-graph.js'
12+
import {
13+
asSemanticGraph,
14+
readArgumentsFromExpression,
15+
} from './expression-utilities.js'
16+
17+
export type IndexExpression = ObjectNode & {
18+
readonly 0: '@index'
19+
readonly object: ObjectNode | Molecule
20+
readonly query: ObjectNode | Molecule
21+
}
22+
23+
export const readIndexExpression = (
24+
node: SemanticGraph | Molecule,
25+
): Either<ElaborationError, IndexExpression> =>
26+
isSpecificExpression('@index', node)
27+
? either.flatMap(
28+
readArgumentsFromExpression(node, [
29+
['object', '1'],
30+
['query', '2'],
31+
]),
32+
([o, q]) => {
33+
const object = asSemanticGraph(o)
34+
const query = asSemanticGraph(q)
35+
if (!isObjectNode(object)) {
36+
return either.makeLeft({
37+
kind: 'invalidExpression',
38+
message: 'object must be an object',
39+
})
40+
} else if (!isObjectNode(query)) {
41+
return either.makeLeft({
42+
kind: 'invalidExpression',
43+
message: 'query must be an object',
44+
})
45+
} else {
46+
return either.map(
47+
keyPathFromObjectNodeOrMolecule(query),
48+
_validKeyPath => makeIndexExpression({ object, query }),
49+
)
50+
}
51+
},
52+
)
53+
: either.makeLeft({
54+
kind: 'invalidExpression',
55+
message: 'not an expression',
56+
})
57+
58+
export const makeIndexExpression = ({
59+
query,
60+
object,
61+
}: {
62+
readonly query: ObjectNode | Molecule
63+
readonly object: ObjectNode | Molecule
64+
}): IndexExpression & { readonly [unelaboratedKey]: true } =>
65+
makeUnelaboratedObjectNode({
66+
0: '@index',
67+
object,
68+
query,
69+
})

src/language/semantics/expressions/lookup-expression.ts

Lines changed: 6 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,12 @@
11
import either, { type Either } from '@matt.kantor/either'
2-
import type { ElaborationError, InvalidExpressionError } from '../../errors.js'
2+
import type { ElaborationError } from '../../errors.js'
33
import type { Molecule } from '../../parsing.js'
44
import { isSpecificExpression } from '../expression.js'
55
import { isFunctionNode } from '../function-node.js'
6-
import { keyPathToMolecule, type KeyPath } from '../key-path.js'
6+
import {
7+
keyPathFromObjectNodeOrMolecule,
8+
keyPathToMolecule,
9+
} from '../key-path.js'
710
import {
811
makeObjectNode,
912
makeUnelaboratedObjectNode,
@@ -40,7 +43,7 @@ export const readLookupExpression = (
4043
: query
4144

4245
return either.map(
43-
keyPathFromObjectNode(canonicalizedQuery),
46+
keyPathFromObjectNodeOrMolecule(canonicalizedQuery),
4447
_keyPath => makeLookupExpression(canonicalizedQuery),
4548
)
4649
}
@@ -58,25 +61,3 @@ export const makeLookupExpression = (
5861
0: '@lookup',
5962
query,
6063
})
61-
62-
const keyPathFromObjectNode = (
63-
node: ObjectNode,
64-
): Either<InvalidExpressionError, KeyPath> => {
65-
const relativePath: string[] = []
66-
let queryIndex = 0
67-
// Consume numeric indexes ("0", "1", …) until exhausted, validating that each is an atom.
68-
let key = node[queryIndex]
69-
while (key !== undefined) {
70-
if (typeof key !== 'string') {
71-
return either.makeLeft({
72-
kind: 'invalidExpression',
73-
message: 'query must be a key path composed of sequential atoms',
74-
})
75-
} else {
76-
relativePath.push(key)
77-
}
78-
queryIndex++
79-
key = node[queryIndex]
80-
}
81-
return either.makeRight(relativePath)
82-
}

src/language/semantics/key-path.ts

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
1-
import either from '@matt.kantor/either'
1+
import either, { type Either } from '@matt.kantor/either'
2+
import type { InvalidExpressionError } from '../errors.js'
23
import type { Atom, Molecule } from '../parsing.js'
34
import { inlinePlz, unparse } from '../unparsing.js'
5+
import type { ObjectNode } from './object-node.js'
46

57
export type KeyPath = readonly Atom[]
68

@@ -12,3 +14,25 @@ export const stringifyKeyPathForEndUser = (keyPath: KeyPath): string =>
1214

1315
export const keyPathToMolecule = (keyPath: KeyPath): Molecule =>
1416
Object.fromEntries(keyPath.flatMap((key, index) => [[index, key]]))
17+
18+
export const keyPathFromObjectNodeOrMolecule = (
19+
node: ObjectNode | Molecule,
20+
): Either<InvalidExpressionError, KeyPath> => {
21+
const relativePath: string[] = []
22+
let queryIndex = 0
23+
// Consume numeric indexes ("0", "1", …) until exhausted, validating that each is an atom.
24+
let key = node[queryIndex]
25+
while (key !== undefined) {
26+
if (typeof key !== 'string') {
27+
return either.makeLeft({
28+
kind: 'invalidExpression',
29+
message: 'expected a key path composed of sequential atoms',
30+
})
31+
} else {
32+
relativePath.push(key)
33+
}
34+
queryIndex++
35+
key = node[queryIndex]
36+
}
37+
return either.makeRight(relativePath)
38+
}

src/language/semantics/keyword.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ export const isKeyword = (input: string) =>
22
input === '@apply' ||
33
input === '@check' ||
44
input === '@function' ||
5+
input === '@index' ||
56
input === '@lookup' ||
67
input === '@runtime' ||
78
input === '@todo'

0 commit comments

Comments
 (0)