Skip to content

Commit fd400b3

Browse files
committed
Add generalized keyword expression syntax sugar
1 parent 1a321ee commit fd400b3

File tree

6 files changed

+71
-61
lines changed

6 files changed

+71
-61
lines changed

README.md

Lines changed: 14 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ git clone [email protected]:mkantor/please-lang-prototype.git
99
cd please-lang-prototype
1010
npm install
1111
npm run build
12-
echo '{"@runtime", { context => :context.program.start_time }}' | ./please --output-format=json
12+
echo '@runtime { context => :context.program.start_time }' | ./please --output-format=json
1313
```
1414

1515
There are more example programs in [`./examples`](./examples).
@@ -178,14 +178,18 @@ expressions_. Most of the interesting stuff that Please does involves evaluating
178178
keyword expressions.
179179

180180
Under the hood, keyword expressions are modeled as objects. For example, `:foo`
181-
desugars to `{ "@lookup", { key: foo } }`. All such expressions have a property
182-
named `0` referring to a value that is an `@`-prefixed atom (the keyword). Most
183-
keyword expressions also require a property named `1` to pass an argument to the
184-
expression. Keywords include `@apply`, `@check`, `@function`, `@if`, `@index`,
185-
`@lookup`, `@panic`, and `@runtime`.
181+
desugars to `{ 0: "@lookup", 1: { key: foo } }`. All such expressions have a
182+
property named `0` referring to a value that is an `@`-prefixed atom (the
183+
keyword). Most keyword expressions also require a property named `1` to pass an
184+
argument to the expression. Keywords include `@apply`, `@check`, `@function`,
185+
`@if`, `@index`, `@lookup`, `@panic`, and `@runtime`.
186186

187-
Currently only `@function`, `@lookup`, `@index`, and `@apply` have syntax
188-
sugars.
187+
In addition to the specific syntax sugars shown above, any keyword expression
188+
can be written using a generalized sugar:
189+
190+
```
191+
@keyword { … } // desugars to `{ 0: "@keyword", 1: { … } }`
192+
```
189193

190194
### Semantics
191195

@@ -211,7 +215,7 @@ function from other programming languages, except there can be any number of
211215
`@runtime` expressions in a given program. Here's an example:
212216

213217
```
214-
{"@runtime", { context => :context.program.start_time }}
218+
@runtime { context => :context.program.start_time }
215219
```
216220

217221
Unsurprisingly, this program outputs the current time when run.
@@ -250,7 +254,7 @@ Take this example `plz` program:
250254
{
251255
language: Please
252256
message: :atom.prepend("Welcome to ")(:language)
253-
now: {"@runtime", { context => :context.program.start_time }}
257+
now: @runtime { context => :context.program.start_time }
254258
}
255259
```
256260

examples/fibonacci.plz

