diff --git a/src/language/compiling.ts b/src/language/compiling.ts index 4c5e90c..0ad810f 100644 --- a/src/language/compiling.ts +++ b/src/language/compiling.ts @@ -1,2 +1,2 @@ export { compile } from './compiling/compiler.js' -export { handlers as keywordHandlers } from './compiling/semantics/keywords.js' +export { keywordHandlers } from './compiling/semantics/keywords.js' diff --git a/src/language/compiling/compiler.ts b/src/language/compiling/compiler.ts index 4414b3a..f6ec925 100644 --- a/src/language/compiling/compiler.ts +++ b/src/language/compiling/compiler.ts @@ -3,12 +3,12 @@ import type { CompilationError } from '../errors.js' import type { JSONValueForbiddingSymbolicKeys } from '../parsing.js' import { canonicalize } from '../parsing.js' import { elaborate, serialize, type Output } from '../semantics.js' -import * as keywordModule from './semantics/keywords.js' +import { keywordHandlers } from './semantics/keywords.js' export const compile = ( input: JSONValueForbiddingSymbolicKeys, ): Either => { const syntaxTree = canonicalize(input) - const semanticGraphResult = elaborate(syntaxTree, keywordModule) + const semanticGraphResult = elaborate(syntaxTree, keywordHandlers) return either.flatMap(semanticGraphResult, serialize) } diff --git a/src/language/compiling/semantics.test.ts b/src/language/compiling/semantics.test.ts index 7bf5ba0..3db73b3 100644 --- a/src/language/compiling/semantics.test.ts +++ b/src/language/compiling/semantics.test.ts @@ -12,13 +12,13 @@ import { type ElaboratedSemanticGraph, type ObjectNode, } from '../semantics.js' +import { prelude } from '../semantics/prelude.js' import type { SemanticGraph } from '../semantics/semantic-graph.js' -import * as keywordModule from './semantics/keywords.js' -import { prelude } from './semantics/prelude.js' +import { keywordHandlers } from './semantics/keywords.js' const elaborationSuite = testCases( (input: Atom | Molecule) => - elaborate(withPhantomData()(input), keywordModule), + elaborate(withPhantomData()(input), keywordHandlers), input => `elaborating expressions in \`${JSON.stringify(input)}\``, ) diff --git a/src/language/compiling/semantics/expressions/runtime-expression.ts b/src/language/compiling/semantics/expressions/runtime-expression.ts deleted file mode 100644 index f26045e..0000000 --- a/src/language/compiling/semantics/expressions/runtime-expression.ts +++ /dev/null @@ -1,91 +0,0 @@ -import { either, type Either } from '../../../../adts.js' -import type { ElaborationError } from '../../../errors.js' -import { - isAssignable, - isFunctionNode, - replaceAllTypeParametersWithTheirConstraints, - types, -} from '../../../semantics.js' -import { - isExpression, - type Expression, - type ExpressionContext, - type KeywordHandler, -} from '../../../semantics/expression-elaboration.js' -import { makeUnelaboratedObjectNode } from '../../../semantics/object-node.js' -import { - containsAnyUnelaboratedNodes, - type SemanticGraph, - type unelaboratedKey, -} from '../../../semantics/semantic-graph.js' -import { - asSemanticGraph, - readArgumentsFromExpression, -} from './expression-utilities.js' - -export const runtimeKeyword = '@runtime' - -export type RuntimeExpression = Expression & { - readonly 0: '@runtime' - readonly function: SemanticGraph -} - -export const readRuntimeExpression = ( - node: SemanticGraph, -): Either => - isExpression(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)) - } - }, - ) - : either.makeLeft({ - kind: 'invalidExpression', - message: 'not an expression', - }) - -export const makeRuntimeExpression = ( - f: SemanticGraph, -): RuntimeExpression & { readonly [unelaboratedKey]: true } => - makeUnelaboratedObjectNode({ - 0: '@runtime', - function: f, - }) - -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)) - } - }) diff --git a/src/language/compiling/semantics/expressions/apply-expression.ts b/src/language/compiling/semantics/keyword-handlers/apply-handler.ts similarity index 55% rename from src/language/compiling/semantics/expressions/apply-expression.ts rename to src/language/compiling/semantics/keyword-handlers/apply-handler.ts index bd87fee..555abc4 100644 --- a/src/language/compiling/semantics/expressions/apply-expression.ts +++ b/src/language/compiling/semantics/keyword-handlers/apply-handler.ts @@ -1,60 +1,15 @@ import { either, type Either } from '../../../../adts.js' import type { ElaborationError } from '../../../errors.js' -import type { Molecule } from '../../../parsing.js' -import { isFunctionNode } from '../../../semantics.js' import { - isExpression, + asSemanticGraph, + containsAnyUnelaboratedNodes, + isFunctionNode, + readApplyExpression, type Expression, type ExpressionContext, type KeywordHandler, -} from '../../../semantics/expression-elaboration.js' -import { makeUnelaboratedObjectNode } from '../../../semantics/object-node.js' -import { - containsAnyUnelaboratedNodes, type SemanticGraph, - type unelaboratedKey, -} from '../../../semantics/semantic-graph.js' -import { - asSemanticGraph, - readArgumentsFromExpression, -} from './expression-utilities.js' - -export const applyKeyword = '@apply' - -export type ApplyExpression = Expression & { - readonly 0: '@apply' - readonly function: SemanticGraph | Molecule - readonly argument: SemanticGraph | Molecule -} - -export const readApplyExpression = ( - node: SemanticGraph, -): Either => - isExpression(node) - ? either.map( - readArgumentsFromExpression(node, [ - ['function', '1'], - ['argument', '2'], - ]), - ([f, argument]) => makeApplyExpression({ function: f, argument }), - ) - : either.makeLeft({ - kind: 'invalidExpression', - message: 'not an expression', - }) - -export const makeApplyExpression = ({ - function: f, - argument, -}: { - readonly function: SemanticGraph | Molecule - readonly argument: SemanticGraph | Molecule -}): ApplyExpression & { readonly [unelaboratedKey]: true } => - makeUnelaboratedObjectNode({ - 0: '@apply', - function: f, - argument, - }) +} from '../../../semantics.js' export const applyKeywordHandler: KeywordHandler = ( expression: Expression, diff --git a/src/language/compiling/semantics/expressions/check-expression.ts b/src/language/compiling/semantics/keyword-handlers/check-handler.ts similarity index 74% rename from src/language/compiling/semantics/expressions/check-expression.ts rename to src/language/compiling/semantics/keyword-handlers/check-handler.ts index d5f8464..556a86b 100644 --- a/src/language/compiling/semantics/expressions/check-expression.ts +++ b/src/language/compiling/semantics/keyword-handlers/check-handler.ts @@ -1,63 +1,16 @@ import { either, option, type Either } from '../../../../adts.js' import type { ElaborationError } from '../../../errors.js' -import type { Molecule } from '../../../parsing.js' -import { isFunctionNode } from '../../../semantics.js' import { - isExpression, + asSemanticGraph, + isFunctionNode, + lookupPropertyOfObjectNode, + readCheckExpression, + stringifySemanticGraphForEndUser, type Expression, type ExpressionContext, type KeywordHandler, -} from '../../../semantics/expression-elaboration.js' -import { - lookupPropertyOfObjectNode, - makeUnelaboratedObjectNode, -} from '../../../semantics/object-node.js' -import { - stringifySemanticGraphForEndUser, type SemanticGraph, - type unelaboratedKey, -} from '../../../semantics/semantic-graph.js' -import { - asSemanticGraph, - readArgumentsFromExpression, -} from './expression-utilities.js' - -export const checkKeyword = '@check' - -export type CheckExpression = Expression & { - readonly 0: '@check' - readonly value: SemanticGraph | Molecule - readonly type: SemanticGraph | Molecule -} - -export const readCheckExpression = ( - node: SemanticGraph, -): Either => - isExpression(node) - ? either.map( - readArgumentsFromExpression(node, [ - ['value', '1'], - ['type', '2'], - ]), - ([value, type]) => makeCheckExpression({ value, type }), - ) - : either.makeLeft({ - kind: 'invalidExpression', - message: 'not an expression', - }) - -export const makeCheckExpression = ({ - value, - type, -}: { - value: SemanticGraph | Molecule - type: SemanticGraph | Molecule -}): CheckExpression & { readonly [unelaboratedKey]: true } => - makeUnelaboratedObjectNode({ - 0: '@check', - value, - type, - }) +} from '../../../semantics.js' export const checkKeywordHandler: KeywordHandler = ( expression: Expression, diff --git a/src/language/compiling/semantics/expressions/function-expression.ts b/src/language/compiling/semantics/keyword-handlers/function-handler.ts similarity index 50% rename from src/language/compiling/semantics/expressions/function-expression.ts rename to src/language/compiling/semantics/keyword-handlers/function-handler.ts index 8a50962..e8679f7 100644 --- a/src/language/compiling/semantics/expressions/function-expression.ts +++ b/src/language/compiling/semantics/keyword-handlers/function-handler.ts @@ -1,75 +1,21 @@ import { either, option, type Either } from '../../../../adts.js' import type { ElaborationError } from '../../../errors.js' -import type { Atom, Molecule } from '../../../parsing.js' import { + asSemanticGraph, + elaborateWithContext, makeFunctionNode, + makeObjectNode, + readFunctionExpression, serialize, types, - type FunctionNode, -} from '../../../semantics.js' -import { - elaborateWithContext, - isExpression, + updateValueAtKeyPathInSemanticGraph, type Expression, type ExpressionContext, + type FunctionExpression, + type FunctionNode, type KeywordHandler, -} from '../../../semantics/expression-elaboration.js' -import { - makeObjectNode, - makeUnelaboratedObjectNode, -} from '../../../semantics/object-node.js' -import { - updateValueAtKeyPathInSemanticGraph, type SemanticGraph, - type unelaboratedKey, -} from '../../../semantics/semantic-graph.js' -import { handlers, isKeyword } from '../keywords.js' -import { - asSemanticGraph, - readArgumentsFromExpression, -} from './expression-utilities.js' - -export const functionKeyword = '@function' - -export type FunctionExpression = Expression & { - readonly 0: '@function' - readonly parameter: Atom - readonly body: SemanticGraph | Molecule -} - -export const readFunctionExpression = ( - node: SemanticGraph, -): Either => - isExpression(node) - ? either.flatMap( - readArgumentsFromExpression(node, [ - ['parameter', '1'], - ['body', '2'], - ]), - ([parameter, body]): Either => - typeof parameter !== 'string' - ? either.makeLeft({ - kind: 'invalidExpression', - message: 'parameter name must be an atom', - }) - : either.map(serialize(asSemanticGraph(body)), body => - makeFunctionExpression(parameter, body), - ), - ) - : either.makeLeft({ - kind: 'invalidExpression', - message: 'not an expression', - }) - -export const makeFunctionExpression = ( - parameter: Atom, - body: SemanticGraph | Molecule, -): FunctionExpression & { readonly [unelaboratedKey]: true } => - makeUnelaboratedObjectNode({ - 0: '@function', - parameter, - body, - }) +} from '../../../semantics.js' export const functionKeywordHandler: KeywordHandler = ( expression: Expression, @@ -115,14 +61,11 @@ const apply = ( }), ), updatedProgram => - elaborateWithContext( - serializedBody, - { handlers, isKeyword }, - { - location: [...context.location, returnKey], - program: updatedProgram, - }, - ), + elaborateWithContext(serializedBody, { + keywordHandlers: context.keywordHandlers, + location: [...context.location, returnKey], + program: updatedProgram, + }), ), ) diff --git a/src/language/compiling/semantics/expressions/lookup-expression.ts b/src/language/compiling/semantics/keyword-handlers/lookup-handler.ts similarity index 75% rename from src/language/compiling/semantics/expressions/lookup-expression.ts rename to src/language/compiling/semantics/keyword-handlers/lookup-handler.ts index d0bc12f..6cb9366 100644 --- a/src/language/compiling/semantics/expressions/lookup-expression.ts +++ b/src/language/compiling/semantics/keyword-handlers/lookup-handler.ts @@ -3,80 +3,23 @@ import type { ElaborationError, InvalidExpressionError, } from '../../../errors.js' -import { isFunctionNode, type KeyPath } from '../../../semantics.js' import { - isExpression, + applyKeyPathToSemanticGraph, + isObjectNode, + keyPathToMolecule, + makeLookupExpression, + makeObjectNode, + prelude, + readFunctionExpression, + readLookupExpression, + stringifyKeyPathForEndUser, type Expression, type ExpressionContext, + type KeyPath, type KeywordHandler, -} from '../../../semantics/expression-elaboration.js' -import { - keyPathToMolecule, - stringifyKeyPathForEndUser, -} from '../../../semantics/key-path.js' -import { - isObjectNode, - makeObjectNode, - makeUnelaboratedObjectNode, type ObjectNode, -} from '../../../semantics/object-node.js' -import { - applyKeyPathToSemanticGraph, type SemanticGraph, - type unelaboratedKey, -} from '../../../semantics/semantic-graph.js' -import { prelude } from '../prelude.js' -import { - asSemanticGraph, - readArgumentsFromExpression, -} from './expression-utilities.js' -import { readFunctionExpression } from './function-expression.js' - -export const lookupKeyword = '@lookup' - -export type LookupExpression = Expression & { - readonly 0: '@lookup' - readonly query: ObjectNode -} - -export const readLookupExpression = ( - node: SemanticGraph, -): Either => - isExpression(node) - ? either.flatMap( - readArgumentsFromExpression(node, [['query', '1']]), - ([q]) => { - const query = asSemanticGraph(q) - if (isFunctionNode(query)) { - return either.makeLeft({ - kind: 'invalidExpression', - message: 'query cannot be a function', - }) - } else { - const canonicalizedQuery = - typeof query === 'string' - ? makeObjectNode(keyPathToMolecule(query.split('.'))) - : query - - return either.map( - keyPathFromObjectNode(canonicalizedQuery), - _keyPath => makeLookupExpression(canonicalizedQuery), - ) - } - }, - ) - : either.makeLeft({ - kind: 'invalidExpression', - message: 'not an expression', - }) - -export const makeLookupExpression = ( - query: ObjectNode, -): LookupExpression & { readonly [unelaboratedKey]: true } => - makeUnelaboratedObjectNode({ - 0: '@lookup', - query, - }) +} from '../../../semantics.js' export const lookupKeywordHandler: KeywordHandler = ( expression: Expression, @@ -230,7 +173,11 @@ const lookup = ({ // Try the parent scope. lookup({ relativePath, - context: { program: context.program, location: pathToCurrentScope }, + context: { + keywordHandlers: context.keywordHandlers, + location: pathToCurrentScope, + program: context.program, + }, }), some: lookedUpValue => either.makeRight(option.makeSome(lookedUpValue)), }), diff --git a/src/language/compiling/semantics/keyword-handlers/runtime-handler.ts b/src/language/compiling/semantics/keyword-handlers/runtime-handler.ts new file mode 100644 index 0000000..ec0ca1d --- /dev/null +++ b/src/language/compiling/semantics/keyword-handlers/runtime-handler.ts @@ -0,0 +1,40 @@ +import { either, type Either } from '../../../../adts.js' +import type { ElaborationError } from '../../../errors.js' +import { + asSemanticGraph, + isAssignable, + isFunctionNode, + makeRuntimeExpression, + readRuntimeExpression, + replaceAllTypeParametersWithTheirConstraints, + types, + type Expression, + type ExpressionContext, + type KeywordHandler, + type SemanticGraph, +} from '../../../semantics.js' + +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)) + } + }) diff --git a/src/language/compiling/semantics/expressions/todo-expression.ts b/src/language/compiling/semantics/keyword-handlers/todo-handler.ts similarity index 56% rename from src/language/compiling/semantics/expressions/todo-expression.ts rename to src/language/compiling/semantics/keyword-handlers/todo-handler.ts index a09b051..96ff8ba 100644 --- a/src/language/compiling/semantics/expressions/todo-expression.ts +++ b/src/language/compiling/semantics/keyword-handlers/todo-handler.ts @@ -1,18 +1,12 @@ import { either, type Either } from '../../../../adts.js' import type { ElaborationError } from '../../../errors.js' import { + makeObjectNode, type Expression, type ExpressionContext, type KeywordHandler, -} from '../../../semantics/expression-elaboration.js' -import { makeObjectNode } from '../../../semantics/object-node.js' -import { type SemanticGraph } from '../../../semantics/semantic-graph.js' - -export const todoKeyword = '@todo' - -export type TodoExpression = Expression & { - readonly 0: '@todo' -} + type SemanticGraph, +} from '../../../semantics.js' export const todoKeywordHandler: KeywordHandler = ( _expression: Expression, diff --git a/src/language/compiling/semantics/keywords.ts b/src/language/compiling/semantics/keywords.ts index fa891e3..4efe461 100644 --- a/src/language/compiling/semantics/keywords.ts +++ b/src/language/compiling/semantics/keywords.ts @@ -1,64 +1,39 @@ -import { type KeywordModule } from '../../semantics.js' -import { - applyKeyword, - applyKeywordHandler, -} from './expressions/apply-expression.js' -import { - checkKeyword, - checkKeywordHandler, -} from './expressions/check-expression.js' -import { - functionKeyword, - functionKeywordHandler, -} from './expressions/function-expression.js' -import { - lookupKeyword, - lookupKeywordHandler, -} from './expressions/lookup-expression.js' -import { - runtimeKeyword, - runtimeKeywordHandler, -} from './expressions/runtime-expression.js' -import { - todoKeyword, - todoKeywordHandler, -} from './expressions/todo-expression.js' - -export const handlers = { +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 { lookupKeywordHandler } from './keyword-handlers/lookup-handler.js' +import { runtimeKeywordHandler } from './keyword-handlers/runtime-handler.js' +import { todoKeywordHandler } from './keyword-handlers/todo-handler.js' + +export const keywordHandlers: KeywordHandlers = { /** * Calls the given function with a given argument. */ - [applyKeyword]: applyKeywordHandler, + '@apply': applyKeywordHandler, /** * Checks whether a given value is assignable to a given type. */ - [checkKeyword]: checkKeywordHandler, + '@check': checkKeywordHandler, /** * Creates a function. */ - [functionKeyword]: functionKeywordHandler, + '@function': functionKeywordHandler, /** * Given a query, resolves the value of a property within the program. */ - [lookupKeyword]: lookupKeywordHandler, + '@lookup': lookupKeywordHandler, /** * Defers evaluation until runtime. */ - [runtimeKeyword]: runtimeKeywordHandler, + '@runtime': runtimeKeywordHandler, /** * Ignores all properties and evaluates to an empty object. */ - [todoKeyword]: todoKeywordHandler, -} satisfies KeywordModule<`@${string}`>['handlers'] - -export type Keyword = keyof typeof handlers - -// `isKeyword` is correct as long as `handlers` does not have excess properties. -const allKeywords = new Set(Object.keys(handlers)) -export const isKeyword = (input: string): input is Keyword => - allKeywords.has(input) + '@todo': todoKeywordHandler, +} diff --git a/src/language/runtime/evaluator.ts b/src/language/runtime/evaluator.ts index 1f51382..945534e 100644 --- a/src/language/runtime/evaluator.ts +++ b/src/language/runtime/evaluator.ts @@ -3,12 +3,12 @@ import type { RuntimeError } from '../errors.js' import type { JSONValueForbiddingSymbolicKeys } from '../parsing.js' import { canonicalize } from '../parsing.js' import { elaborate, serialize, type Output } from '../semantics.js' -import * as keywordModule from './keywords.js' +import { keywordHandlers } from './keywords.js' export const evaluate = ( input: JSONValueForbiddingSymbolicKeys, ): Either => { const syntaxTree = canonicalize(input) - const semanticGraphResult = elaborate(syntaxTree, keywordModule) + const semanticGraphResult = elaborate(syntaxTree, keywordHandlers) return either.flatMap(semanticGraphResult, serialize) } diff --git a/src/language/runtime/keywords.ts b/src/language/runtime/keywords.ts index b21c72d..c56e99a 100644 --- a/src/language/runtime/keywords.ts +++ b/src/language/runtime/keywords.ts @@ -9,11 +9,10 @@ import { makeObjectNode, serialize, types, - type KeywordElaborationResult, - type KeywordModule, + type Expression, + type KeywordHandlers, type SemanticGraph, } from '../semantics.js' -import type { Expression } from '../semantics/expression-elaboration.js' import { lookupPropertyOfObjectNode } from '../semantics/object-node.js' const unserializableFunction = () => @@ -125,12 +124,12 @@ const runtimeContext = makeObjectNode({ }), }) -export const handlers = { +export const keywordHandlers: KeywordHandlers = { ...compilerKeywordHandlers, /** * Evaluates the given function, passing runtime context captured in `world`. */ - '@runtime': (expression): KeywordElaborationResult => { + '@runtime': expression => { const runtimeFunction = lookupWithinExpression( ['function', '1'], expression, @@ -158,14 +157,7 @@ export const handlers = { } } }, -} satisfies KeywordModule<`@${string}`>['handlers'] - -export type Keyword = keyof typeof handlers - -// `isKeyword` is correct as long as `handlers` does not have excess properties. -const allKeywords = new Set(Object.keys(handlers)) -export const isKeyword = (input: string): input is Keyword => - allKeywords.has(input) +} const lookupWithinExpression = ( keyAliases: [Atom, ...(readonly Atom[])], diff --git a/src/language/semantics.ts b/src/language/semantics.ts index 751ebb1..4793e91 100644 --- a/src/language/semantics.ts +++ b/src/language/semantics.ts @@ -1,23 +1,68 @@ export { elaborate, + elaborateWithContext, type ElaboratedSemanticGraph, type ExpressionContext, type KeywordElaborationResult, - type KeywordModule, + type KeywordHandler, + type KeywordHandlers, } from './semantics/expression-elaboration.js' +export { isExpression, type Expression } from './semantics/expression.js' +export { + makeApplyExpression, + readApplyExpression, + type ApplyExpression, +} from './semantics/expressions/apply-expression.js' +export { + makeCheckExpression, + readCheckExpression, + type CheckExpression, +} from './semantics/expressions/check-expression.js' +export { + asSemanticGraph, + readArgumentsFromExpression, +} from './semantics/expressions/expression-utilities.js' +export { + makeFunctionExpression, + readFunctionExpression, + type FunctionExpression, +} from './semantics/expressions/function-expression.js' +export { + makeLookupExpression, + readLookupExpression, + type LookupExpression, +} from './semantics/expressions/lookup-expression.js' +export { + makeRuntimeExpression, + readRuntimeExpression, + type RuntimeExpression, +} from './semantics/expressions/runtime-expression.js' +export { type TodoExpression } from './semantics/expressions/todo-expression.js' export { isFunctionNode, makeFunctionNode, type FunctionNode, } from './semantics/function-node.js' -export { type KeyPath } from './semantics/key-path.js' +export { + keyPathToMolecule, + stringifyKeyPathForEndUser, + type KeyPath, +} from './semantics/key-path.js' +export { isKeyword, type Keyword } from './semantics/keyword.js' export { isObjectNode, + lookupPropertyOfObjectNode, makeObjectNode, + makeUnelaboratedObjectNode, type ObjectNode, } from './semantics/object-node.js' +export { prelude } from './semantics/prelude.js' export { + applyKeyPathToSemanticGraph, + containsAnyUnelaboratedNodes, serialize, + stringifySemanticGraphForEndUser, + updateValueAtKeyPathInSemanticGraph, type Output, type SemanticGraph, } from './semantics/semantic-graph.js' diff --git a/src/language/semantics/expression-elaboration.ts b/src/language/semantics/expression-elaboration.ts index 39a93bf..dd23586 100644 --- a/src/language/semantics/expression-elaboration.ts +++ b/src/language/semantics/expression-elaboration.ts @@ -3,8 +3,14 @@ import { withPhantomData, type WithPhantomData } from '../../phantom-data.js' import type { Writable } from '../../utility-types.js' import type { ElaborationError, InvalidSyntaxTreeError } from '../errors.js' import type { Atom, Molecule, SyntaxTree } from '../parsing.js' -import { makeObjectNode, type KeyPath, type ObjectNode } from '../semantics.js' -import { makeUnelaboratedObjectNode } from './object-node.js' +import type { Expression } from './expression.js' +import type { KeyPath } from './key-path.js' +import { isKeyword, type Keyword } from './keyword.js' +import { + makeObjectNode, + makeUnelaboratedObjectNode, + type ObjectNode, +} from './object-node.js' import { extractStringValueIfPossible, updateValueAtKeyPathInSemanticGraph, @@ -15,16 +21,10 @@ declare const _elaborated: unique symbol type Elaborated = { readonly [_elaborated]: true } export type ElaboratedSemanticGraph = WithPhantomData -export type Expression = ObjectNode & { - readonly 0: `@${string}` -} - -export const isExpression = (node: SemanticGraph): node is Expression => - typeof node === 'object' && typeof node[0] === 'string' && node[0][0] === '@' - export type ExpressionContext = { - readonly program: SemanticGraph + readonly keywordHandlers: KeywordHandlers readonly location: KeyPath + readonly program: SemanticGraph } export type KeywordElaborationResult = Either @@ -34,16 +34,14 @@ export type KeywordHandler = ( context: ExpressionContext, ) => KeywordElaborationResult -export type KeywordModule = { - readonly isKeyword: (input: string) => input is Keyword - readonly handlers: Readonly> -} +export type KeywordHandlers = Readonly> export const elaborate = ( program: SyntaxTree, - keywordModule: KeywordModule<`@${string}`>, + keywordHandlers: KeywordHandlers, ): Either => - elaborateWithContext(program, keywordModule, { + elaborateWithContext(program, { + keywordHandlers, location: [], program: typeof program === 'string' @@ -53,19 +51,17 @@ export const elaborate = ( export const elaborateWithContext = ( program: SyntaxTree, - keywordModule: KeywordModule<`@${string}`>, context: ExpressionContext, ): Either => either.map( typeof program === 'string' ? handleAtomWhichMayNotBeAKeyword(program) - : elaborateWithinMolecule(program, keywordModule, context), + : elaborateWithinMolecule(program, context), withPhantomData(), ) const elaborateWithinMolecule = ( molecule: Molecule, - keywordModule: KeywordModule<`@${string}`>, context: ExpressionContext, ): Either => { const possibleExpressionAsObjectNode: Writable = makeObjectNode( @@ -83,14 +79,11 @@ const elaborateWithinMolecule = ( if (typeof value === 'string') { possibleExpressionAsObjectNode[updatedKey] = value } else { - const elaborationResult = elaborateWithinMolecule( - value, - keywordModule, - { - location: [...context.location, key], - program: updatedProgram, - }, - ) + const elaborationResult = elaborateWithinMolecule(value, { + keywordHandlers: context.keywordHandlers, + location: [...context.location, key], + program: updatedProgram, + }) if (either.isLeft(elaborationResult)) { // Immediately bail on error. return elaborationResult @@ -150,8 +143,8 @@ const elaborateWithinMolecule = ( ...possibleExpressionAsObjectNode, 0: possibleKeywordAsString, }, - keywordModule, { + keywordHandlers: context.keywordHandlers, program: updatedProgram, location: context.location, }, @@ -160,32 +153,28 @@ const elaborateWithinMolecule = ( } } -const handleObjectNodeWhichMayBeAExpression = ( +const handleObjectNodeWhichMayBeAExpression = ( node: ObjectNode & { readonly 0: Atom }, - keywordModule: KeywordModule, context: ExpressionContext, ): Either => { const { 0: possibleKeyword, ...possibleArguments } = node - return option.match( - option.fromPredicate(possibleKeyword, keywordModule.isKeyword), - { - none: () => - /^@[^@]/.test(possibleKeyword) - ? either.makeLeft({ - kind: 'unknownKeyword', - message: `unknown keyword: \`${possibleKeyword}\``, - }) - : either.makeRight({ - ...node, - 0: unescapeKeywordSigil(possibleKeyword), - }), - some: keyword => - keywordModule.handlers[keyword]( - makeObjectNode({ ...possibleArguments, 0: keyword }), - context, - ), - }, - ) + return option.match(option.fromPredicate(possibleKeyword, isKeyword), { + none: () => + /^@[^@]/.test(possibleKeyword) + ? either.makeLeft({ + kind: 'unknownKeyword', + message: `unknown keyword: \`${possibleKeyword}\``, + }) + : either.makeRight({ + ...node, + 0: unescapeKeywordSigil(possibleKeyword), + }), + some: keyword => + context.keywordHandlers[keyword]( + makeObjectNode({ ...possibleArguments, 0: keyword }), + context, + ), + }) } const handleAtomWhichMayNotBeAKeyword = ( diff --git a/src/language/semantics/expression.ts b/src/language/semantics/expression.ts new file mode 100644 index 0000000..3698123 --- /dev/null +++ b/src/language/semantics/expression.ts @@ -0,0 +1,8 @@ +import type { ObjectNode, SemanticGraph } from '../semantics.js' + +export type Expression = ObjectNode & { + readonly 0: `@${string}` +} + +export const isExpression = (node: SemanticGraph): node is Expression => + typeof node === 'object' && typeof node[0] === 'string' && node[0][0] === '@' diff --git a/src/language/semantics/expressions/apply-expression.ts b/src/language/semantics/expressions/apply-expression.ts new file mode 100644 index 0000000..371a8cc --- /dev/null +++ b/src/language/semantics/expressions/apply-expression.ts @@ -0,0 +1,42 @@ +import { either, type Either } from '../../../adts.js' +import type { ElaborationError } from '../../errors.js' +import type { Molecule } from '../../parsing.js' +import { isExpression, type Expression } from '../expression.js' +import { makeUnelaboratedObjectNode } from '../object-node.js' +import { type SemanticGraph, type unelaboratedKey } from '../semantic-graph.js' +import { readArgumentsFromExpression } from './expression-utilities.js' + +export type ApplyExpression = Expression & { + readonly 0: '@apply' + readonly function: SemanticGraph | Molecule + readonly argument: SemanticGraph | Molecule +} + +export const readApplyExpression = ( + node: SemanticGraph, +): Either => + isExpression(node) + ? either.map( + readArgumentsFromExpression(node, [ + ['function', '1'], + ['argument', '2'], + ]), + ([f, argument]) => makeApplyExpression({ function: f, argument }), + ) + : either.makeLeft({ + kind: 'invalidExpression', + message: 'not an expression', + }) + +export const makeApplyExpression = ({ + function: f, + argument, +}: { + readonly function: SemanticGraph | Molecule + readonly argument: SemanticGraph | Molecule +}): ApplyExpression & { readonly [unelaboratedKey]: true } => + makeUnelaboratedObjectNode({ + 0: '@apply', + function: f, + argument, + }) diff --git a/src/language/semantics/expressions/check-expression.ts b/src/language/semantics/expressions/check-expression.ts new file mode 100644 index 0000000..351d3d7 --- /dev/null +++ b/src/language/semantics/expressions/check-expression.ts @@ -0,0 +1,42 @@ +import { either, type Either } from '../../../adts.js' +import type { ElaborationError } from '../../errors.js' +import type { Molecule } from '../../parsing.js' +import { isExpression, type Expression } from '../expression.js' +import { makeUnelaboratedObjectNode } from '../object-node.js' +import { type SemanticGraph, type unelaboratedKey } from '../semantic-graph.js' +import { readArgumentsFromExpression } from './expression-utilities.js' + +export type CheckExpression = Expression & { + readonly 0: '@check' + readonly value: SemanticGraph | Molecule + readonly type: SemanticGraph | Molecule +} + +export const readCheckExpression = ( + node: SemanticGraph, +): Either => + isExpression(node) + ? either.map( + readArgumentsFromExpression(node, [ + ['value', '1'], + ['type', '2'], + ]), + ([value, type]) => makeCheckExpression({ value, type }), + ) + : either.makeLeft({ + kind: 'invalidExpression', + message: 'not an expression', + }) + +export const makeCheckExpression = ({ + value, + type, +}: { + value: SemanticGraph | Molecule + type: SemanticGraph | Molecule +}): CheckExpression & { readonly [unelaboratedKey]: true } => + makeUnelaboratedObjectNode({ + 0: '@check', + value, + type, + }) diff --git a/src/language/compiling/semantics/expressions/expression-utilities.ts b/src/language/semantics/expressions/expression-utilities.ts similarity index 83% rename from src/language/compiling/semantics/expressions/expression-utilities.ts rename to src/language/semantics/expressions/expression-utilities.ts index 3b86749..b3c543a 100644 --- a/src/language/compiling/semantics/expressions/expression-utilities.ts +++ b/src/language/semantics/expressions/expression-utilities.ts @@ -1,21 +1,19 @@ -import { either, option, type Either, type Option } from '../../../../adts.js' -import type { ElaborationError } from '../../../errors.js' -import type { Atom, Molecule } from '../../../parsing.js' -import { type ObjectNode } from '../../../semantics.js' -import type { - Expression, - ExpressionContext, -} from '../../../semantics/expression-elaboration.js' -import { stringifyKeyPathForEndUser } from '../../../semantics/key-path.js' +import { either, option, type Either, type Option } from '../../../adts.js' +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 { stringifyKeyPathForEndUser } from '../key-path.js' import { lookupPropertyOfObjectNode, makeUnelaboratedObjectNode, -} from '../../../semantics/object-node.js' + type ObjectNode, +} from '../object-node.js' import { applyKeyPathToSemanticGraph, isSemanticGraph, type SemanticGraph, -} from '../../../semantics/semantic-graph.js' +} from '../semantic-graph.js' export const asSemanticGraph = ( value: SemanticGraph | Molecule, diff --git a/src/language/semantics/expressions/function-expression.ts b/src/language/semantics/expressions/function-expression.ts new file mode 100644 index 0000000..e37d5d3 --- /dev/null +++ b/src/language/semantics/expressions/function-expression.ts @@ -0,0 +1,54 @@ +import { either, type Either } from '../../../adts.js' +import type { ElaborationError } from '../../errors.js' +import type { Atom, Molecule } from '../../parsing.js' +import { isExpression, type Expression } from '../expression.js' +import { makeUnelaboratedObjectNode } from '../object-node.js' +import { + serialize, + type SemanticGraph, + type unelaboratedKey, +} from '../semantic-graph.js' +import { + asSemanticGraph, + readArgumentsFromExpression, +} from './expression-utilities.js' + +export type FunctionExpression = Expression & { + readonly 0: '@function' + readonly parameter: Atom + readonly body: SemanticGraph | Molecule +} + +export const readFunctionExpression = ( + node: SemanticGraph, +): Either => + isExpression(node) + ? either.flatMap( + readArgumentsFromExpression(node, [ + ['parameter', '1'], + ['body', '2'], + ]), + ([parameter, body]): Either => + typeof parameter !== 'string' + ? either.makeLeft({ + kind: 'invalidExpression', + message: 'parameter name must be an atom', + }) + : either.map(serialize(asSemanticGraph(body)), body => + makeFunctionExpression(parameter, body), + ), + ) + : either.makeLeft({ + kind: 'invalidExpression', + message: 'not an expression', + }) + +export const makeFunctionExpression = ( + parameter: Atom, + body: SemanticGraph | Molecule, +): FunctionExpression & { readonly [unelaboratedKey]: true } => + makeUnelaboratedObjectNode({ + 0: '@function', + parameter, + body, + }) diff --git a/src/language/semantics/expressions/lookup-expression.ts b/src/language/semantics/expressions/lookup-expression.ts new file mode 100644 index 0000000..0271e26 --- /dev/null +++ b/src/language/semantics/expressions/lookup-expression.ts @@ -0,0 +1,81 @@ +import { either, type Either } from '../../../adts.js' +import type { ElaborationError, InvalidExpressionError } from '../../errors.js' +import { isExpression, type Expression } from '../expression.js' +import { isFunctionNode } from '../function-node.js' +import { keyPathToMolecule, type KeyPath } from '../key-path.js' +import { + makeObjectNode, + 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 LookupExpression = Expression & { + readonly 0: '@lookup' + readonly query: ObjectNode +} + +export const readLookupExpression = ( + node: SemanticGraph, +): Either => + isExpression(node) + ? either.flatMap( + readArgumentsFromExpression(node, [['query', '1']]), + ([q]) => { + const query = asSemanticGraph(q) + if (isFunctionNode(query)) { + return either.makeLeft({ + kind: 'invalidExpression', + message: 'query cannot be a function', + }) + } else { + const canonicalizedQuery = + typeof query === 'string' + ? makeObjectNode(keyPathToMolecule(query.split('.'))) + : query + + return either.map( + keyPathFromObjectNode(canonicalizedQuery), + _keyPath => makeLookupExpression(canonicalizedQuery), + ) + } + }, + ) + : either.makeLeft({ + kind: 'invalidExpression', + message: 'not an expression', + }) + +export const makeLookupExpression = ( + query: ObjectNode, +): LookupExpression & { readonly [unelaboratedKey]: true } => + makeUnelaboratedObjectNode({ + 0: '@lookup', + query, + }) + +const keyPathFromObjectNode = ( + node: ObjectNode, +): Either => { + 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) +} diff --git a/src/language/semantics/expressions/runtime-expression.ts b/src/language/semantics/expressions/runtime-expression.ts new file mode 100644 index 0000000..1ccaa60 --- /dev/null +++ b/src/language/semantics/expressions/runtime-expression.ts @@ -0,0 +1,54 @@ +import { either, type Either } from '../../../adts.js' +import type { ElaborationError } from '../../errors.js' +import { isExpression, type Expression } from '../expression.js' +import { isFunctionNode } from '../function-node.js' +import { makeUnelaboratedObjectNode } from '../object-node.js' +import { + containsAnyUnelaboratedNodes, + type SemanticGraph, + type unelaboratedKey, +} from '../semantic-graph.js' +import { + asSemanticGraph, + readArgumentsFromExpression, +} from './expression-utilities.js' + +export type RuntimeExpression = Expression & { + readonly 0: '@runtime' + readonly function: SemanticGraph +} + +export const readRuntimeExpression = ( + node: SemanticGraph, +): Either => + isExpression(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)) + } + }, + ) + : either.makeLeft({ + kind: 'invalidExpression', + message: 'not an expression', + }) + +export const makeRuntimeExpression = ( + f: SemanticGraph, +): RuntimeExpression & { readonly [unelaboratedKey]: true } => + makeUnelaboratedObjectNode({ + 0: '@runtime', + function: f, + }) diff --git a/src/language/semantics/expressions/todo-expression.ts b/src/language/semantics/expressions/todo-expression.ts new file mode 100644 index 0000000..111cb3c --- /dev/null +++ b/src/language/semantics/expressions/todo-expression.ts @@ -0,0 +1,5 @@ +import { type Expression } from '../expression.js' + +export type TodoExpression = Expression & { + readonly 0: '@todo' +} diff --git a/src/language/semantics/keyword.ts b/src/language/semantics/keyword.ts new file mode 100644 index 0000000..f92ef17 --- /dev/null +++ b/src/language/semantics/keyword.ts @@ -0,0 +1,13 @@ +export const isKeyword = (input: string) => + input === '@apply' || + input === '@check' || + input === '@function' || + input === '@lookup' || + input === '@runtime' || + input === '@todo' + +export type Keyword = typeof isKeyword extends ( + input: string, +) => input is string & infer Keyword + ? Keyword + : never diff --git a/src/language/compiling/semantics/prelude.ts b/src/language/semantics/prelude.ts similarity index 97% rename from src/language/compiling/semantics/prelude.ts rename to src/language/semantics/prelude.ts index bf954d4..9340343 100644 --- a/src/language/compiling/semantics/prelude.ts +++ b/src/language/semantics/prelude.ts @@ -1,31 +1,28 @@ -import { either, option, type Either } from '../../../adts.js' -import type { DependencyUnavailable, Panic } from '../../errors.js' -import type { Atom } from '../../parsing.js' +import { either, option, type Either } from '../../adts.js' +import type { DependencyUnavailable, Panic } from '../errors.js' +import type { Atom } from '../parsing.js' +import { isFunctionNode, makeFunctionNode } from './function-node.js' +import { keyPathToMolecule } from './key-path.js' import { - isFunctionNode, isObjectNode, - makeFunctionNode, - makeObjectNode, - types, - type ObjectNode, -} from '../../semantics.js' -import { keyPathToMolecule } from '../../semantics/key-path.js' -import { lookupPropertyOfObjectNode, + makeObjectNode, makeUnelaboratedObjectNode, -} from '../../semantics/object-node.js' + type ObjectNode, +} from './object-node.js' import { containsAnyUnelaboratedNodes, isSemanticGraph, type SemanticGraph, -} from '../../semantics/semantic-graph.js' +} from './semantic-graph.js' +import { types } from './type-system.js' import { makeFunctionType, makeObjectType, makeTypeParameter, makeUnionType, type FunctionType, -} from '../../semantics/type-system/type-formats.js' +} from './type-system/type-formats.js' const handleUnavailableDependencies = (