Skip to content

Conversation

@lihaoyi
Copy link
Contributor

@lihaoyi lihaoyi commented Jan 14, 2026

Attempts to fix #24051, vibe coded

Root Cause

When an opaque type is accessed via an export, the returned type has a ThisType prefix from inside the opaque-defining scope. This ThisType prefix grants transparent access to the underlying alias even when the code accessing the type is outside the opaque scope.

Solution

The fix involves changes to 3 files:

  1. Types.scala - Main fix in TypeRef.superType (lines 3044-3068)
  • Added override of superType to check if we're accessing an opaque type from outside its defining scope
  • When the prefix is ThisType that sees opaques but ctx.owner is outside the opaque scope, return Any instead of the underlying alias
  • This prevents OpaqueType[String] from being treated as Unit in subtype checking
  1. Types.scala - Additional safeguards
  • reduceProjection: Don't dealias opaque types through this code path
  • computeInfo: Don't expose opaque aliases through lookupRefined
  1. Namer.scala - Export forwarder and type inference fixes
  • Use TypeRef(pathType, sym) instead of pathType.select(sym) for export forwarders
  • dealiasIfUnit: Don't dealias opaque types to Unit, preserving opacity in type inference

Test Case

A new test file tests/neg/i24051.scala was created that verifies:
package example {
package types {
opaque type OpaqueType[A] = Unit
object OpaqueType {
def apply[A]: OpaqueType[A] = ()
}
}
object exports {
export example.types.OpaqueType
}
import exports.*
def test[A](a: A)(using ev: A =:= Unit) = a
val proof = test(OpaqueType[String]) // error - correctly fails now
}

The fix ensures that OpaqueType[String] is NOT treated as equal to Unit outside the opaque scope, while still working correctly inside the scope.

@lihaoyi lihaoyi marked this pull request as draft January 14, 2026 23:48
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Exporting an opaque type causes it to be seen as its underlying type

1 participant