Skip to content

Inlines: decide sealing against original target, while using widened type as cast destination - #25448

Merged
mbovel merged 1 commit into
scala:mainfrom
tanishiking:i25427
Mar 9, 2026
Merged

Inlines: decide sealing against original target, while using widened type as cast destination#25448
mbovel merged 1 commit into
scala:mainfrom
tanishiking:i25427

Conversation

@tanishiking

@tanishiking tanishiking commented Mar 6, 2026

Copy link
Copy Markdown
Member

Fixes #25417 fixes #25427
Regression introduced by tanishiking@816fc6c (#25111), which fixed #25091.

Background:

For example:

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 remainsScope.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 #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.

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

@mbovel
mbovel self-requested a review March 6, 2026 09:51
@tanishiking tanishiking changed the title [WIP] Make sure we insert cast when adaptation is needed when inlining Inlines: decide sealing against original target, while using widened type as cast destination Mar 8, 2026
@tanishiking
tanishiking marked this pull request as ready for review March 8, 2026 03:58
@tanishiking

Copy link
Copy Markdown
Member Author

I'd request review from @mbovel (he already assigned himself though 😄 )

@mbovel mbovel left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the fix!

TLDR; I think it would be clearer to use <:< directly instead of going through ensureConforms.

Otherwise looks good to me!

Comment on lines +633 to +643
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)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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:

/** cast `tree` to `tp` (or its box/unbox/cast equivalent when after
* erasure and value and non-value types are mixed),
* unless tree's type already conforms to `tp`.
*/
def ensureConforms(tp: Type)(using Context): Tree =
if (tree.tpe <:< tp) tree
else if (!ctx.erasedTypes) cast(tp)
else Erasure.Boxing.adaptToType(tree, tp)

We could also flatten to a single if then ... else if ... else ....

Suggested change
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 😄

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
// 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>
@mbovel
mbovel merged commit 819d127 into scala:main Mar 9, 2026
64 checks passed
@tanishiking
tanishiking deleted the i25427 branch March 9, 2026 11:01
tgodzik pushed a commit to scala/scala3-lts that referenced this pull request Mar 24, 2026
â€Ķ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]
@WojciechMazur WojciechMazur added this to the 3.8.4 milestone Mar 31, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

4 participants