@@ -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'
1318import { optionallySurroundedByParentheses } from './parentheses.js'
1419import { trivia } from './trivia.js'
1520
1621export type Molecule = { readonly [ key : Atom ] : Molecule | Atom }
1722
1823export 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.
3630type 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
10045const propertyKey = atomParser
10146const 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
10851const 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+
12191const 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