Skip to content

Commit ac2e2f8

Browse files
committed
Refactor keyword/expression handling
1 parent 239bab8 commit ac2e2f8

16 files changed

+953
-662
lines changed

src/end-to-end.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ testCases(endToEnd, code => code)('end-to-end tests', [
4949
either.makeRight({
5050
0: '@function',
5151
parameter: 'a',
52-
body: { 0: '@lookup', 1: { 0: 'a' } },
52+
body: { 0: '@lookup', query: { 0: 'a' } },
5353
}),
5454
],
5555
['{ a: ({ A }) }', either.makeRight({ a: { 0: 'A' } })],

src/language/compiling/semantics.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -574,7 +574,7 @@ elaborationSuite('@function', [
574574
either.makeRight({
575575
0: '@function',
576576
parameter: 'x',
577-
body: { 0: '@lookup', 1: { 0: 'x' } },
577+
body: { 0: '@lookup', query: { 0: 'x' } },
578578
}),
579579
)
580580
},
Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
import { either, type Either } from '../../../../adts.js'
2+
import type { ElaborationError } from '../../../errors.js'
3+
import type { Molecule } from '../../../parsing.js'
4+
import { isFunctionNode } from '../../../semantics.js'
5+
import {
6+
isExpression,
7+
type Expression,
8+
type ExpressionContext,
9+
type KeywordHandler,
10+
} from '../../../semantics/expression-elaboration.js'
11+
import { makeUnelaboratedObjectNode } from '../../../semantics/object-node.js'
12+
import {
13+
containsAnyUnelaboratedNodes,
14+
type SemanticGraph,
15+
type unelaboratedKey,
16+
} from '../../../semantics/semantic-graph.js'
17+
import {
18+
asSemanticGraph,
19+
readArgumentsFromExpression,
20+
} from './expression-utilities.js'
21+
22+
export const applyKeyword = '@apply'
23+
24+
export type ApplyExpression = Expression & {
25+
readonly 0: '@apply'
26+
readonly function: SemanticGraph | Molecule
27+
readonly argument: SemanticGraph | Molecule
28+
}
29+
30+
export const readApplyExpression = (
31+
node: SemanticGraph,
32+
): Either<ElaborationError, ApplyExpression> =>
33+
isExpression(node)
34+
? either.map(
35+
readArgumentsFromExpression(node, [
36+
['function', '1'],
37+
['argument', '2'],
38+
]),
39+
([f, argument]) => makeApplyExpression({ function: f, argument }),
40+
)
41+
: either.makeLeft({
42+
kind: 'invalidExpression',
43+
message: 'not an expression',
44+
})
45+
46+
export const makeApplyExpression = ({
47+
function: f,
48+
argument,
49+
}: {
50+
readonly function: SemanticGraph | Molecule
51+
readonly argument: SemanticGraph | Molecule
52+
}): ApplyExpression & { readonly [unelaboratedKey]: true } =>
53+
makeUnelaboratedObjectNode({
54+
0: '@apply',
55+
function: f,
56+
argument,
57+
})
58+
59+
export const applyKeywordHandler: KeywordHandler = (
60+
expression: Expression,
61+
_context: ExpressionContext,
62+
): Either<ElaborationError, SemanticGraph> =>
63+
either.flatMap(
64+
readApplyExpression(expression),
65+
(applyExpression): Either<ElaborationError, SemanticGraph> => {
66+
if (containsAnyUnelaboratedNodes(applyExpression.argument)) {
67+
// The argument isn't ready, so keep the @apply unelaborated.
68+
return either.makeRight(applyExpression)
69+
} else {
70+
const functionToApply = asSemanticGraph(applyExpression.function)
71+
if (isFunctionNode(functionToApply)) {
72+
const result = functionToApply(
73+
asSemanticGraph(applyExpression.argument),
74+
)
75+
if (either.isLeft(result)) {
76+
if (result.value.kind === 'dependencyUnavailable') {
77+
// Keep the @apply unelaborated.
78+
return either.makeRight(applyExpression)
79+
} else {
80+
return either.makeLeft(result.value)
81+
}
82+
} else {
83+
return result
84+
}
85+
} else if (containsAnyUnelaboratedNodes(functionToApply)) {
86+
// The function isn't ready, so keep the @apply unelaborated.
87+
return either.makeRight(applyExpression)
88+
} else {
89+
return either.makeLeft({
90+
kind: 'invalidExpression',
91+
message: 'only functions can be applied',
92+
})
93+
}
94+
}
95+
},
96+
)
Lines changed: 161 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,161 @@
1+
import { either, option, type Either } from '../../../../adts.js'
2+
import type { ElaborationError } from '../../../errors.js'
3+
import type { Molecule } from '../../../parsing.js'
4+
import { isFunctionNode } from '../../../semantics.js'
5+
import {
6+
isExpression,
7+
type Expression,
8+
type ExpressionContext,
9+
type KeywordHandler,
10+
} from '../../../semantics/expression-elaboration.js'
11+
import {
12+
lookupPropertyOfObjectNode,
13+
makeUnelaboratedObjectNode,
14+
} from '../../../semantics/object-node.js'
15+
import {
16+
stringifySemanticGraphForEndUser,
17+
type SemanticGraph,
18+
type unelaboratedKey,
19+
} from '../../../semantics/semantic-graph.js'
20+
import {
21+
asSemanticGraph,
22+
readArgumentsFromExpression,
23+
} from './expression-utilities.js'
24+
25+
export const checkKeyword = '@check'
26+
27+
export type CheckExpression = Expression & {
28+
readonly 0: '@check'
29+
readonly value: SemanticGraph | Molecule
30+
readonly type: SemanticGraph | Molecule
31+
}
32+
33+
export const readCheckExpression = (
34+
node: SemanticGraph,
35+
): Either<ElaborationError, CheckExpression> =>
36+
isExpression(node)
37+
? either.map(
38+
readArgumentsFromExpression(node, [
39+
['value', '1'],
40+
['type', '2'],
41+
]),
42+
([value, type]) => makeCheckExpression({ value, type }),
43+
)
44+
: either.makeLeft({
45+
kind: 'invalidExpression',
46+
message: 'not an expression',
47+
})
48+
49+
export const makeCheckExpression = ({
50+
value,
51+
type,
52+
}: {
53+
value: SemanticGraph | Molecule
54+
type: SemanticGraph | Molecule
55+
}): CheckExpression & { readonly [unelaboratedKey]: true } =>
56+
makeUnelaboratedObjectNode({
57+
0: '@check',
58+
value,
59+
type,
60+
})
61+
62+
export const checkKeywordHandler: KeywordHandler = (
63+
expression: Expression,
64+
context: ExpressionContext,
65+
): Either<ElaborationError, SemanticGraph> =>
66+
either.flatMap(readCheckExpression(expression), ({ value, type }) =>
67+
check({
68+
value: asSemanticGraph(value),
69+
type: asSemanticGraph(type),
70+
context,
71+
}),
72+
)
73+
74+
const check = ({
75+
context,
76+
value,
77+
type,
78+
}: {
79+
readonly context: ExpressionContext
80+
readonly value: SemanticGraph
81+
readonly type: SemanticGraph
82+
}): Either<ElaborationError, SemanticGraph> => {
83+
if (typeof type === 'string') {
84+
return typeof value === 'string' &&
85+
typeof type === 'string' &&
86+
value === type
87+
? either.makeRight(value)
88+
: either.makeLeft({
89+
kind: 'typeMismatch',
90+
message: `the value \`${stringifySemanticGraphForEndUser(
91+
value,
92+
)}\` is not assignable to the type \`${stringifySemanticGraphForEndUser(
93+
type,
94+
)}\``,
95+
})
96+
} else if (isFunctionNode(value)) {
97+
// TODO: Model function signatures as data and allow checking them.
98+
return either.makeLeft({
99+
kind: 'invalidSyntaxTree',
100+
message: 'functions cannot be type checked',
101+
})
102+
} else if (isFunctionNode(type)) {
103+
const result = type(value)
104+
if (either.isLeft(result)) {
105+
// The compile-time-evaluated function panicked or had an unavailable dependency (which
106+
// results in a panic anyway in this context).
107+
return either.makeLeft({
108+
kind: 'panic',
109+
message: result.value.message,
110+
})
111+
} else if (typeof result.value !== 'string' || result.value !== 'true') {
112+
return either.makeLeft({
113+
kind: 'typeMismatch',
114+
message: `the value \`${stringifySemanticGraphForEndUser(
115+
value,
116+
)}\` did not pass the given type guard`,
117+
})
118+
} else {
119+
// The value was valid according to the type guard.
120+
return either.makeRight(value)
121+
}
122+
} else if (typeof value === 'string') {
123+
// The only remaining case is when the type is an object, so we must have a type error.
124+
return either.makeLeft({
125+
kind: 'typeMismatch',
126+
message: `the value \`${stringifySemanticGraphForEndUser(
127+
value,
128+
)}\` is not assignable to the type \`${stringifySemanticGraphForEndUser(
129+
type,
130+
)}\``,
131+
})
132+
} else {
133+
// Make sure all properties in the type are present and valid in the value (recursively).
134+
// Values may legally have additional properties beyond what is required by the type.
135+
for (const [key, typePropertyValue] of Object.entries(type)) {
136+
const valuePropertyValue = lookupPropertyOfObjectNode(key, value)
137+
if (option.isNone(valuePropertyValue)) {
138+
return either.makeLeft({
139+
kind: 'typeMismatch',
140+
message: `the value \`${stringifySemanticGraphForEndUser(
141+
value,
142+
)}\` is not assignable to the type \`${stringifySemanticGraphForEndUser(
143+
type,
144+
)}\` because it is missing the property \`${key}\``,
145+
})
146+
} else {
147+
// Recursively check the property:
148+
const resultOfCheckingProperty = check({
149+
context,
150+
value: valuePropertyValue.value,
151+
type: asSemanticGraph(typePropertyValue),
152+
})
153+
if (either.isLeft(resultOfCheckingProperty)) {
154+
return resultOfCheckingProperty
155+
}
156+
}
157+
}
158+
// If this function has not yet returned then the value is assignable to the type.
159+
return either.makeRight(value)
160+
}
161+
}
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
import { either, option, type Either, type Option } from '../../../../adts.js'
2+
import type { ElaborationError } from '../../../errors.js'
3+
import type { Atom, Molecule } from '../../../parsing.js'
4+
import { type ObjectNode } from '../../../semantics.js'
5+
import type {
6+
Expression,
7+
ExpressionContext,
8+
} from '../../../semantics/expression-elaboration.js'
9+
import { stringifyKeyPathForEndUser } from '../../../semantics/key-path.js'
10+
import {
11+
lookupPropertyOfObjectNode,
12+
makeUnelaboratedObjectNode,
13+
} from '../../../semantics/object-node.js'
14+
import {
15+
applyKeyPathToSemanticGraph,
16+
isSemanticGraph,
17+
type SemanticGraph,
18+
} from '../../../semantics/semantic-graph.js'
19+
20+
export const asSemanticGraph = (
21+
value: SemanticGraph | Molecule,
22+
): SemanticGraph =>
23+
isSemanticGraph(value) ? value : makeUnelaboratedObjectNode(value)
24+
25+
export const locateSelf = (context: ExpressionContext) =>
26+
option.match(applyKeyPathToSemanticGraph(context.program, context.location), {
27+
none: () =>
28+
either.makeLeft({
29+
kind: 'bug',
30+
message: `failed to locate self at \`${stringifyKeyPathForEndUser(
31+
context.location,
32+
)}\` in program`,
33+
}),
34+
some: either.makeRight,
35+
})
36+
37+
export const readArgumentsFromExpression = <
38+
const Specification extends readonly (readonly [
39+
string,
40+
...(readonly string[]),
41+
])[],
42+
>(
43+
expression: Expression,
44+
specification: Specification,
45+
): Either<ElaborationError, ParsedExpressionArguments<Specification>> => {
46+
const expressionArguments: ObjectNode[string][] = []
47+
for (const aliases of specification) {
48+
const argument = lookupWithinExpression(aliases, expression)
49+
if (option.isNone(argument)) {
50+
const requiredKeySummary = aliases
51+
.map(alias => `\`${alias}\``)
52+
.join(' or ')
53+
return either.makeLeft({
54+
kind: 'invalidExpression',
55+
message: `missing required property ${requiredKeySummary}`,
56+
})
57+
} else {
58+
expressionArguments.push(argument.value)
59+
}
60+
}
61+
return either.makeRight(
62+
// This is correct since the above loop pushes one argument for each `specification` element.
63+
expressionArguments as ParsedExpressionArguments<Specification>,
64+
)
65+
}
66+
type ParsedExpressionArguments<
67+
Specification extends readonly (readonly [string, ...(readonly string[])])[],
68+
> = {
69+
[Index in keyof Specification]: ObjectNode[string]
70+
}
71+
72+
const lookupWithinExpression = (
73+
keyAliases: readonly [Atom, ...(readonly Atom[])],
74+
expression: Expression,
75+
): Option<SemanticGraph> => {
76+
for (const key of keyAliases) {
77+
const result = lookupPropertyOfObjectNode(key, expression)
78+
if (!option.isNone(result)) {
79+
return result
80+
}
81+
}
82+
return option.none
83+
}

0 commit comments

Comments
 (0)