Skip to content

Commit e3212cd

Browse files
authored
Merge pull request #8 from mkantor/comments
Add syntax for comments
2 parents c9afa3e + e445148 commit e3212cd

File tree

8 files changed

+135
-38
lines changed

8 files changed

+135
-38
lines changed

src/end-to-end.test.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,18 @@ testCases(endToEnd, code => code)('end-to-end tests', [
6262
['{ (a: A) (b: B) }', either.makeRight({ a: 'A', b: 'B' })],
6363
['( { ((a): :(b)) ( ( b ): B ) } )', either.makeRight({ a: 'B', b: 'B' })],
6464
['{ (a: :(")")), (")": (B)) }', either.makeRight({ a: 'B', ')': 'B' })],
65+
[`/**/a/**/`, either.makeRight('a')],
66+
['hello//world', either.makeRight('hello')],
67+
[`"hello//world"`, either.makeRight('hello//world')],
68+
[`{a/* this works as a delimiter */b}`, either.makeRight({ 0: 'a', 1: 'b' })],
69+
[
70+
`/**/{/**/a:/**/b/**/,/**/c:/**/d/**/}/**/`,
71+
either.makeRight({ a: 'b', c: 'd' }),
72+
],
73+
[
74+
`/**/(/**/a/**/=>/**/:a/**/)(/**/output/**/)/**/`,
75+
either.makeRight('output'),
76+
],
6577
[':match({ a: A })({ tag: a, value: {} })', either.makeRight('A')],
6678
[':{string concatenate}(a)(b)', either.makeRight('ba')],
6779
[
@@ -70,6 +82,7 @@ testCases(endToEnd, code => code)('end-to-end tests', [
7082
],
7183
[
7284
`{
85+
// foo: bar
7386
"static data":"blah blah blah"
7487
"evaluated data": {
7588
0:@runtime

src/language/parsing/atom.ts

Lines changed: 53 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { parser, type Parser } from '../../parsing.js'
22
import { optionallySurroundedByParentheses } from './parentheses.js'
3+
import { whitespace } from './trivia.js'
34

45
export type Atom = string
56

@@ -9,31 +10,62 @@ export const isAtom = (value: unknown): value is Atom =>
910
export const unit = '' as const
1011

1112
export const atomParser: Parser<Atom> = optionallySurroundedByParentheses(
12-
parser.map(
13-
parser.lazy(() => parser.oneOf([quotedAtom, unquotedAtom])),
14-
output => output.join(''),
15-
),
13+
parser.lazy(() => parser.oneOf([quotedAtom, unquotedAtom])),
14+
)
15+
16+
const quotedAtom = parser.map(
17+
parser.sequence([
18+
parser.as(parser.literal('"'), ''),
19+
parser.map(
20+
parser.zeroOrMore(
21+
parser.oneOf([
22+
parser.butNot(
23+
parser.anySingleCharacter,
24+
parser.oneOf([parser.literal('"'), parser.literal('\\')]),
25+
'`"` or `\\`',
26+
),
27+
parser.as(parser.literal('\\"'), '"'),
28+
parser.as(parser.literal('\\\\'), '\\'),
29+
]),
30+
),
31+
output => output.join(''),
32+
),
33+
parser.as(parser.literal('"'), ''),
34+
]),
35+
([_1, contents, _2]) => contents,
1636
)
1737

