Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
2f83046
Allow `?` postfix operator
odersky Aug 13, 2026
28de1ca
Desugar to Magic types
odersky Aug 14, 2026
5aae5eb
Fixing a crash when recompiling
odersky Aug 15, 2026
229f39b
Allow magic only if -Yexplicit-nulls is also set
odersky Aug 15, 2026
d27f872
Handling maybe types
odersky Aug 15, 2026
4ea2a08
Move some of library and tests into bootstrapped directory
odersky Aug 15, 2026
09b3d04
Print maybe types with `?`
odersky Aug 15, 2026
a80e759
Treat magic as non-viral
odersky Aug 15, 2026
dedc258
Prepare for binary maybes
odersky Aug 16, 2026
3142634
Binary maybe
odersky Aug 17, 2026
4e32e18
Optimizations for maybe matching
odersky Aug 18, 2026
c3a6300
Fixes for handling orelse types
odersky Aug 18, 2026
95866f4
Adapt Space logic
odersky Aug 19, 2026
f5e85b2
Fix maybe widening in TypeComparer
odersky Aug 19, 2026
b612df6
Make Err constructor inline
odersky Aug 19, 2026
7cb61b6
Make T? a subtype of T | Null if T is not nullable
odersky Aug 20, 2026
bf1305c
Optimize redundant dual tests in pattern matcher
odersky Aug 20, 2026
c1fb280
Disable orelse test under scalajs
odersky Aug 20, 2026
040364b
Move Maybe class to magic.compiletime package
odersky Aug 20, 2026
1cf6b87
Allow widening also for binary orelse types
odersky Aug 21, 2026
334ace1
Direct style core infrastructure
odersky Aug 21, 2026
445a9f1
Drop mostly redundant tests
odersky Aug 21, 2026
a2a2384
Make newSyntax setting depend on language imports
odersky Aug 21, 2026
d740b53
Set source version to future under magic
odersky Aug 21, 2026
aa03e71
Error note if maybe widening fails because of nullability
odersky Aug 22, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion compiler/src/dotty/tools/dotc/ast/Desugar.scala
Original file line number Diff line number Diff line change
Expand Up @@ -2416,13 +2416,16 @@ object desugar {
// This is a deliberate departure from scalac, where StringContext is not rooted (See #4732)
Apply(Select(Apply(scalaDot(nme.StringContext), strs), id).withSpan(tree.span), elems)
case PostfixOp(t, op) =>
if (ctx.mode is Mode.Type) && !isBackquoted(op) && op.name == tpnme.raw.STAR then
def isOp(name: TypeName) = op.name == name && !isBackquoted(op)
if isOp(tpnme.raw.STAR) then
if ctx.isJava then
AppliedTypeTree(ref(defn.RepeatedParamType), t)
else
Annotated(
AppliedTypeTree(ref(defn.SeqType), t),
New(ref(defn.RepeatedAnnot.typeRef), Nil :: Nil))
else if isOp(tpnme.?) then
AppliedTypeTree(ref(defn.MagicMaybeClass.typeRef), t :: ref(defn.UnitClass.typeRef) :: Nil)
else
assert(ctx.mode.isExpr || ctx.reporter.errorsReported || ctx.mode.is(Mode.Interactive), ctx.mode)
Select(t, op.name)
Expand Down
11 changes: 11 additions & 0 deletions compiler/src/dotty/tools/dotc/ast/tpd.scala
Original file line number Diff line number Diff line change
Expand Up @@ -1094,6 +1094,13 @@ object tpd extends Trees.Instance[Type] with TypedTreeInfo {
receiver.select(defn.Object_ne).appliedTo(nullLit).withSpan(tree.span)
}

/** `null == tree` if cond, else `null != tree`
* Simpler than `testNotNull`. TODO: Can we replace testNotNull with this?
*/
def nullTest(cond: Boolean)(using Context) =
nullLiteral.select(if cond then defn.Any_== else defn.Any_!=)
.appliedTo(tree)

/** If inititializer tree is `_`, the default value of its type,
* otherwise the tree itself.
*/
Expand All @@ -1104,6 +1111,10 @@ object tpd extends Trees.Instance[Type] with TypedTreeInfo {
def and(that: Tree)(using Context): Tree =
tree.select(defn.Boolean_&&).appliedTo(that)

/** `!this`, for boolean tree `this` */
def not(using Context): Tree =
tree.select(defn.Boolean_!)

/** `this || that`, for boolean trees `this`, `that` */
def or(that: Tree)(using Context): Tree =
tree.select(defn.Boolean_||).appliedTo(that)
Expand Down
16 changes: 10 additions & 6 deletions compiler/src/dotty/tools/dotc/config/Feature.scala
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ object Feature:
val specializedTraits = experimental("specializedTraits")

val nonViralExperimentalFeatures: Set[TermName] =
Set(captureChecking, separationChecking, safe)
Set(captureChecking, separationChecking, safe, magic)

/** Experimental language imports that imply that the importing unit
* is experimental.
Expand Down Expand Up @@ -187,12 +187,12 @@ object Feature:

def quotedPatternsWithPolymorphicFunctionsEnabled(using Context) =
enabled(quotedPatternsWithPolymorphicFunctions)
def inlineTraitsEnabled(using Context) =

def inlineTraitsEnabled(using Context) =
enabledBySetting(inlineTraits)
|| enabledBySetting(specializedTraits)
|| ctx.compilationUnit.knowsInlineTraits

/** Is pureFunctions enabled for this compilation unit? */
def pureFunsEnabled(using Context) =
enabledBySetting(pureFunctions)
Expand Down Expand Up @@ -277,12 +277,15 @@ object Feature:
ccEnabledSomewhere && (defn.ccExperimental.contains(sym)
|| sym.exists && defn.ccExperimental.contains(sym.owner))

private def magicException(sym: Symbol)(using Context): Boolean =
Feature.magicEnabled && sym.isContainedIn(defn.MagicPackageClass)

def checkExperimentalDef(sym: Symbol, srcPos: SrcPos)(using Context) =
val experimentalSym =
if sym.hasAnnotation(defn.ExperimentalAnnot) then sym
else if sym.owner.hasAnnotation(defn.ExperimentalAnnot) then sym.owner
else NoSymbol
if !isExperimentalEnabled && !ccException(experimentalSym) then
if !isExperimentalEnabled && !ccException(experimentalSym) && !magicException(experimentalSym) then
val msg =
experimentalSym.getAnnotation(defn.ExperimentalAnnot).map {
case ExperimentalAnnotation(msg) if msg.nonEmpty => s": $msg"
Expand Down Expand Up @@ -345,9 +348,10 @@ object Feature:
true
case `magic` =>
ctx.compilationUnit.magic = true
ctx.compilationUnit.sourceVersion = Some(SourceVersion.future)
true
case `inlineTraits` =>
ctx.compilationUnit.knowsInlineTraits = true
ctx.compilationUnit.knowsInlineTraits = true
if ctx.run != null then ctx.run.nn.inlineTraitsImportEncountered = true
true
case _ =>
Expand Down
25 changes: 22 additions & 3 deletions compiler/src/dotty/tools/dotc/core/Definitions.scala
Original file line number Diff line number Diff line change
Expand Up @@ -252,6 +252,7 @@ class Definitions {
@tu lazy val SysPackage : Symbol = requiredModule("scala.sys.package")
@tu lazy val Sys_error: Symbol = SysPackage.moduleClass.requiredMethod(nme.error)


@tu lazy val ScalaXmlPackageClass: Symbol = getPackageClassIfDefined("scala.xml")

@tu lazy val CompiletimePackageClass: Symbol = requiredPackage("scala.compiletime").moduleClass
Expand All @@ -267,8 +268,6 @@ class Definitions {
@tu lazy val Compiletime_summonFrom : Symbol = CompiletimePackageClass.requiredMethod("summonFrom")
@tu lazy val Compiletime_summonInline : Symbol = CompiletimePackageClass.requiredMethod("summonInline")
@tu lazy val Compiletime_summonAll : Symbol = CompiletimePackageClass.requiredMethod("summonAll")
@tu lazy val Compiletime_spec : Symbol = CompiletimePackageClass.requiredMethod("$spec")
@tu lazy val Compiletime_wrappedType : Symbol = CompiletimePackageClass.requiredMethod("$wrappedType")
@tu lazy val CompiletimeTestingPackage: Symbol = requiredPackage("scala.compiletime.testing")
@tu lazy val CompiletimeTesting_typeChecks: Symbol = CompiletimeTestingPackage.requiredMethod("typeChecks")
@tu lazy val CompiletimeTesting_typeCheckErrors: Symbol = CompiletimeTestingPackage.requiredMethod("typeCheckErrors")
Expand Down Expand Up @@ -482,6 +481,23 @@ class Definitions {
}
def AnyKindType: TypeRef = AnyKindClass.typeRef

// Magic stuff
@tu lazy val MagicPackageClass: ClassSymbol = requiredPackage("scala.magic").moduleClass.asClass
@tu lazy val MagicMaybeClass: ClassSymbol = requiredClass("scala.magic.compiletime.Maybe")
@tu lazy val MagicValidClass: ClassSymbol = requiredClass("scala.magic.runtime.Valid")
@tu lazy val MagicFailClass: ClassSymbol = requiredClass("scala.magic.runtime.Fail")

@tu lazy val MagicOkModule: Symbol = requiredModule("scala.magic.Ok")
@tu lazy val Magic_OkUnapply: Symbol = MagicOkModule.requiredMethod(nme.unapply)

@tu lazy val MagicErrModule: Symbol = requiredModule("scala.magic.Err")
@tu lazy val Magic_ErrUnapply: Symbol = MagicErrModule.requiredMethod(nme.unapply)

@tu lazy val MagicCompiletimePackage: Symbol = requiredPackage("scala.magic.compiletime")
@tu lazy val Magic_spec: Symbol = MagicCompiletimePackage.requiredMethod("$spec")
@tu lazy val Magic_wrappedType: Symbol = MagicCompiletimePackage.requiredMethod("$wrappedType")

// More synthetic symbols
@tu lazy val andType: TypeSymbol = enterBinaryAlias(tpnme.AND, AndType(_, _))
@tu lazy val orType: TypeSymbol = enterBinaryAlias(tpnme.OR, OrType(_, _, soft = false))

Expand Down Expand Up @@ -1592,6 +1608,8 @@ class Definitions {
@tu lazy val erasedValueMethods =
capsErasedValueMethods + Compiletime_erasedValue

@tu lazy val unitSuperClasses: Set[Symbol] = Set(UnitClass, AnyValClass, AnyClass)

@tu lazy val AbstractFunctionType: Array[TypeRef] = mkArityArray("scala.runtime.AbstractFunction", MaxImplementedFunctionArity, 0).asInstanceOf[Array[TypeRef]]
val AbstractFunctionClassPerRun: PerRun[Array[Symbol]] = new PerRun(AbstractFunctionType.map(_.symbol.asClass))
def AbstractFunctionClass(n: Int)(using Context): Symbol = AbstractFunctionClassPerRun()(using ctx)(n)
Expand Down Expand Up @@ -1770,7 +1788,7 @@ class Definitions {
private val PredefImportFns: RootRef =
RootRef(() => ScalaPredefModule.termRef)

// The new Specialized lives in scala.specialize.
// The new Specialized lives in scala.specialize.
// This is to avoid conflict with the Scala2 specialized annotation.
// It is not imported by default with the scala package, so we additionally import it here.
private val SpecializeImportFns: RootRef =
Expand Down Expand Up @@ -2217,6 +2235,7 @@ class Definitions {
m(TupleClass) = ProductClass
m(NonEmptyTupleClass) = ProductClass
m(PairClass) = ObjectClass
m(MagicMaybeClass) = ObjectClass
m

// ----- Initialization ---------------------------------------------------
Expand Down
18 changes: 11 additions & 7 deletions compiler/src/dotty/tools/dotc/core/SymDenotations.scala
Original file line number Diff line number Diff line change
Expand Up @@ -635,7 +635,8 @@ object SymDenotations {
case myInfo: ModuleCompleter =>
// Instead of completing the ModuleCompleter, we can check whether
// the module class is absent, which might require less completions.
myInfo.moduleClass.isAbsent(canForce)
val mcls: Symbol = myInfo.moduleClass
!mcls.exists || mcls.isAbsent(canForce)
case _: SymbolLoader if canForce =>
// Completing a SymbolLoader might call `markAbsent()`
completeOnce()
Expand Down Expand Up @@ -675,9 +676,9 @@ object SymDenotations {
final def isSpecializedTraitImplementationClass(using Context): Boolean =
isClass && name.isSpecializedTraitImplementationName

/** Is this symbol a specialized trait implementation class that
/** Is this symbol a specialized trait implementation class that
* was generated from a specialization using only top classes / Nothing
* and is therefore not subject to a specialized interface */
* and is therefore not subject to a specialized interface */
final def isRawSpecializedTraitImplementationClass(using Context): Boolean =
isClass && name.isSpecializedTraitImplementationName

Expand Down Expand Up @@ -933,7 +934,10 @@ object SymDenotations {
/** Is this symbol a class of which `null` is a value? */
final def isNullableClass(using Context): Boolean =
if ctx.mode.is(Mode.SafeNulls) && !ctx.phase.erasedTypes
then symbol == defn.NullClass || symbol == defn.AnyClass || symbol == defn.AnyValClass || symbol == defn.MatchableClass
then symbol == defn.NullClass
|| symbol == defn.AnyClass
|| symbol == defn.AnyValClass
|| symbol == defn.MatchableClass
else isNullableClassAfterErasure

/** Is this symbol a class of which `null` is a value after erasure?
Expand Down Expand Up @@ -1086,11 +1090,11 @@ object SymDenotations {

def isInlineTrait(using Context): Boolean =
isAllOf(InlineTrait)
def isSpecializedMethod(using Context): Boolean =

def isSpecializedMethod(using Context): Boolean =
Specialization.isSpecializedMethod(symbol)

def isSpecializedTrait(using Context): Boolean =
def isSpecializedTrait(using Context): Boolean =
Specialization.isSpecializedTrait(symbol)

/** Does this method or field need to be retained at runtime */
Expand Down
47 changes: 45 additions & 2 deletions compiler/src/dotty/tools/dotc/core/TypeComparer.scala
Original file line number Diff line number Diff line change
Expand Up @@ -854,6 +854,16 @@ class TypeComparer(@constructorOnly initctx: Context) extends ConstraintHandling
return recur(tp1, OrType(tp21, tp221, tp2.isSoft)) && recur(tp1, OrType(tp21, tp222, tp2.isSoft))
case _ =>
}
tp2 match
case OrNull(tp2a) =>
tp1w match
case MagicMaybeType(tp1a, errArg, _) =>
if errArg.isRef(defn.UnitClass) then
if tp1a.isNotNullNorMaybe then
return recur(tp1a, tp2a)
else nullableNote(tp1a, tp2a)
case _ =>
case _ =>
either(recur(tp1, tp21), recur(tp1, tp22)) || fourthTry
case tp2: MatchType =>
val reduced = tp2.reduced
Expand Down Expand Up @@ -1042,6 +1052,8 @@ class TypeComparer(@constructorOnly initctx: Context) extends ConstraintHandling
// Same as above; this.type is also a singleton type in spec language
!ctx.explicitNulls && isNullable(tp.underlying)
case tp: RefinedOrRecType => isNullable(tp.parent)
case AppliedType(tycon, _ :: errArg :: Nil) if tycon.isRef(defn.MagicMaybeClass) =>
isSubType(defn.UnitType, errArg)
case tp: AppliedType => isNullable(tp.tycon)
case AndType(tp1, tp2) => isNullable(tp1) && isNullable(tp2)
case OrType(tp1, tp2) => isNullable(tp1) || isNullable(tp2)
Expand Down Expand Up @@ -1506,6 +1518,15 @@ class TypeComparer(@constructorOnly initctx: Context) extends ConstraintHandling
case _ => false
} && recordGadtUsageIf(true)

/** T <: T? if T is not null */
def byMaybeWidening: Boolean = tp2 match
case MagicMaybeType(res2, err2, _) =>
if tp1.isNotNullNorMaybe then recur(tp1, res2)
else
nullableNote(tp1, res2)
false
case _ => false

tycon2 match {
case param2: TypeParamRef =>
isMatchingApply(tp1) ||
Expand All @@ -1514,6 +1535,7 @@ class TypeComparer(@constructorOnly initctx: Context) extends ConstraintHandling
case tycon2: TypeRef =>
isMatchingApply(tp1)
|| byGadtBounds
|| byMaybeWidening
|| defn.isCompiletimeAppliedType(tycon2.symbol)
&& compareCompiletimeAppliedType(tp2, tp1, fromBelow = true)
|| tycon2.info.match
Expand Down Expand Up @@ -1657,6 +1679,10 @@ class TypeComparer(@constructorOnly initctx: Context) extends ConstraintHandling
&& tp1.derivesFromCapSet
&& tp2.derivesFromCapSet

def nullableNote(test1: Type, test2: Type) =
if isSubTypeWhenFrozen(test1, test2) then
addErrorNote(NoGenericWideningNote(tp1, tp2, test1))

// begin recur
if tp2 eq NoType then false
else if tp1 eq tp2 then true
Expand Down Expand Up @@ -1974,7 +2000,7 @@ class TypeComparer(@constructorOnly initctx: Context) extends ConstraintHandling
&& defn.isByNameFunction(arg2.dealias) =>
isSubArg(arg1res, arg2.argInfos.head)
case _ =>
if v < 0 then
if v < 0 then
val isValidSubtype = isSubType(arg2, arg1)
// Specialized traits have special variance rules because they have special erasure
if tp1.classSymbol.isSpecializedTrait
Expand All @@ -1986,7 +2012,7 @@ class TypeComparer(@constructorOnly initctx: Context) extends ConstraintHandling
false
else // Normal contravariance case
isValidSubtype
else if v > 0 then
else if v > 0 then
val isValidSubtype = isSubType(arg1, arg2)
// Specialized traits have special variance rules because they have special erasure
if tp1.classSymbol.isSpecializedTrait
Expand Down Expand Up @@ -3690,6 +3716,23 @@ object TypeComparer {
currentComparer.isolated(op, x => x)
}

class NoGenericWideningNote(tp1: Type, tp2: Type, val nullableTp: Type) extends Note {

def render(using Context) =
def workaround = tp2 match
case OrNull(_) => "pattern match"
case _ => "wrapper"
i"""
|
|Note that $tp1 cannot be widened to $tp2 since $nullableTp might contain null or a `?` instance.
|An explicit `Ok(...)` $workaround is needed."""

override def covers(other: Note)(using Context) = other match
case other: NoGenericWideningNote =>
other.nullableTp frozen_<:< this.nullableTp
case _ => false
}

object MatchReducer:
import printing.*, Texts.{*, given}
enum MatchResult extends Showable:
Expand Down
Loading
Loading