Lines changed: 8 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,21 @@
11
{
2-
fibonacci: n => {
3-
"@if"
4-
{
2+
fibonacci: n =>
3+
@if {
54
condition: :n < 2
65
then: :n
76
else: :fibonacci(:n - 1) + :fibonacci(:n - 2)
87
}
9-
}
108

11-
input: {
12-
"@runtime"
13-
{ context => :context.arguments.lookup(input) }
9+
input: @runtime { context =>
10+
:context.arguments.lookup(input)
1411
}
1512

1613
output: :input match {
1714
none: _ => "missing input argument"
18-
some: input => {
19-
"@if"
20-
{
21-
condition: :natural_number.is(:input)
22-
then: :fibonacci(:input)
23-
else: "input must be a natural number"
24-
}
15+
some: input => @if {
16+
condition: :natural_number.is(:input)
17+
then: :fibonacci(:input)
18+
else: "input must be a natural number"
2519
}
2620
}
2721
}.output

examples/kitchen-sink.plz

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,6 @@
88
add_one: :integer.add(1)
99
three: :add_one(:two)
1010
function: x => { value: :x }
11-
conditional_value: :function({ "@if", { :sky_is_blue, :two, :three } })
11+
conditional_value: :function(@if { :sky_is_blue, :two, :three })
1212
side_effect: { "@runtime", { context => :context.log("this goes to stderr") } }
1313
}

examples/lookup-environment-variable.plz

Lines changed: 7 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -2,16 +2,12 @@
22
* Given CLI arguments like `--variable=FOO`, looks up the environment
33
* variable named `FOO`.
44
*/
5-
{
6-
"@runtime"
7-
{
8-
context =>
9-
:context.arguments.lookup(variable) match {
10-
none: {}
11-
some: :context.environment.lookup >> :match({
12-
none: {}
13-
some: :identity
14-
})
15-
}
5+
@runtime { context =>
6+
:context.arguments.lookup(variable) match {
7+
none: {}
8+
some: :context.environment.lookup >> :match({
9+
none: {}
10+
some: :identity
11+
})
1612
}
1713
}

src/end-to-end.test.ts

Lines changed: 22 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -28,9 +28,9 @@ testCases(endToEnd, code => code)('end-to-end tests', [
2828
['{,a,b,c,}', either.makeRight({ 0: 'a', 1: 'b', 2: 'c' })],
2929
['{a,1:overwritten,c}', either.makeRight({ 0: 'a', 1: 'c' })],
3030
['{overwritten,0:a,c}', either.makeRight({ 0: 'a', 1: 'c' })],
31-
['{"@check", {type:true, value:true}}', either.makeRight('true')],
31+
['@check {type:true, value:true}', either.makeRight('true')],
3232
[
33-
'{"@panic"}',
33+
'@panic',
3434
result => {
3535
assert(either.isLeft(result))
3636
assert('kind' in result.value)
@@ -42,7 +42,7 @@ testCases(endToEnd, code => code)('end-to-end tests', [
4242
['{a:A, b: :a}', either.makeRight({ a: 'A', b: 'A' })],
4343
['{a:A, :a}', either.makeRight({ a: 'A', 0: 'A' })],
4444
[
45-
'{"@runtime", {_ => {"@panic"}}}',
45+
'@runtime {_ => @panic}',
4646
result => {
4747
assert(either.isLeft(result))
4848
assert('kind' in result.value)
@@ -153,9 +153,9 @@ testCases(endToEnd, code => code)('end-to-end tests', [
153153
],
154154
[':match({ a: A })({ tag: a, value: {} })', either.makeRight('A')],
155155
[
156-
`{"@runtime", { context =>
156+
`@runtime { context =>
157157
:identity(:context).program.start_time
158-
}}`,
158+
}`,
159159
output => {
160160
if (either.isLeft(output)) {
161161
assert.fail(output.value.message)
@@ -181,9 +181,7 @@ testCases(endToEnd, code => code)('end-to-end tests', [
181181
[`(1 - 2) - 3`, either.makeRight('-4')],
182182
[':flow(:atom.append(b))(:atom.append(a))(z)', either.makeRight('zab')],
183183
[
184-
`{"@runtime"
185-
{ :object.lookup("key which does not exist in runtime context") }
186-
}`,
184+
`@runtime { :object.lookup("key which does not exist in runtime context") }`,
187185
either.makeRight({ tag: 'none', value: {} }),
188186
],
189187
[
@@ -213,7 +211,7 @@ testCases(endToEnd, code => code)('end-to-end tests', [
213211
either.makeRight({ true: 'true', false: 'false' }),
214212
],
215213
[
216-
`{"@runtime", {
214+
`@runtime {
217215
:flow(
218216
:match({
219217
none: "environment does not exist"
@@ -228,7 +226,7 @@ testCases(endToEnd, code => code)('end-to-end tests', [
228226
})
229227
)(
230228
:object.lookup(environment)
231-
)}
229+
)
232230
}`,
233231
output => {
234232
if (either.isLeft(output)) {
@@ -260,9 +258,9 @@ testCases(endToEnd, code => code)('end-to-end tests', [
260258
either.makeRight({ 0: 'a', 1: 'b', 2: 'c', 3: 'd' }),
261259
],
262260
[
263-
`{"@runtime", { context =>
261+
`@runtime { context =>
264262
:context.environment.lookup(PATH)
265-
}}`,
263+
}`,
266264
output => {
267265
if (either.isLeft(output)) {
268266
assert.fail(output.value.message)
@@ -273,11 +271,11 @@ testCases(endToEnd, code => code)('end-to-end tests', [
273271
},
274272
],
275273
[
276-
`{"@if", {
274+
`@if {
277275
true
278276
"it works!"
279-
{"@panic"}
280-
}}`,
277+
@panic
278+
}`,
281279
either.makeRight('it works!'),
282280
],
283281
[
@@ -289,24 +287,23 @@ testCases(endToEnd, code => code)('end-to-end tests', [
289287
either.makeRight({ 0: 'a', 1: 'b', 2: 'c' }),
290288
],
291289
[
292-
`{"@runtime", { context =>
293-
{"@if", {
290+
`@runtime { context =>
291+
@if {
294292
:boolean.not(:boolean.is(:context))
295293
"it works!"
296-
{"@panic"}
297-
}}
298-
}}`,
294+
@panic
295+
}
296+
}`,
299297
either.makeRight('it works!'),
300298
],
301299
[
302300
`{
303-
fibonacci: n => {
304-
"@if", {
301+
fibonacci: n =>
302+
@if {
305303
:integer.less_than(2)(:n)
306304
then: :n
307305
else: :fibonacci(:n - 1) + :fibonacci(:n - 2)
308306
}
309-
}
310307
result: :fibonacci(10)
311308
}.result`,
312309
either.makeRight('55'),
@@ -362,7 +359,7 @@ testCases(endToEnd, code => code)('end-to-end tests', [
362359
either.makeRight('2'),
363360
],
364361
[
365-
`{"@runtime", { context =>
362+
`@runtime { context =>
366363
(
367364
PATH
368365
|> :context.environment.lookup
@@ -371,7 +368,7 @@ testCases(endToEnd, code => code)('end-to-end tests', [
371368
some: :atom.prepend("PATH=")
372369
})
373370
)
374-
}}`,
371+
}`,
375372
result => {
376373
if (either.isLeft(result)) {
377374
assert.fail(result.value.message)

src/language/parsing/expression.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,10 +13,12 @@ import { keyPathToMolecule, type KeyPath } from '../semantics.js'
1313
import {
1414
atom,
1515
atomWithAdditionalQuotationRequirements,
16+
unquotedAtomParser,
1617
type Atom,
1718
} from './atom.js'
1819
import {
1920
arrow,
21+
atSign,
2022
closingBrace,
2123
colon,
2224
comma,
@@ -371,6 +373,22 @@ const precededByAtomThenArrow = map(
371373
},
372374
)
373375

376+
// @runtime { context => … }
377+
// @panic
378+
// @todo blah
379+
const precededByAtSign = map(
380+
sequence([
381+
atSign,
382+
unquotedAtomParser,
383+
optionalTrivia,
384+
optional(lazy(() => expression)),
385+
]),
386+
([_atSign, keyword, _trivia, argument]) => ({
387+
0: `@${keyword}`,
388+
1: argument === undefined ? {} : argument,
389+
}),
390+
)
391+
374392
// :a
375393
// :a.b
376394
// :a.b(1).c
@@ -421,6 +439,7 @@ export const expression: Parser<Atom | Molecule> = map(
421439
oneOf([
422440
precededByOpeningParenthesis,
423441
precededByOpeningBrace,
442+
precededByAtSign,
424443
precededByColonThenAtom,
425444
precededByAtomThenArrow,
426445
atom,

0 commit comments

Comments
 (0)