-
Notifications
You must be signed in to change notification settings - Fork 7
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
ellipses #37
ellipses #37
Conversation
No question. Make ellipses mandatory. Adding contracts to exports makes the spec more verbose than an identifier but clarifies things at the same time. Adding You name it, these small verbosities (seems to be an okay word) are good signals to the future reader. (I have no code base that depends on |
(struct rec with-stx [depth pvar] #:transparent) | ||
; no surface syntax, just a mechanism to combine recs | ||
; something like [(import x) (import y)] ~> (recs (list (rec x) (rec y))) | ||
(struct recs [specs] #:transparent) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The field of recs could simply contain pvars; they'll get there from the pass that combines together import
forms into a single recs
form, which will happen after things like depth checking.
Great! Seems like everybody is happy with explicit ellipses. @quasarbright I think it then definitely makes sense to try and do #33 and get rid of |
I agree. For now, to get ellipses merged, i'll address your review comments first and then I can do ellipsized implicits after since it should be a non-breaking behavioral change |
a720ffd
to
b5e5f1a
Compare
(define bspec-distributed-ellipses (bspec-flatten-groups (bspec-distribute-ellipses bspec-flattened))) | ||
(define bspec-absorbed-ellipses (bspec-absorb-ellipses-into-imports bspec-distributed-ellipses)) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't like this and I'm not sure if there is a case that can break this. But I couldn't come up with a better idea
(host-interface/expression | ||
(many-local ([d:defn ...] ...) body:racket-expr) | ||
; this group is unnecessary, but we want to test the behavior of ellipsized groups with imports | ||
#:binding (scope [[[[(import d)]] ...] ...] body) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this tests that weird ellipsized grouped import case
Discussing today, we realized there are several further design issues with ellipses and the operational order of expansion. Ellipsized groups mixing binding and referenceConsider this production for a
The design intent in syntax-spec is that the operational order of expansion matches the left-to-right reading of the binding rule. Extending that to ellipses, we would want the order to follow the reading of the binding rule expanded out with concrete instances. For example, for this letrec instance:
we would read order of bindings and references as:
Notice that we refer to Previously our solution has been a static restriction on the order of forms in a We plan to disallow mixing of different categories of operators within an ellipsized group. The following will be mutually exclusive within such a group:
Then, the order checking rules can operate on these homogenous ellipsized groups rather than single operators. Order of import operationsEven within imports alone, the order can matter. In a Racket definition context, the resolution of syntactic form names or macros in the first pass of expansion can depend on bindings established earlier in that first pass. For example, this expands:
If we allow multiple Consider this strange form with two groups of definitions:
This binding rule suggests that the first definition in defs1 will expand, then the first in defs2, then the second in defs1, and so on. So this should expand successfully:
On the other hand, if we wrote the spec this way it would error:
With this latter spec all the defs1 would pass1-expand before any of the defs2. We think that since we do currently allow multiple imports in a scope, we should be careful to match the apparent operational order in this way. We can realize this operational order in the compilation of imports:
Alternative options:
|
My personal preference would be to start with a restrictive version and check how often it fails with existing code. Then relax in the simplest way possible until we reach equilibrium or we decide that what people have already written, we don't want to support. |
Can we add this as a test? It checks that the different options for combining ellipses with multiple imports produce the expected order of expansion in the first pass.
Could also add error-case tests where the macro use is in the wrong order wrt the def and isn't bound. |
necessary to guarantee that imports only contains imports by the time we reach the order check
for homogeneity error, highlight the group instead of the ellipses, and say "cannot mix imports or exports with other kinds of binding specs |
to fix ...+ not working, match on datum instead of literal for it in |
This PR introduces an ellipsis syntax in binding specs, and makes them mandatory. Spec variable references in binding specs must occur at the same ellipsis depth as their syntax spec counterparts.
Motivation
Ellipses help control the binding structure of syntax with sequences. For example:
In the above example, we're defining a version of
cond
that binds the result of the test in the branch's body. In the face of sequences of syntax,syntax-spec
infers the binding structure automatically. However, it cannot distinguish between this cond-like binding structure and the let-like binding structure. In both cases, there is a list of identifiers being bound in a list of bodies. But should each identifier be bound only in the corresponding body? Or all bodies? The inferred structure binds each variable in all bodies, which is wrong in this case.To allow users more control over binding structure, we should add ellipses to binding specs.
Here is the example re-written with ellipses:
The placement of ellipses in binding specs distinguishes between the two cases.
I have verified that this properly treats the reference to
x
in the preceding example as unbound in the ellipsis implementation.Interaction with nesting
Ellipses also change the syntax for
nest
:Note that
nest
has internal ellipses for the binding pair sequence. This declares that the sequence is to be folded over.nest-one
no longer exists. Just usenest
with no ellipses on the first argument. So(nest-one b e)
is now written as(nest b e)
.Interaction with -syntaxes bindings forms
The
export-syntaxes
andbind-syntaxes
forms create simultaneous syntax bindings for a number of identifiers, using the multiple values returned from a single compile-time expression, similar to Racket'sdefine-syntaxes
.These forms now have internal ellipses for the sequence of identifiers:
Drawbacks
Syntaxes like
my-cond
where the current implicit behavior is incorrect have been rare in practice.Requiring ellipses everywhere makes many binding specs somewhat more verbose. For example:
vs the previous
Or,
vs
An alternative: optional ellipses
Should ellipses be mandatory? This PR includes a static check that ensures all variables are properly ellipsized. However, it would also be possible to make ellipses optional and default to the current inferred behavior when they are not present. This would ensure all binding structures are possible to specify without requiring ellipses all the time. We could still statically check that there are not too many ellipses, since that would cause problems, but with too few ellipses, the old behavior of inferred binding structure would be used.
One subtle issue with this more implicit design is
nest
. In the implicit design, it seems most consistent with the behavior of the other binding forms that we be able to write what are nownest
andnest-one
in exactly the same way, relying on the ellipsis depth of the pattern variable binding to choose between the meanings. So a let* with a sequence of binding pairs:would have the same binding rule as a form with a single such binding pair:
In the first case, nest is folding over a sequence, whereas in the second case it is nesting a single element. This implicit difference might be confusing.