Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -153,7 +153,7 @@ keyword expressions.
Under the hood, keyword expressions are modeled as objects. For example, `:foo`
desugars to `{@lookup query: foo}`. All such expressions have a key `0`
referring to a value that is an `@`-prefixed atom (the keyword). Keywords
include `@function`, `@lookup`, `@apply`, `@check`, and `@runtime`.
include `@function`, `@lookup`, `@apply`, `@check`, `@index`, and `@runtime`.

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

Expand Down
28 changes: 28 additions & 0 deletions src/language/compiling/semantics.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -173,6 +173,34 @@ elaborationSuite('@check', [
],
])

elaborationSuite('@index', [
[{ 0: '@index', 1: { foo: 'bar' }, 2: { 0: 'foo' } }, success('bar')],
[
{ 0: '@index', object: { foo: 'bar' }, query: { 0: 'foo' } },
success('bar'),
],
[
{
0: '@index',
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' },
},
success({ c: 'it works' }),
],
[
{ 0: '@index', object: {}, query: { 0: 'thisPropertyDoesNotExist' } },
output => assert(either.isLeft(output)),
],
])

elaborationSuite('@lookup', [
[
{
Expand Down
36 changes: 36 additions & 0 deletions src/language/compiling/semantics/keyword-handlers/index-handler.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import either, { type Either } from '@matt.kantor/either'
import option from '@matt.kantor/option'
import type { ElaborationError } from '../../../errors.js'
import {
applyKeyPathToSemanticGraph,
asSemanticGraph,
keyPathFromObjectNodeOrMolecule,
readIndexExpression,
stringifyKeyPathForEndUser,
type Expression,
type ExpressionContext,
type KeywordHandler,
type SemanticGraph,
} from '../../../semantics.js'

export const indexKeywordHandler: KeywordHandler = (
expression: Expression,
_context: ExpressionContext,
): Either<ElaborationError, SemanticGraph> =>
either.flatMap(readIndexExpression(expression), ({ object, query }) =>
either.flatMap(keyPathFromObjectNodeOrMolecule(query), keyPath =>
option.match(
applyKeyPathToSemanticGraph(asSemanticGraph(object), keyPath),
{
none: () =>
either.makeLeft({
kind: 'invalidExpression',
message: `property \`${stringifyKeyPathForEndUser(
keyPath,
)}\` not found`,
}),
some: either.makeRight,
},
),
),
)
Original file line number Diff line number Diff line change
@@ -1,13 +1,10 @@
import either, { type Either } from '@matt.kantor/either'
import option, { type Option } from '@matt.kantor/option'
import type {
ElaborationError,
InvalidExpressionError,
} from '../../../errors.js'
import type { Molecule } from '../../../parsing.js'
import type { ElaborationError } from '../../../errors.js'
import {
applyKeyPathToSemanticGraph,
isObjectNode,
keyPathFromObjectNodeOrMolecule,
keyPathToMolecule,
makeLookupExpression,
makeObjectNode,
Expand All @@ -19,7 +16,6 @@ import {
type ExpressionContext,
type KeyPath,
type KeywordHandler,
type ObjectNode,
type SemanticGraph,
} from '../../../semantics.js'

Expand All @@ -28,7 +24,7 @@ export const lookupKeywordHandler: KeywordHandler = (
context: ExpressionContext,
): Either<ElaborationError, SemanticGraph> =>
either.flatMap(readLookupExpression(expression), ({ query }) =>
either.flatMap(keyPathFromObject(query), relativePath => {
either.flatMap(keyPathFromObjectNodeOrMolecule(query), relativePath => {
if (isObjectNode(context.program)) {
return either.flatMap(
lookup({
Expand Down Expand Up @@ -56,28 +52,6 @@ export const lookupKeywordHandler: KeywordHandler = (
}),
)

const keyPathFromObject = (
node: ObjectNode | Molecule,
): Either<InvalidExpressionError, KeyPath> => {
const relativePath: string[] = []
let queryIndex = 0
// Consume numeric indexes ("0", "1", …) until exhausted, validating that each is an atom.
let key = node[queryIndex]
while (key !== undefined) {
if (typeof key !== 'string') {
return either.makeLeft({
kind: 'invalidExpression',
message: 'query must be a key path composed of sequential atoms',
})
} else {
relativePath.push(key)
}
queryIndex++
key = node[queryIndex]
}
return either.makeRight(relativePath)
}

const lookup = ({
context,
relativePath,
Expand Down
6 changes: 6 additions & 0 deletions src/language/compiling/semantics/keywords.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { type KeywordHandlers } from '../../semantics.js'
import { applyKeywordHandler } from './keyword-handlers/apply-handler.js'
import { checkKeywordHandler } from './keyword-handlers/check-handler.js'
import { functionKeywordHandler } from './keyword-handlers/function-handler.js'
import { indexKeywordHandler } from './keyword-handlers/index-handler.js'
import { lookupKeywordHandler } from './keyword-handlers/lookup-handler.js'
import { runtimeKeywordHandler } from './keyword-handlers/runtime-handler.js'
import { todoKeywordHandler } from './keyword-handlers/todo-handler.js'
Expand All @@ -22,6 +23,11 @@ export const keywordHandlers: KeywordHandlers = {
*/
'@function': functionKeywordHandler,

/**
* Returns the value of a property within an object.
*/
'@index': indexKeywordHandler,

/**
* Given a query, resolves the value of a property within the program.
*/
Expand Down
6 changes: 6 additions & 0 deletions src/language/semantics.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,11 @@ export {
readFunctionExpression,
type FunctionExpression,
} from './semantics/expressions/function-expression.js'
export {
makeIndexExpression,
readIndexExpression,
type IndexExpression,
} from './semantics/expressions/index-expression.js'
export {
makeLookupExpression,
readLookupExpression,
Expand All @@ -44,6 +49,7 @@ export {
type FunctionNode,
} from './semantics/function-node.js'
export {
keyPathFromObjectNodeOrMolecule,
keyPathToMolecule,
stringifyKeyPathForEndUser,
type KeyPath,
Expand Down
69 changes: 69 additions & 0 deletions src/language/semantics/expressions/index-expression.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
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 { keyPathFromObjectNodeOrMolecule } from '../key-path.js'
import {
isObjectNode,
makeUnelaboratedObjectNode,
type ObjectNode,
} from '../object-node.js'
import { type SemanticGraph, type unelaboratedKey } from '../semantic-graph.js'
import {
asSemanticGraph,
readArgumentsFromExpression,
} from './expression-utilities.js'

export type IndexExpression = ObjectNode & {
readonly 0: '@index'
readonly object: ObjectNode | Molecule
readonly query: ObjectNode | Molecule
}

export const readIndexExpression = (
node: SemanticGraph | Molecule,
): Either<ElaborationError, IndexExpression> =>
isSpecificExpression('@index', node)
? either.flatMap(
readArgumentsFromExpression(node, [
['object', '1'],
['query', '2'],
]),
([o, q]) => {
const object = asSemanticGraph(o)
const query = asSemanticGraph(q)
if (!isObjectNode(object)) {
return either.makeLeft({
kind: 'invalidExpression',
message: 'object must be an object',
})
} else if (!isObjectNode(query)) {
return either.makeLeft({
kind: 'invalidExpression',
message: 'query must be an object',
})
} else {
return either.map(
keyPathFromObjectNodeOrMolecule(query),
_validKeyPath => makeIndexExpression({ object, query }),
)
}
},
)
: either.makeLeft({
kind: 'invalidExpression',
message: 'not an expression',
})

export const makeIndexExpression = ({
query,
object,
}: {
readonly query: ObjectNode | Molecule
readonly object: ObjectNode | Molecule
}): IndexExpression & { readonly [unelaboratedKey]: true } =>
makeUnelaboratedObjectNode({
0: '@index',
object,
query,
})
31 changes: 6 additions & 25 deletions src/language/semantics/expressions/lookup-expression.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
import either, { type Either } from '@matt.kantor/either'
import type { ElaborationError, InvalidExpressionError } from '../../errors.js'
import type { ElaborationError } from '../../errors.js'
import type { Molecule } from '../../parsing.js'
import { isSpecificExpression } from '../expression.js'
import { isFunctionNode } from '../function-node.js'
import { keyPathToMolecule, type KeyPath } from '../key-path.js'
import {
keyPathFromObjectNodeOrMolecule,
keyPathToMolecule,
} from '../key-path.js'
import {
makeObjectNode,
makeUnelaboratedObjectNode,
Expand Down Expand Up @@ -40,7 +43,7 @@ export const readLookupExpression = (
: query

return either.map(
keyPathFromObjectNode(canonicalizedQuery),
keyPathFromObjectNodeOrMolecule(canonicalizedQuery),
_keyPath => makeLookupExpression(canonicalizedQuery),
)
}
Expand All @@ -58,25 +61,3 @@ export const makeLookupExpression = (
0: '@lookup',
query,
})

const keyPathFromObjectNode = (
node: ObjectNode,
): Either<InvalidExpressionError, KeyPath> => {
const relativePath: string[] = []
let queryIndex = 0
// Consume numeric indexes ("0", "1", …) until exhausted, validating that each is an atom.
let key = node[queryIndex]
while (key !== undefined) {
if (typeof key !== 'string') {
return either.makeLeft({
kind: 'invalidExpression',
message: 'query must be a key path composed of sequential atoms',
})
} else {
relativePath.push(key)
}
queryIndex++
key = node[queryIndex]
}
return either.makeRight(relativePath)
}
26 changes: 25 additions & 1 deletion src/language/semantics/key-path.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import either from '@matt.kantor/either'
import either, { type Either } from '@matt.kantor/either'
import type { InvalidExpressionError } from '../errors.js'
import type { Atom, Molecule } from '../parsing.js'
import { inlinePlz, unparse } from '../unparsing.js'
import type { ObjectNode } from './object-node.js'

export type KeyPath = readonly Atom[]

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

export const keyPathToMolecule = (keyPath: KeyPath): Molecule =>
Object.fromEntries(keyPath.flatMap((key, index) => [[index, key]]))

export const keyPathFromObjectNodeOrMolecule = (
node: ObjectNode | Molecule,
): Either<InvalidExpressionError, KeyPath> => {
const relativePath: string[] = []
let queryIndex = 0
// Consume numeric indexes ("0", "1", …) until exhausted, validating that each is an atom.
let key = node[queryIndex]
while (key !== undefined) {
if (typeof key !== 'string') {
return either.makeLeft({
kind: 'invalidExpression',
message: 'expected a key path composed of sequential atoms',
})
} else {
relativePath.push(key)
}
queryIndex++
key = node[queryIndex]
}
return either.makeRight(relativePath)
}
1 change: 1 addition & 0 deletions src/language/semantics/keyword.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ export const isKeyword = (input: string) =>
input === '@apply' ||
input === '@check' ||
input === '@function' ||
input === '@index' ||
input === '@lookup' ||
input === '@runtime' ||
input === '@todo'
Expand Down