-
Notifications
You must be signed in to change notification settings - Fork 562
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
Prevent typeclass instance self-reference (#3429). #3638
base: master
Are you sure you want to change the base?
Prevent typeclass instance self-reference (#3429). #3638
Conversation
Sorry about the lack of feedback on this so far. It looks interesting, but also tricky, so I've not had a chance to check out what's going on yet. We're mainly focusing on planned breaking changes to get stuff in shape for the 0.13 release, so it might not get a look until after that. |
@@ -332,6 +332,7 @@ everythingWithContextOnValues | |||
-> (r -> r -> r) | |||
-> (s -> Declaration -> (s, r)) | |||
-> (s -> Expr -> (s, r)) | |||
-> (s -> Expr -> (s, r)) |
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.
Can we document the change to this traversal in a comment? It wouldn't be clear to me just seeing this signature why we have a seemingly duplicated parameter like this that isn't present in other traversals.
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.
Yeah, that makes sense. Documentation would definitely be helpful here.
Any thoughts on the following documentation attempt? Suggestions for making it clearer (more accurate) (more relevant) for example?
-- |
-- A fold for paramorphisms associated with (1) an initial object represented
-- by the coproduct 'Node'
-- @
-- data Node
-- = A Declaration
-- | NonLiteral Expr
-- | Literal Expr
-- | B Binder
-- | C CaseAlternative
-- | D DoNotationElement
-- @
-- and (2) the functorial context
-- > type Context s r = (s, r, r -> r -> r, s -> Node -> (s, r))
--
-- Given an initial state, a default output value, a binary action on the
-- output type, and six independent state-transition transformers (one each for
-- 'Declaration's, 'Binder's, 'CaseAlternative's, and 'DoNotationElement's; and
-- two for 'Expr's), determine five corresponding mutually recursive data-
-- gathering functions that generate "measurements" of type 'r' for values of
-- any of the constituent types of the coproduct 'Node'.
--
-- Two input functions for 'Expr' are required in order to allow distinguishing
-- of values inside 'Literal' 'Expr's from values independent of literal objects
-- and arrays. (The function 'immediateLitIdentsAndAllOtherIdents' in module
-- 'Language.PureScript.Sugar.BindingGroups' is an example of a client for
-- this feature.)
--
everythingWithContextOnValues
:: forall s r
. s
-> r
-> (r -> r -> r)
-> (s -> Declaration -> (s, r))
-> (s -> Expr -> (s, r))
-- ^ Transformer of 'Expr' nodes without 'Literal' ancestors
-> (s -> Expr -> (s, r))
-- ^ Transformer of 'Expr' nodes strictly dominated by a 'Literal' 'Expr'
-> (s -> Binder -> (s, r))
-> (s -> CaseAlternative -> (s, r))
-> (s -> DoNotationElement -> (s, r))
-> ( Declaration -> r
, Expr -> r
, Binder -> r
, CaseAlternative -> r
, DoNotationElement -> r)
(Thanks for your feedback by the way.)
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 realize it's probably a little obnoxious to necro-bump a PR without actually reviewing the important part of it, so sorry; whether this approach is a good idea or not is above my pay grade. But I wanted to register my confusion with respect to this change to everythingWithContextOnValues
. Why is this necessary? Couldn't the condition of being inside a Literal
be included as part of the context? I realize that would make the implementation of immediateLitIdentsAndAllOtherIdents
more complicated but I don't (yet?) buy that this belongs in a generic traversal.
Resolves #3429.
Previously, non-function members of constrained typeclass instances could reference each other. Because members also implicitly reference the overarching typeclass dictionary, the resulting cycle in a typeclass declaration between a dictionary and its members can cause runtime misbehavior.
For example, @LiamGoodacre, in #3429, documents the following problematic typeclass declarations:
Because
what
andfoo
reference each other in the following generated javascript, any invocation ofwhat
causes a stack-overflow error.The previously established procedure for preventing cycles in typeclass declarations fails in @LiamGoodacre's scenario because, in order to allow implementations of superclass methods to depend on subclass implementations (cf.
functorEffect
andapplicativeEffect
), the desugaring process in "Sugar/BindingGroups.hs" deliberately allows cyclical references if they occur inside lambda abstractions. And since, during type elaboration, constrained typeclass dictionaries are represented as abstractions, dictionary cycles fail to trigger a "CycleInDeclaration" error.Code changes in this pull request fix issue #3429 by adding a new cycle check that is specific to typeclass value declarations. Unlike the primary check, it disallows self-reference even if it occurs within abstractions outside literal objects. Therefore, the implicit cycle in the
what
instance above (and the explicit cycle in the desugared "pseudo"-purescript representation below) induces an error.Like the primary cycle check, the new check permits self-reference if it occurs within a bare abstraction of an object member. For example, the following class and instance successfully compile.
Or in pseudo-purescript:
However, unlike the primary cycle check, self-reference in purescript typeclass "IFFE"s is now considered invalid. This is designed to prevent the following scenario.
The corresponding pseudo-purescript may make the reason for proscription clearer.
Constrained typeclass members, like constrained typeclasses, are represented as abstractions. However, they are also immediately invoked, and so the same kind of deviant runtime behavior as documented by @LiamGoodacre can occur unless screened.
Typeclass self-reference, even with the introduced cyclicality check, is still possible. The earlier case
is one example. Other possibilities arise when the members of class declarations are constrained.
For instance, the following purescript
or pseudo-purescript,
despite its pronounced and unhelpful self-reference, compiles successfully. Unlike constrained instance members, constrained class members are not immediately invoked, and therefore, the abstraction exception for self-reference takes precedence.
This might be considered a distasteful loophole by the cyclicality screening procedure. Similar cases unrelated to constrained typeclasses already exist, however. Consider the following unconstrained instance.
Or even functions independent of typeclasses altogether like
g
below.In addition to more robust cycle prevention, this pull request also improves error messages for end users.
Error messages for typeclasses now specify the individual members of an instance declaration that contribute to the error -- as well as the members' locations in source code.
For example,
induces an error message like the following.
Additionally, error messages distinguish between instance members that are functions and those that aren't in order to provide additional guidance to end users.
For example,
induces an error message like the following.