Skip to content

Commit ea11eca

Browse files
committed
Add syntax sugar for @index expressions
1 parent ab91a8b commit ea11eca

File tree

2 files changed

+173
-70
lines changed

2 files changed

+173
-70
lines changed

src/end-to-end.test.ts

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,47 @@ testCases(endToEnd, code => code)('end-to-end tests', [
5353
body: { 0: '@lookup', query: { 0: 'a' } },
5454
}),
5555
],
56+
['{ success }.0', either.makeRight('success')],
57+
['{ f: :identity }.f(success)', either.makeRight('success')],
58+
['{ f: :identity }.f({ a: success }).a', either.makeRight('success')],
59+
[
60+
'{ f: :identity }.f({ g: :identity }).g({ a: success }).a',
61+
either.makeRight('success'),
62+
],
63+
['{ a: { b: success } }.a.b', either.makeRight('success')],
64+
[
65+
'{ a: { "b.c(d) e \\" {}": success } }.a."b.c(d) e \\" {}"',
66+
either.makeRight('success'),
67+
],
68+
['(a => { b: :a }.b)(success)', either.makeRight('success')],
69+
['(a => { b: :a })(success).b', either.makeRight('success')],
70+
['{ success }/**/./**/0', either.makeRight('success')],
71+
[
72+
`
73+
{ a: { b: success } } // blah
74+
// blah
75+
.a // blah
76+
// blah
77+
.b // blah
78+
`,
79+
either.makeRight('success'),
80+
],
81+
[
82+
`{
83+
a: {
84+
b: {
85+
c: z => {
86+
d: y => x => {
87+
e: {
88+
f: w => { g: { :z :y :x :w } }
89+
}
90+
}
91+
}
92+
}
93+
}
94+
}.a.b.c(a).d(b)(c).e.f(d).g`,
95+
either.makeRight({ 0: 'a', 1: 'b', 2: 'c', 3: 'd' }),
96+
],
5697
['{ a: ({ A }) }', either.makeRight({ a: { 0: 'A' } })],
5798
['{ a: ( A ) }', either.makeRight({ a: 'A' })],
5899
['{ a: ("A A A") }', either.makeRight({ a: 'A A A' })],

src/language/parsing/molecule.ts

Lines changed: 132 additions & 70 deletions
Original file line numberDiff line numberDiff line change
@@ -9,28 +9,22 @@ import {
99
zeroOrMore,
1010
type Parser,
1111
} from '@matt.kantor/parsing'
12-
import { atomParser, type Atom } from './atom.js'
12+
import { keyPathToMolecule } from '../semantics.js'
13+
import {
14+
atomParser,
15+
atomWithAdditionalQuotationRequirements,
16+
type Atom,
17+
} from './atom.js'
1318
import { optionallySurroundedByParentheses } from './parentheses.js'
1419
import { trivia } from './trivia.js'
1520

1621
export type Molecule = { readonly [key: Atom]: Molecule | Atom }
1722

1823
export const unit: Molecule = {}
1924

20-
export const moleculeParser: Parser<Molecule> = oneOf([
21-
optionallySurroundedByParentheses(
22-
map(
23-
lazy(() => moleculeAsEntries(makeIncrementingIndexer())),
24-
Object.fromEntries,
25-
),
26-
),
27-
lazy(() => sugaredApply),
28-
lazy(() => sugaredFunction),
29-
])
30-
31-
const optional = <Output>(
32-
parser: Parser<NonNullable<Output>>,
33-
): Parser<Output | undefined> => oneOf([parser, nothing])
25+
export const moleculeParser: Parser<Molecule> = lazy(
26+
() => potentiallySugaredMolecule,
27+
)
3428

3529
// Keyless properties are automatically assigned numeric indexes, which uses some mutable state.
3630
type Indexer = () => string
@@ -44,65 +38,14 @@ const makeIncrementingIndexer = (): Indexer => {
4438
}
4539
}
4640

47-
const propertyDelimiter = oneOf([
48-
sequence([optional(trivia), literal(','), optional(trivia)]),
49-
trivia,
50-
])
51-
52-
const sugaredLookup: Parser<Molecule> = optionallySurroundedByParentheses(
53-
map(
54-
sequence([literal(':'), oneOf([atomParser, moleculeParser])]),
55-
([_colon, query]) => ({ 0: '@lookup', query }),
56-
),
57-
)
58-
59-
const sugaredFunction: Parser<Molecule> = optionallySurroundedByParentheses(
60-
map(
61-
sequence([
62-
atomParser,
63-
trivia,
64-
literal('=>'),
65-
trivia,
66-
lazy(() => propertyValue),
67-
]),
68-
([parameter, _trivia1, _arrow, _trivia2, body]) => ({
69-
0: '@function',
70-
parameter,
71-
body,
72-
}),
73-
),
74-
)
75-
76-
const sugaredApply: Parser<Molecule> = map(
77-
sequence([
78-
oneOf([sugaredLookup, lazy(() => sugaredFunction)]),
79-
oneOrMore(
80-
sequence([
81-
literal('('),
82-
optional(trivia),
83-
lazy(() => propertyValue),
84-
optional(trivia),
85-
literal(')'),
86-
]),
87-
),
88-
]),
89-
([f, multipleArguments]) =>
90-
multipleArguments.reduce<Molecule>(
91-
(expression, [_1, _2, argument, _3, _4]) => ({
92-
0: '@apply',
93-
function: expression,
94-
argument,
95-
}),
96-
f,
97-
),
98-
)
41+
const optional = <Output>(
42+
parser: Parser<NonNullable<Output>>,
43+
): Parser<Output | undefined> => oneOf([parser, nothing])
9944

