-
Notifications
You must be signed in to change notification settings - Fork 440
Implementation and spec: clarify and implemen t common return type classes #28110
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
base: main
Are you sure you want to change the base?
Conversation
Signed-off-by: Danila Fedorin <[email protected]>
Signed-off-by: Danila Fedorin <[email protected]>
Signed-off-by: Danila Fedorin <[email protected]>
jabraham17
left a comment
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.
In general, this looks good.
But given that this modifies the spec in a significant way, this to me goes beyond just being a bug fix. At a minimum, I think this needs to be discussed with the wider community. It also may break "stability", in the sense that 1 program may compile with one version of the compiler but not another in the same edition. If this is just a bug fix, not a problem. But this feels like more than that (changing the specs meaning).
| exist one type such that an implicit conversion is allowed | ||
| between every other type and that type, and such that it can be implicitly | ||
| converted into any other type that satisfies the previous condition. That | ||
| type becomes the inferred return type. If the above requirements are not | ||
| satisfied, it is an error. | ||
|
|
||
| *Example (infer-parent-type-from-classes.chpl)*. | ||
|
|
||
| In this example, two classes ``Child1`` and ``Child2`` inherit from | ||
| a parent class ``Parent``, which in turn inherits from the class |
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.
consider also adding an example that doesn't use classes
| exist one type such that an implicit conversion is allowed | ||
| between every other type and that type, and such that it can be implicitly | ||
| converted into any other type that satisfies the previous condition. That | ||
| type becomes the inferred return type. If the above requirements are not | ||
| satisfied, it is an error. |
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.
please also make clear the stipulation about no coercion to RootClass
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.
a few nits about the tests
- please add some unmanaged test cases
- please add some more test cases like the one in the spec with grandparents
| } | ||
|
|
||
| // Helper function to find a common parent type for class hierarchies. | ||
| // Returns NULL if no common parent can be found. |
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.
nit, returns nullptr not NULL
| // parent type for class hierarchies (addresses issue #14765). | ||
| if (retType == dtUnknown && retTypes.n > 1) { | ||
| Type* commonParent = findCommonParentForClasses(retTypes, retSymbols, fn); | ||
| if (commonParent != NULL) { |
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.
nit, nullptr not NULL
| // Track the decorator (managed/borrowed/unmanaged) and nilability. | ||
| // If decorators don't match and one of them is borrowed, guess borrowed. | ||
| chpl::optional<ClassTypeDecoratorEnum> commonDecorator; | ||
| bool anyNilable = false, managerMismatch = false; |
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 worries me somewhat.
proc foo(x) {
if x then return new shared ...
else return new owned ...;
}The return type should either be owned or shared, not borrowed. Returning borrowed will cause memory issues (borrowed class to an owned/shared that no longer exists). Similarly,
proc foo(x) {
if x then return myGlobalBorrowedClass;
else return new owned ...;
}If the return type is borrowed, in the else branch case you get the same memory issue
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.
In the first case the return is not computed. Borrowed is only an option if one of the returns is borrowed.
You also don't get memory issues because of lifetime checking. So if you mix new shared and borrowed, you get an error. That's why my tests create globals variables - they have to.
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.
In the first case the return is not computed. Borrowed is only an option if one of the returns is borrowed.
Can you add tests of this?
You also don't get memory issues because of lifetime checking
Yeah I agree you would get lifetime checking errors, but it seems a little weird for the compiler to let you do that. Infering the return type to be borrowed when I return an owned because another branch returns an owned is surprising behavior to me (even if the global case works)
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 I agree you would get lifetime checking errors, but it seems a little weird for the compiler to let you do that.
It might be surprising, but it's part of the (original and updated) spec.
| AggregateType* seenManager = nullptr; | ||
|
|
||
| // Extract canonical class types (strip decorators) | ||
| Vec<AggregateType*> canonicalClasses; |
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.
consider a small vec for this
| INT_ASSERT(canonicalClasses.n == retTypes.n); | ||
|
|
||
| // Build a list of ancestors for the first class | ||
| Vec<AggregateType*> ancestors; |
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.
nit, consider smallvec
| AggregateType* current = canonicalClasses.v[0]; | ||
| ancestors.add(current); | ||
|
|
||
| // Walk up the parent chain | ||
| while (current->dispatchParents.n > 0) { | ||
| current = current->dispatchParents.v[0]; | ||
| if (current == dtObject) break; | ||
| ancestors.add(current); | ||
| } |
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.
im surprised there is no existing helper for this
|
In terms of stability, this is not a breaking change. It enables more patterns to work. It specifically modifies code that previously was guaranteed to emit a compiler error and exit. Historically we have been okay with such changes. My changes to the spec do not change the semantics encoded therein. Rather, they fix an error in the spec. This is true unless you consider it an error for Child1 to coerce to Parent when there is a Grandparent present (the old spec considers this a bug). The spec always claimed it to be possible to coerce children to each other -- see the original formulation. This is explicitly allowed in the spec and not implemented in the compiler. |
|
I am not saying this is a breaking change, but there is an important distinction. If this was the intended behavior all along according to the spec, then this is a bug fix and I have no concerns. But if the spec changes (i.e. the language changes), then in compiler version A a program may not compile but in compiler version B it will. This breaks editions, because I can no longer guarantee my code will work within a given edition (barring bug fixes).
Thats not how I read it. The statement "Thus, the uniqueness condition is not met, and the return type according to spec should be an error. This seems needlessly restrictive.". Per this statement, you are relaxing the spec for what determines the return type, which is a language change |
This PR fixes issues with the spec and implements a missing behavior, thereby closes #14765.
Throughout this, I'll use the following four classes:
To start with, our spec seems to intend to allow the return type inference when one branch returns
Child1and one branch returnsChild2. However, to Michael's surprise in the original motivator issue, today, this doesn't occur.I believe the spec intends for this to work because it's stated in terms of there existing a "common type", which does exist for
Child1andChild2: that type isParent. However, today, the Chapel compiler only computes the return type by checking if return types from all other branches can be coerced to the return type of one branch. NeitherChild1norChild2is a subtype of the other, so this fails.However, I think the spec formulation is wrong, because it asks for there to be a unique type to which all branches are coercible. This is a very strong condition. For instance, it disallows inference in both of the following cases:
Child1and branch 2 returnsChild2. There are two types to whichChild1andChild2are both coercible:ParentandGrandparent. Thus, the uniqueness conditin is not met, and the return type according to spec should be an error. This seems needlessly restrictive.Child1, branch 2 returnsParent. Once again, bothParentandGrandparentare valid types to which both branches' returns can be coerced. So, according to spec, this should be disallowed. This one is particularly heinous because production compiler allows it.Both of these deductions seem to go against the spirit of the spec, which I would say allows both conversions. The key insight is that although
ParentandGrandparentare both valid types,Parentis more specific. Specifically,Parentcan be coerced toGrandparent, but not the other way around. Thus, intuitively,Parentmakes more sense.I adjust a spec with this as an updated condition (that there needn't be a unique target type, but that if there are multiple target types, one needs to be convertable to all the others). This is, by the way, a classic coproduct construction. To clarify the rather dense definition, I add an example.
Not only is the behavior absent from the spec, but it's also not implemented. So, in this PR, before returning an error for failing to compute a return type by normal (all-branches-coercible-to-one) means, I add an additional procedure to compute the common class type where possible. At this time, I don't allow coercions to
objectto allow cases like accidentally returning unrelated classes and the return types unifying toobject.Testing