Inlines: decide sealing against original target, while using widened type as cast destination - #25448
Conversation
|
I'd request review from @mbovel (he already assigned himself though ð ) |
| else | ||
| inlined.ensureConforms(resultType) | ||
| // Here we can't just use `inlined.ensureConforms(resultType)` | ||
| // `target.widenIfUnstable` is an upper approximation of `target`. | ||
| // a tree may conform to `target.widenIfUnstable` while still not conforming to `target`. | ||
| // This can happen when widening drops path-/prefix-sensitive information (e.g. projected opaque-proxy types). | ||
| // So adaptation must be decided against the original `target`, while we cast to the widened type. | ||
| // see https://github.com/scala/scala3/issues/25417 | ||
| if inlined.ensureConforms(target) eq inlined then | ||
| inlined | ||
| else | ||
| inlined.cast(resultType) |
There was a problem hiding this comment.
The current code uses ensureConforms(target) only to check whether a cast is needed (it discards the result via eq inlined). This is a roundabout way of asking "does the type conform?" and has the side effect of potentially creating an unnecessary intermediate cast tree that gets thrown away.
I think we could directly test:
if inlined.tpe <:< target then
inlined
else
inlined.cast(resultType)which looks clearer to me.
This is equivalent because ensureConforms is defined as:
scala3/compiler/src/dotty/tools/dotc/ast/tpd.scala
Lines 1071 to 1078 in bf48ee8
We could also flatten to a single if then ... else if ... else ....
| else | |
| inlined.ensureConforms(resultType) | |
| // Here we can't just use `inlined.ensureConforms(resultType)` | |
| // `target.widenIfUnstable` is an upper approximation of `target`. | |
| // a tree may conform to `target.widenIfUnstable` while still not conforming to `target`. | |
| // This can happen when widening drops path-/prefix-sensitive information (e.g. projected opaque-proxy types). | |
| // So adaptation must be decided against the original `target`, while we cast to the widened type. | |
| // see https://github.com/scala/scala3/issues/25417 | |
| if inlined.ensureConforms(target) eq inlined then | |
| inlined | |
| else | |
| inlined.cast(resultType) | |
| else if !(inlined.tpe <:< target) then | |
| // Make sure that the sealing with the declared type | |
| // is type correct. Without it we might get problems since the | |
| // expression's type is the opaque alias but the call's type is | |
| // the opaque type itself. An example is in pos/opaque-inline1.scala. | |
| // | |
| // Here we can't just use `inlined.ensureConforms(resultType)`: | |
| // `target.widenIfUnstable` is an upper approximation of `target`, | |
| // so a tree may conform to it while still not conforming to `target`. | |
| // This can happen when widening drops path-/prefix-sensitive information | |
| // (e.g. projected opaque-proxy types). | |
| // We check conformance against the original `target`, but cast to the | |
| // widened type to avoid NoType issues at erasure (see #25091, #25417). | |
| inlined.cast(resultType) | |
| else | |
| inlined |
This version also passes all compilation tests.
I suspect there is more room for streamlining this logic (including forceCast), but we can revisit later ð
There was a problem hiding this comment.
Thanks! I used ensureConforms(target) on purpose so that the logic doesn't diverse, but
has the side effect of potentially creating an unnecessary intermediate cast tree that gets thrown away.
that's right. Considering ensureConforms is pretty simple (especially before erasure), let's streamline the condition :)
| val withAdjustedThisTypes = if call.symbol.is(Macro) then fixThisTypeModuleClassReferences(unpacked) else unpacked | ||
| (call.tpe & withAdjustedThisTypes, withAdjustedThisTypes != unpacked) | ||
| else (call.tpe, false) | ||
| // target might contain method reference, which is invalid cast target. Use it's return type instead |
There was a problem hiding this comment.
| // target might contain method reference, which is invalid cast target. Use it's return type instead | |
| // `target` might contain a method reference, which is an invalid cast target. Use its return type instead. |
âĶtype as cast destination Fixes scala#25417 scala#25427 Regression introduced by 816fc6c (scala#25111), which fixed scala#25091. Background: - scala#25091 showed that inline sealing could cast target contains method-reference that results in `notype` at erasure. - scala#25111 fixed this by using `target.widenIfUnstable` to remove method-reference from cast destination, and use it's return type instead. - However, deciding adaptation against the widened type is too weak and could skip a required cast, and leak projected opaque-proxy types (scala#25417). For example: ```scala import scala.compiletime.summonInline trait MyEvidence[T] object Scope: opaque type T = String inline given MyEvidence[T] with {} inline def summonInlineNoOpProxy: MyEvidence[Scope.T] = summonInline[MyEvidence[Scope.T]] val smoke = ( summonInline[MyEvidence[Scope.T]], summonInlineNoOpProxy, ) ``` For `summonInline[MyEvidence[Scope.T]]`: - `inlined.tpe`: `Scope.type{type T = String}#given_MyEvidence_T` - `target` (before widening): `(Scope.given_MyEvidence_T : => Scope.given_MyEvidence_T)` - `resultType = target.widenIfUnstable`: `Scope.given_MyEvidence_T` Previously - `inlined.tpe <:< resultType` hold, so adaptation is skipped. - Therefore, no cast is inserted, inlined tree type remains`Scope.type{type T = String}#given_MyEvidence_T` - This is seen as `MyEvidence[String]` - Later typing fails for required `MyEvidence[Scope.T]` vs `MyEvidence[String]` (path dependence infor is dropped). --- This commit fixes the problem by: decide whether adaptation is needed against original `target` (the real call-site contract), while using `target.widenIfUnstable` as a cast destination, so that scala#25091 fix) `target.widenIfUnstable` is an upper approximation of `target`. Conformance to widened type is weaker than conformance to original `target`. Therefore adaptation-need must be checked against `target`, while widened type is used only as cast destination. Co-Authored-By: Matt Bovel <matthieu@bovel.net>
âĶtype as cast destination (scala#25448) Fixes scala#25417 fixes scala#25427 Regression introduced by tanishiking@816fc6c (scala#25111), which fixed scala#25091. Background: - scala#25091 showed that inline sealing could cast target contains method-reference that results in `notype` at erasure. - scala#25111 fixed this by using `target.widenIfUnstable` to remove method-reference from cast destination, and use it's return type instead. - However, deciding adaptation against the widened type is too weak and could skip a required cast, and leak projected opaque-proxy types (scala#25417). For example: ```scala import scala.compiletime.summonInline trait MyEvidence[T] object Scope: opaque type T = String inline given MyEvidence[T] with {} inline def summonInlineNoOpProxy: MyEvidence[Scope.T] = summonInline[MyEvidence[Scope.T]] val smoke = ( summonInline[MyEvidence[Scope.T]], summonInlineNoOpProxy, ) ``` For `summonInline[MyEvidence[Scope.T]]`: - `inlined.tpe`: `Scope.type{type T = String}#given_MyEvidence_T` - `target` (before widening): `(Scope.given_MyEvidence_T : => Scope.given_MyEvidence_T)` - `resultType = target.widenIfUnstable`: `Scope.given_MyEvidence_T` Previously - `inlined.tpe <:< resultType` hold, so adaptation is skipped. - Therefore, no cast is inserted, inlined tree type remains`Scope.type{type T = String}#given_MyEvidence_T` - This is seen as `MyEvidence[String]` - Later typing fails for required `MyEvidence[Scope.T]` vs `MyEvidence[String]` (path dependence infor is dropped). --- This commit fixes the problem by: decide whether adaptation is needed against original `target` (the real call-site contract), while using `target.widenIfUnstable` as a cast destination, so that scala#25091 fix) `target.widenIfUnstable` is an upper approximation of `target`. Conformance to widened type is weaker than conformance to original `target`. Therefore adaptation-need must be checked against `target`, while widened type is used only as cast destination. <!-- Fixes #XYZ (where XYZ is the issue number from the issue tracker) --> <!-- TODO description of the change --> <!-- Ideally should have a title like "Fix #XYZ: Short fix description" --> <!-- TODO first sign the CLA https://contribute.akka.io/cla/scala --> <!-- if the PR is still a WIP, create it as a draft PR (or convert it into one) --> ## How much have your relied on LLM-based tools in this contribution? <!-- State clearly in the pull request description, whether LLM-based tools were used and to what extent (extensively/moderately/minimally/not at all) --> <!-- Refer to our [LLM usage policy](https://github.com/scala/scala3/blob/main/LLM_POLICY.md) for rules and guidelines regarding usage of LLM-based tools in contributions. --> Worked with GPT5.3-Codex ## How was the solution tested? `testCompilation (i25091|i25417|i25427)` <!-- If automated tests are included, mention it. If they are not, explain why and how the solution was tested. --> ## Additional notes <!-- Placeholder for any extra context regarding this contribution. --> <!-- When in doubt, and for support regarding contributions to a particular component of the compiler, refer to [our contribution guide](https://github.com/scala/scala3/blob/main/CONTRIBUTING.md), and feel free to tag the maintainers listed there for the area(s) you are modifying. --> Co-authored-by: Matt Bovel <matthieu@bovel.net> [Cherry-picked 819d127]
Fixes #25417 fixes #25427
Regression introduced by tanishiking@816fc6c (#25111), which fixed #25091.
Background:
notypeat erasure.target.widenIfUnstableto remove method-reference from cast destination, and use it's return type instead.For example:
For
summonInline[MyEvidence[Scope.T]]:inlined.tpe:Scope.type{type T = String}#given_MyEvidence_Ttarget(before widening):(Scope.given_MyEvidence_T : => Scope.given_MyEvidence_T)resultType = target.widenIfUnstable:Scope.given_MyEvidence_TPreviously
inlined.tpe <:< resultTypehold, so adaptation is skipped.Scope.type{type T = String}#given_MyEvidence_TMyEvidence[String]MyEvidence[Scope.T]vsMyEvidence[String](path dependence infor is dropped).This commit fixes the problem by: decide whether adaptation is needed against original
target(the real call-site contract), while usingtarget.widenIfUnstableas a cast destination, so that #25091 fix)target.widenIfUnstableis an upper approximation oftarget. Conformance to widened type is weaker than conformance to originaltarget.Therefore adaptation-need must be checked against
target, while widened type is used only as cast destination.How much have your relied on LLM-based tools in this contribution?
Worked with GPT5.3-Codex
How was the solution tested?
testCompilation (i25091|i25417|i25427)Additional notes