10045
const propertyKey = atomParser
10146
const propertyValue = oneOf([
102-
sugaredApply, // must come first to avoid ambiguity
103-
lazy(() => moleculeParser), // must come second to avoid ambiguity
47+
lazy(() => potentiallySugaredMolecule),
10448
atomParser,
105-
sugaredLookup,
10649
])
10750

10851
const namedProperty = map(
@@ -118,6 +61,33 @@ const property = (index: Indexer) =>
11861
oneOf([namedProperty, numberedProperty(index)]),
11962
)
12063

64+
const propertyDelimiter = oneOf([
65+
sequence([optional(trivia), literal(','), optional(trivia)]),
66+
trivia,
67+
])
68+
69+
const argument = map(
70+
sequence([
71+
literal('('),
72+
optional(trivia),
73+
propertyValue,
74+
optional(trivia),
75+
literal(')'),
76+
]),
77+
([_openingParenthesis, _trivia1, argument, _trivia2, _closingParenthesis]) =>
78+
argument,
79+
)
80+
81+
const dottedKeyPathComponent = map(
82+
sequence([
83+
optional(trivia),
84+
literal('.'),
85+
optional(trivia),
86+
atomWithAdditionalQuotationRequirements(literal('.')),
87+
]),
88+
([_trivia1, _dot, _trivia2, key]) => key,
89+
)
90+
12191
const moleculeAsEntries = (
12292
index: Indexer,
12393
): Parser<readonly (readonly [string, string | Molecule])[]> =>
@@ -146,3 +116,95 @@ const moleculeAsEntries = (
146116
? remainingProperties
147117
: [optionalInitialProperty, ...remainingProperties],
148118
)
119+
120+
const sugarFreeMolecule: Parser<Molecule> = optionallySurroundedByParentheses(
121+
map(
122+
lazy(() => moleculeAsEntries(makeIncrementingIndexer())),
123+
Object.fromEntries,
124+
),
125+
)
126+
127+
const sugaredLookup: Parser<Molecule> = optionallySurroundedByParentheses(
128+
map(
129+
sequence([literal(':'), oneOf([atomParser, sugarFreeMolecule])]),
130+
([_colon, query]) => ({ 0: '@lookup', query }),
131+
),
132+
)
133+
134+
const sugaredFunction: Parser<Molecule> = optionallySurroundedByParentheses(
135+
map(
136+
sequence([atomParser, trivia, literal('=>'), trivia, propertyValue]),
137+
([parameter, _trivia1, _arrow, _trivia2, body]) => ({
138+
0: '@function',
139+
parameter,
140+
body,
141+
}),
142+
),
143+
)
144+
145+
const potentiallySugaredMolecule: Parser<Molecule> = (() => {
146+
// The awkward setup in here avoids infinite recursion when applying the mutually-dependent
147+
// parsers for index and apply sugars. Indexes/applications can be chained to form
148+
// arbitrarily-long expressions (e.g. `:a.b.c(d).e(f)(g).h.i(j).k`).
149+
150+
const potentiallySugaredNonApply = map(
151+
sequence([
152+
oneOf([sugaredLookup, sugaredFunction, sugarFreeMolecule]),
153+
zeroOrMore(dottedKeyPathComponent),
154+
]),
155+
([object, keyPath]) =>
156+
keyPath.length === 0
157+
? object
158+
: {
159+
0: '@index',
160+
object,
161+
query: keyPathToMolecule(keyPath),
162+
},
163+
)
164+
165+
const sugaredApplyWithOptionalTrailingIndexesAndApplies = map(
166+
sequence([
167+
potentiallySugaredNonApply,
168+
oneOrMore(argument),
169+
zeroOrMore(
170+
sequence([oneOrMore(dottedKeyPathComponent), zeroOrMore(argument)]),
171+
),
172+
]),
173+
([
174+
functionToApply,
175+
multipleArguments,
176+
trailingIndexQueriesAndApplyArguments,
177+
]) => {
178+
const initialApply = multipleArguments.reduce<Molecule>(
179+
(expression, argument) => ({
180+
0: '@apply',
181+
function: expression,
182+
argument,
183+
}),
184+
functionToApply,
185+
)
186+
187+
return trailingIndexQueriesAndApplyArguments.reduce(
188+
(expression, [keyPath, possibleArguments]) =>
189+
possibleArguments.reduce<Molecule>(
190+
(functionToApply, argument) => ({
191+
0: '@apply',
192+
function: functionToApply,
193+
argument,
194+
}),
195+
{
196+
0: '@index',
197+
object: expression,
198+
query: keyPathToMolecule(keyPath),
199+
},
200+
),
201+
initialApply,
202+
)
203+
},
204+
)
205+
206+
return oneOf([
207+
sugaredApplyWithOptionalTrailingIndexesAndApplies,
208+
potentiallySugaredNonApply,
209+
])
210+
})()

0 commit comments

Comments
 (0)