18-
const quotedAtom = parser.sequence([
19-
parser.as(parser.literal('"'), ''),
20-
parser.map(
21-
parser.zeroOrMore(
38+
const unquotedAtom = parser.map(
39+
parser.oneOrMore(
40+
parser.butNot(
41+
parser.anySingleCharacter,
2242
parser.oneOf([
23-
parser.butNot(
24-
parser.anySingleCharacter,
25-
parser.oneOf([parser.literal('"'), parser.literal('\\')]),
26-
'`"` or `\\`',
27-
),
28-
parser.as(parser.literal('\\"'), '"'),
29-
parser.as(parser.literal('\\\\'), '\\'),
43+
whitespace,
44+
parser.literal('"'),
45+
parser.literal('{'),
46+
parser.literal('}'),
47+
parser.literal('['),
48+
parser.literal(']'),
49+
parser.literal('('),
50+
parser.literal(')'),
51+
parser.literal('<'),
52+
parser.literal('>'),
53+
parser.literal('#'),
54+
parser.literal('&'),
55+
parser.literal('|'),
56+
parser.literal('\\'),
57+
parser.literal('='),
58+
parser.literal(':'),
59+
parser.literal(';'),
60+
parser.literal(','),
61+
parser.literal('//'),
62+
parser.literal('/*'),
63+
parser.literal('*/'),
3064
]),
65+
'a forbidden character sequence',
3166
),
32-
output => output.join(''),
3367
),
34-
parser.as(parser.literal('"'), ''),
35-
])
36-
37-
const unquotedAtom = parser.oneOrMore(
38-
parser.regularExpression(/[^\s{}[\]()<>#&\|\\=:;,]+/),
68+
characters => characters.join(''),
3969
)
70+
71+
export { unquotedAtom as unquotedAtomParser }

src/language/parsing/molecule.ts

Lines changed: 14 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { parser, type Parser } from '../../parsing.js'
22
import { atomParser, type Atom } from './atom.js'
33
import { optionallySurroundedByParentheses } from './parentheses.js'
4-
import { whitespace } from './whitespace.js'
4+
import { trivia } from './trivia.js'
55

66
export type Molecule = { readonly [key: Atom]: Molecule | Atom }
77

@@ -48,7 +48,14 @@ const makeIncrementingIndexer = (): Indexer => {
4848

4949
// Language-specific parsers follow.
5050

51-
const propertyDelimiter = parser.regularExpression(/[\s,]+/)
51+
const propertyDelimiter = parser.oneOf([
52+
parser.sequence([
53+
optional(omit(trivia)),
54+
parser.literal(','),
55+
optional(omit(trivia)),
56+
]),
57+
trivia,
58+
])
5259

5360
const sugaredLookup: Parser<PartialMolecule> =
5461
optionallySurroundedByParentheses(
@@ -67,9 +74,9 @@ const sugaredFunction: Parser<PartialMolecule> =
6774
flat(
6875
parser.sequence([
6976
parser.map(atomParser, output => [output]),
70-
omit(whitespace),
77+
omit(trivia),
7178
omit(parser.literal('=>')),
72-
omit(whitespace),
79+
omit(trivia),
7380
parser.map(
7481
parser.lazy(() => propertyValue),
7582
output => [output],
@@ -90,9 +97,9 @@ const sugaredApply: Parser<PartialMolecule> = parser.map(
9097
parser.oneOrMore(
9198
parser.sequence([
9299
parser.literal('('),
93-
optional(omit(whitespace)),
100+
optional(omit(trivia)),
94101
parser.lazy(() => propertyValue),
95-
optional(omit(whitespace)),
102+
optional(omit(trivia)),
96103
parser.literal(')'),
97104
]),
98105
),
@@ -120,7 +127,7 @@ const namedProperty = flat(
120127
parser.sequence([
121128
propertyKey,
122129
omit(parser.literal(':')),
123-
optional(omit(whitespace)),
130+
optional(omit(trivia)),
124131
propertyValue,
125132
]),
126133
)

src/language/parsing/parentheses.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { parser, type Parser } from '../../parsing.js'
2-
import { whitespace } from './whitespace.js'
2+
import { trivia } from './trivia.js'
33

44
const optionallySurroundedBy = <Output>(
55
parser1: Parser<unknown>,
@@ -18,15 +18,15 @@ export const optionallySurroundedByParentheses = <Output>(
1818
theParser: Parser<Output>,
1919
): Parser<Output> =>
2020
parser.oneOf([
21-
// This allows `theParser` to greedily consume whitespace.
21+
// This allows `theParser` to greedily consume trivia.
2222
optionallySurroundedBy(
2323
parser.literal('('),
2424
theParser,
25-
parser.sequence([parser.zeroOrMore(whitespace), parser.literal(')')]),
25+
parser.sequence([parser.zeroOrMore(trivia), parser.literal(')')]),
2626
),
2727
optionallySurroundedBy(
28-
parser.sequence([parser.literal('('), parser.zeroOrMore(whitespace)]),
28+
parser.sequence([parser.literal('('), parser.zeroOrMore(trivia)]),
2929
theParser,
30-
parser.sequence([parser.zeroOrMore(whitespace), parser.literal(')')]),
30+
parser.sequence([parser.zeroOrMore(trivia), parser.literal(')')]),
3131
),
3232
])

src/language/parsing/syntax-tree.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import type {
1010
import type { KeyPath } from '../semantics.js'
1111
import { atomParser, type Atom } from './atom.js'
1212
import { moleculeParser, type Molecule } from './molecule.js'
13+
import { trivia } from './trivia.js'
1314

1415
declare const _canonicalized: unique symbol
1516
export type Canonicalized = { readonly [_canonicalized]: true }
@@ -79,6 +80,10 @@ type JSONRecordForbiddingSymbolicKeys = {
7980
}>
8081

8182
export const syntaxTreeParser: Parser<SyntaxTree> = parser.map(
82-
parser.oneOf([atomParser, moleculeParser]),
83-
canonicalize,
83+
parser.sequence([
84+
parser.zeroOrMore(trivia),
85+
parser.oneOf([atomParser, moleculeParser]),
86+
parser.zeroOrMore(trivia),
87+
]),
88+
([_leadingTrivia, syntaxTree, _trailingTrivia]) => canonicalize(syntaxTree),
8489
)

src/language/parsing/trivia.ts

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import { parser } from '../../parsing.js'
2+
3+
const blockComment = parser.sequence([
4+
parser.literal('/*'),
5+
parser.zeroOrMore(
6+
parser.oneOf([
7+
parser.butNot(parser.anySingleCharacter, parser.literal('*'), '*'),
8+
parser.lookaheadNot(parser.literal('*'), parser.literal('/'), '/'),
9+
]),
10+
),
11+
parser.literal('*/'),
12+
])
13+
14+
const singleLineComment = parser.sequence([
15+
parser.literal('//'),
16+
parser.zeroOrMore(
17+
parser.butNot(parser.anySingleCharacter, parser.literal('\n'), 'newline'),
18+
),
19+
])
20+
21+
export const whitespace = parser.regularExpression(/\s+/)
22+
23+
export const trivia = parser.oneOrMore(
24+
parser.oneOf([whitespace, singleLineComment, blockComment]),
25+
)

src/language/parsing/whitespace.ts

Lines changed: 0 additions & 3 deletions
This file was deleted.

src/parsing/combinators.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,24 @@ export const lazy =
3737
input =>
3838
parser()(input)
3939

40+
export const lookaheadNot =
41+
<Output>(
42+
parser: Parser<Output>,
43+
notFollowedBy: Parser<unknown>,
44+
followedByName: string,
45+
): Parser<Output> =>
46+
input =>
47+
either.flatMap(parser(input), success =>
48+
either.match(notFollowedBy(success.remainingInput), {
49+
left: _ => either.makeRight(success),
50+
right: _ =>
51+
either.makeLeft({
52+
input,
53+
message: `input was unexpectedly followed by ${followedByName}`,
54+
}),
55+
}),
56+
)
57+
4058
export const map =
4159
<Output, NewOutput>(
4260
parser: Parser<Output>,

0 commit comments

Comments
 (0)