-
Notifications
You must be signed in to change notification settings - Fork 111
TM026 block shorthand
Currently in Pyret, each block can hold multiple whitespace-separated expressions, and blocks typically evaluate to the value of the last (check bocks are an exception).
The vast majority of blocks hold a single expression. Pedagogically, it's not frequent that a student intends to write two expressions in a block until they are a pretty savvy programmer. If they do accidentally, it's all of a sudden necessary to explain that Pyret evaluated their first expression and threw out the value, which is why they're seeing the behavior they're getting.
It would be nice to restrict blocks by default to have only a (possibly empty)
sequence of let-bindings followed by a single expression. Of course, this
restriction on its own is quite severe; it directly impacs print
debugging,
having a sequence of when
-based error checks at the front of a function,
and writing stateful algorithms that need to sequence operations.
This proposal aims to reconcile these two desires.
This proposal has two parts. For the first, the goal is to logically introduce
a new production—body
—which is as described above:
body := (let-expr *) binop-expr
This new body
production will replace block
in the following contexts:
fun-expr
multi-let-expr
letrec-expr
type-let-expr
when-expr
lambda-expr
method-expr
-
obj-field
(in the method case) if-expr
else-if
-
if-pipe-expr
(AKA "ask") if-pipe-branch
cases-branch
for-expr
It does not replace block
in the following contexts:
check-expr
where-clause
user-block-expr
program
Since changing the grammar in this way will simply cause poor parse error
messages, we will instead keep the block
production in place, and check for
this shape in well-formedness, where we can give a much more useful error
message.
Note that if this was all we did, we could still write sequencing code by using
block: ... end
. So a function that needed to mutate a variable and return
value could do so:
var x = 0
fun gensym(s):
block:
x := x + 1
s + num-to-string(x)
end
end
The extra typing and indentation, especially to add a call to a debugging
helper, is quite onerous. It would be great to have a way to not need to
introduce block: ... end
into all of these contexts.
In order to recover the ability to use blocks with sequences of expressions, we
will augment all of the forms above with an optional additional keyword that
indicates all direct sub-blocks may have sequences of expressions, and are not
limited to body
. So instead of the above, we would write:
var x = 0
fun gensym(s) block:
x := x + 1
s + num-to-string(x)
end
With annotations:
var x = 0
fun gensym(s :: String) -> String block:
x := x + 1
s + num-to-string(x)
end
(The keyword seq
as opposed to block
has also been proposed.)
Some other examples, which would all be well-formedness errors with block
omitted:
ask block:
| key == "left" then:
print(key)
move-player-left()
| key == "right" then:
move-player-right()
end
for map(elt from lst) block:
store-in-cache(elt)
transform(elt)
end
cases(List<Number>) l block:
| empty => empty
| link(f, r) =>
when f == "sentinel":
raise("Sentinel value detected, that's an error")
end
link(transform(f), r)
end
These additions would happen in the grammar, and would require adding flags or new variants for all of the relevant AST types so that well-formedness could make the right decisions. Some form of flag is probably the best to avoid bloating the size of ast.arr more than necessary.