diff --git a/compiler/src/dotty/tools/dotc/ast/Desugar.scala b/compiler/src/dotty/tools/dotc/ast/Desugar.scala index 0145630276fc..6cb3e2c6136f 100644 --- a/compiler/src/dotty/tools/dotc/ast/Desugar.scala +++ b/compiler/src/dotty/tools/dotc/ast/Desugar.scala @@ -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) diff --git a/compiler/src/dotty/tools/dotc/ast/tpd.scala b/compiler/src/dotty/tools/dotc/ast/tpd.scala index f5f3641b74c4..596e244b4a07 100644 --- a/compiler/src/dotty/tools/dotc/ast/tpd.scala +++ b/compiler/src/dotty/tools/dotc/ast/tpd.scala @@ -98,6 +98,12 @@ object tpd extends Trees.Instance[Type] with TypedTreeInfo { def If(cond: Tree, thenp: Tree, elsep: Tree)(using Context): If = ta.assignType(untpd.If(cond, thenp, elsep), thenp, elsep) + def conditional(cond: Tree, thenp: Tree, elsep: Tree)(using Context): Tree = + cond match + case Literal(Constant(true)) => thenp + case Literal(Constant(false)) => elsep + case _ => If(cond, thenp, elsep) + def InlineIf(cond: Tree, thenp: Tree, elsep: Tree)(using Context): If = ta.assignType(untpd.InlineIf(cond, thenp, elsep), thenp, elsep) @@ -1094,6 +1100,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. */ @@ -1104,6 +1117,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) diff --git a/compiler/src/dotty/tools/dotc/config/Feature.scala b/compiler/src/dotty/tools/dotc/config/Feature.scala index f5e0502e1689..a3bfa7ea5602 100644 --- a/compiler/src/dotty/tools/dotc/config/Feature.scala +++ b/compiler/src/dotty/tools/dotc/config/Feature.scala @@ -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. @@ -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) @@ -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" @@ -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 _ => diff --git a/compiler/src/dotty/tools/dotc/core/Definitions.scala b/compiler/src/dotty/tools/dotc/core/Definitions.scala index d0b461681a95..469d5ad95c65 100644 --- a/compiler/src/dotty/tools/dotc/core/Definitions.scala +++ b/compiler/src/dotty/tools/dotc/core/Definitions.scala @@ -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 @@ -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") @@ -482,6 +481,30 @@ class Definitions { } def AnyKindType: TypeRef = AnyKindClass.typeRef + // Magic stuff + @tu lazy val MagicPackage: Symbol = requiredPackage("scala.magic") + @tu lazy val MagicPackageClass: ClassSymbol = MagicPackage.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 Magic_provided1: Symbol = MagicPackage.info.member(termName("provided")).suchThat(_.info.isInstanceOf[MethodType]).symbol + @tu lazy val Magic_provided2: Symbol = MagicPackage.info.member(termName("provided")).suchThat(_.info.isInstanceOf[PolyType]).symbol + @tu lazy val Magic_CanErr: Symbol = MagicPackageClass.requiredType("CanErr") + + @tu lazy val MagicOkModule: Symbol = requiredModule("scala.magic.Ok") + @tu lazy val Magic_OkApply: Symbol = MagicOkModule.requiredMethod(nme.apply) + @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") + + @tu lazy val MagicRuntimePackageClass = requiredPackage("scala.magic.runtime").moduleClass.asClass + + // More synthetic symbols @tu lazy val andType: TypeSymbol = enterBinaryAlias(tpnme.AND, AndType(_, _)) @tu lazy val orType: TypeSymbol = enterBinaryAlias(tpnme.OR, OrType(_, _, soft = false)) @@ -1592,6 +1615,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) @@ -1768,7 +1793,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 = @@ -2215,6 +2240,7 @@ class Definitions { m(TupleClass) = ProductClass m(NonEmptyTupleClass) = ProductClass m(PairClass) = ObjectClass + m(MagicMaybeClass) = ObjectClass m // ----- Initialization --------------------------------------------------- diff --git a/compiler/src/dotty/tools/dotc/core/SymDenotations.scala b/compiler/src/dotty/tools/dotc/core/SymDenotations.scala index 177fbe87ef43..55f12a55d005 100644 --- a/compiler/src/dotty/tools/dotc/core/SymDenotations.scala +++ b/compiler/src/dotty/tools/dotc/core/SymDenotations.scala @@ -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() @@ -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 @@ -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? @@ -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 */ diff --git a/compiler/src/dotty/tools/dotc/core/TypeComparer.scala b/compiler/src/dotty/tools/dotc/core/TypeComparer.scala index 1e00179c4e5f..5a50c1a05d4a 100644 --- a/compiler/src/dotty/tools/dotc/core/TypeComparer.scala +++ b/compiler/src/dotty/tools/dotc/core/TypeComparer.scala @@ -854,6 +854,14 @@ 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) && tp1a.isNotNullNorMaybe then + return recur(tp1a, tp2a) + case _ => + case _ => either(recur(tp1, tp21), recur(tp1, tp22)) || fourthTry case tp2: MatchType => val reduced = tp2.reduced @@ -1042,6 +1050,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) @@ -1506,6 +1516,12 @@ 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 => + recur(tp1, res2) + case _ => false + tycon2 match { case param2: TypeParamRef => isMatchingApply(tp1) || @@ -1514,6 +1530,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 @@ -1974,7 +1991,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 @@ -1986,7 +2003,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 diff --git a/compiler/src/dotty/tools/dotc/core/TypeErasure.scala b/compiler/src/dotty/tools/dotc/core/TypeErasure.scala index bbb29b5dc862..c7bb89aa263b 100644 --- a/compiler/src/dotty/tools/dotc/core/TypeErasure.scala +++ b/compiler/src/dotty/tools/dotc/core/TypeErasure.scala @@ -79,10 +79,14 @@ end SourceLanguage */ object TypeErasure: - private val DisallowSpecialized = Property.Key[Unit] + private val DisallowSpecialized = Property.Key[Unit] private def erasureDependsOnArgs(sym: Symbol)(using Context) = - sym == defn.ArrayClass || sym == defn.PairClass || sym.isDerivedValueClass || sym.isSpecializedTrait + sym == defn.ArrayClass + || sym == defn.PairClass + || sym == defn.MagicMaybeClass + || sym.isDerivedValueClass + || sym.isSpecializedTrait /** The arity of this tuple type, which can be made up of EmptyTuple, TupleX and `*:` pairs. * @@ -214,7 +218,7 @@ object TypeErasure: /** The current context but with Foo[Int] erasing to Foo instead of * Foo$sp$Int when Foo is a specialized trait. */ def disallowSpecializedCtx(using Context) = ctx.fresh.setProperty(DisallowSpecialized, ()) - + /** The current context but with Foo[Int] erasing to Foo$sp$Int instead of * Foo when Foo is a specialized trait. */ def allowSpecializedCtx(using Context) = ctx.fresh.dropProperty(DisallowSpecialized) @@ -789,12 +793,12 @@ class TypeErasure(sourceLanguage: SourceLanguage, semiEraseVCs: Boolean, isConst else if semiEraseVCs && sym.isDerivedValueClass then eraseDerivedValueClass(tp) else if defn.isSyntheticFunctionClass(sym) then defn.functionTypeErasure(sym) else eraseNormalClassRef(tp) - case Specialization(spec) if ((ctx.phase == erasurePhase || ctx.erasedTypes) // At the beginning the $sp$ trait symbols are not present so up until + case Specialization(spec) if ((ctx.phase == erasurePhase || ctx.erasedTypes) // At the beginning the $sp$ trait symbols are not present so up until // erasure need to consider the signature of def foo(x: Foo[Int]): Int as // foo(Foo):Int. Only at erasure do the symbols swap. This ensures // the signatures don't change before erasure which is required (meta-ordering // constraint in Compiler.scala) - && spec.isSpecialized && ctx.property(DisallowSpecialized).isEmpty) => + && spec.isSpecialized && ctx.property(DisallowSpecialized).isEmpty) => val specName = spec.newSpecializedTraitName val interfaceSymbol = spec.symbol.owner.enclosingPackageClass.info.decls.lookup(specName) assert(interfaceSymbol.exists && interfaceSymbol.isClass) @@ -805,6 +809,7 @@ class TypeErasure(sourceLanguage: SourceLanguage, semiEraseVCs: Boolean, isConst else if (tycon.isRef(defn.PairClass)) erasePair(tp) else if (tp.isRepeatedParam) apply(tp.translateFromRepeated(toArray = sourceLanguage.isJava)) else if (semiEraseVCs && tycon.classSymbol.isDerivedValueClass) eraseDerivedValueClass(tp) + else if tycon.isRef(defn.MagicMaybeClass) then eraseMaybe(tp) else this(checkedSuperType(tp)) case tp: TermRef => this(underlyingOfTermRef(tp)) @@ -897,7 +902,7 @@ class TypeErasure(sourceLanguage: SourceLanguage, semiEraseVCs: Boolean, isConst if ((cls eq defn.ObjectClass) || cls.isPrimitiveValueClass) Nil else // Match corresponding tree erasure in Erasure::typedClassDef - val parents1 = + val parents1 = if cls.isSpecializedTraitInterface then // {source: inline trait Bar[T: Specialized] extends Foo[T] both specialized traits} inline trait Bar$sp$Int extends Object, Bar, Foo$sp$Int val (obj :: originalTrait :: inheritedParents) = parents : @unchecked eraseParent(obj) :: apply(originalTrait)(using disallowSpecializedCtx) :: inheritedParents.mapConserve(eraseParent(_)(using allowSpecializedCtx)) @@ -909,7 +914,7 @@ class TypeErasure(sourceLanguage: SourceLanguage, semiEraseVCs: Boolean, isConst // {source: class Bar extends Foo[Int](10) with Baz[Int](10)} // class Bar extends Object, Foo(10), Baz(10), Foo$sp$Int, Baz$sp$Int - parents.mapConserve(p => if p.typeSymbol.isSpecializedTrait then + parents.mapConserve(p => if p.typeSymbol.isSpecializedTrait then apply(p)(using disallowSpecializedCtx) else eraseParent(p)) ::: originalSpecializedTraits parents1 match { @@ -990,6 +995,18 @@ class TypeErasure(sourceLanguage: SourceLanguage, semiEraseVCs: Boolean, isConst else defn.TupleXXLClass.typeRef } + /** The erasure of `T ? E`. A value of that type is represented at runtime either + * as a value of type `T`, or, if `E` is `Unit`, as `null`, or otherwise as a + * `runtime.Fail` wrapping the error. So we can erase to the erasure of `T` only + * if `T` cannot be `null` and no `Fail` can arise, i.e. `E` is `Unit`. + */ + private def eraseMaybe(tp: AppliedType)(using Context): Type = + val arg :: errArg :: Nil = tp.args: @unchecked + if arg.isNotNull && arg.derivesFrom(defn.ObjectClass) + && (errArg.isRef(defn.UnitClass) || errArg.isRef(defn.NothingClass)) + then apply(arg) + else defn.ObjectType + /** The erasure of a symbol's info. This is different from `apply` in the way `ExprType`s and * `PolyType`s are treated. `eraseInfo` maps them to method types, whereas `apply` maps them * to the underlying type. diff --git a/compiler/src/dotty/tools/dotc/core/Types.scala b/compiler/src/dotty/tools/dotc/core/Types.scala index f68267dac74c..3b8f885c2276 100644 --- a/compiler/src/dotty/tools/dotc/core/Types.scala +++ b/compiler/src/dotty/tools/dotc/core/Types.scala @@ -381,17 +381,22 @@ object Types extends TypeUtils { } /** Is this type guaranteed not to have `null` as a value? */ - final def isNotNull(using Context): Boolean = this match { + final def isNotNull(norMaybe: Boolean)(using Context): Boolean = this match case tp: ConstantType => tp.value.value != null case tp: FlexibleType => false case tp: ClassInfo => !tp.cls.isNullableClass && !tp.isNothingType - case tp: AppliedType => tp.superType.isNotNull - case tp: TypeBounds => tp.hi.isNotNull - case tp: TypeProxy => tp.underlying.isNotNull - case AndType(tp1, tp2) => tp1.isNotNull || tp2.isNotNull - case OrType(tp1, tp2) => tp1.isNotNull && tp2.isNotNull + case MagicMaybeType(_, _, nullable) => !norMaybe && !nullable + case tp: TypeBounds => tp.hi.isNotNull(norMaybe) + case tp: TypeProxy => tp.underlying.isNotNull(norMaybe) + case AndType(tp1, tp2) => tp1.isNotNull(norMaybe) || tp2.isNotNull(norMaybe) + case OrType(tp1, tp2) => tp1.isNotNull(norMaybe) && tp2.isNotNull(norMaybe) case _ => false - } + + /** Is this type guaranteed not to have `null` as a value? */ + final def isNotNull(using Context): Boolean = isNotNull(norMaybe = false) + + /** Is this type guaranteed not to have `null` or `Fail(...)` as a value? */ + final def isNotNullNorMaybe(using Context): Boolean = isNotNull(norMaybe = true) /** Is `null` a value of this type? */ def admitsNull(using Context): Boolean = @@ -5790,6 +5795,19 @@ object Types extends TypeUtils { def unapply(tp: MatchAlias): Option[Type] = Some(tp.alias) } + object MagicMaybeType { + /** The maybe type `resTp ? errTp` */ + def apply(resTp: Type, errTp: Type)(using Context) = + defn.MagicMaybeClass.typeRef.appliedTo(resTp, errTp) + + /** Matches types T ? E, returns (T, E, E >: Unit) */ + def unapply(tp: Type)(using Context): Option[(Type, Type, Boolean)] = tp.dealias match + case AppliedType(tycon, resArg :: errArg :: Nil) if tycon.isRef(defn.MagicMaybeClass) => + Some((resArg, errArg, defn.unitSuperClasses.contains(errArg.classSymbol))) + case _ => + None + } + // ----- Annotated and Import types ----------------------------------------------- /** An annotated type tpe @ annot */ diff --git a/compiler/src/dotty/tools/dotc/inlines/Inliner.scala b/compiler/src/dotty/tools/dotc/inlines/Inliner.scala index 008254c01432..e2011db1a1b9 100644 --- a/compiler/src/dotty/tools/dotc/inlines/Inliner.scala +++ b/compiler/src/dotty/tools/dotc/inlines/Inliner.scala @@ -15,6 +15,7 @@ import config.Printers.inlining import ErrorReporting.errorTree import util.{SimpleIdentitySet, SrcPos} import Nullables.computeNullableDeeply +import config.Printers.transforms import collection.mutable import reporting.trace @@ -210,6 +211,33 @@ object Inliner: else constToLiteral(rootTree) + /** If tree is an equality test == with known outcome and no side effects, replace it + * by a constant true or false. + * Known outcome means currently: + * - arguments are both constants, or + * - at least one argument is of Unit type + */ + def reduceEQ(tree: Tree)(using Context): Tree = tree match + case Apply(sel @ Select(arg1, nme.EQ), arg2 :: Nil) if isPureExpr(arg1) && isPureExpr(arg2) => + val tp1 = arg1.tpe.widen + val tp2 = arg2.tpe.widen + def const(b: Boolean) = + cpy.Literal(tree)(Constant(b)) + .showing(i"REDUCE $tree to $result in ${ctx.compilationUnit} in ${ctx.owner.ownersIterator.toList}/${arg1.tpe},${arg2.tpe}", transforms) + def reduceUnit(tp1: Type, tp2: Type) = + if tp1.isRef(defn.UnitClass) then + if tp2.isRef(defn.UnitClass) then const(true) + else if !tp2.isBottomType && !tp2.isTopType then const(false) + else EmptyTree + else EmptyTree + (tp1, tp2) match + case (ConstantType(c1), ConstantType(c2)) => + if c1 == c2 then const(true) else const(false) + case _ => + reduceUnit(tp1, tp2).orElse(reduceUnit(tp2, tp1)).orElse(tree) + case _ => + tree + private[inlines] def newSym(name: Name, flags: FlagSet, info: Type, span: Span)(using Context): Symbol = newSymbol(ctx.owner, name, flags, info, coord = span) end Inliner @@ -806,7 +834,7 @@ class Inliner(val call: tpd.Tree)(using Context): // corresponding arguments or proxies on the type and term level. It also changes // the owner from the inlined method to the current owner. - // This is reused through InlineTraitAncestors for inline traits, so inlinedMethod might not exist there + // This is reused through InlineTraitAncestors for inline traits, so inlinedMethod might not exist there val oldOwners = if (inlinedMethod.exists) then inlinedMethod :: Nil else Nil val newOwners = if (inlinedMethod.exists) then ctx.owner :: Nil else Nil diff --git a/compiler/src/dotty/tools/dotc/inlines/Inlines.scala b/compiler/src/dotty/tools/dotc/inlines/Inlines.scala index 7007ccb4893f..87a5e2a640d1 100644 --- a/compiler/src/dotty/tools/dotc/inlines/Inlines.scala +++ b/compiler/src/dotty/tools/dotc/inlines/Inlines.scala @@ -129,15 +129,15 @@ object Inlines: private def inlineTraitAncestors(cls: TypeDef)(using Context): List[Tree] = cls match { case tpd.TypeDef(_, tmpl: Template) => val parentTrees: Map[Symbol, Tree] = tmpl.parents.map(par => symbolFromParent(par) -> par).toMap.filter(_._1.isInlineTrait) - + // TODO: We need to stop inlining if there is a non-inline trait or class that sits between the inline trait and the current class. - // Because we also inline into other inline traits, it should be possible to do this by just + // Because we also inline into other inline traits, it should be possible to do this by just // looking at the direct parents of the class instead of also needing to look at the indirect parents (baseClasses). // See inline-trait-non-inline-blocks-inlining.scala val ancestors: List[ClassSymbol] = cls.tpe.baseClasses.filter(sym => sym != cls.symbol && sym.isInlineTrait && !(cls.symbol.asClass.ownersIterator.toList.tail.exists(p => p.isInlineTrait)) // We can skip anything that would be inlined into a class that lives somewhere inside an inline trait - // because it must be on the RHS of a member definition in the inline trait and so pruned out later + // because it must be on the RHS of a member definition in the inline trait and so pruned out later ) ancestors.flatMap(ancestor => @@ -151,7 +151,7 @@ object Inlines: report.error(s"unknown base type ${baseTpe.show} for ancestor ${ancestor.show} of ${cls.symbol.show}") None parentTrees.get(ancestor).orElse(baseTree.map(_.withSpan(cls.span))) - ).flatMap { tree => + ).flatMap { tree => tree.tpe match { case Specialization(spec) if spec.hasSpecializedParams && !spec.isFullySpecialized => None // these can only exist in cases where we don't want to inline because: // 1) they will be pruned out later anyway and if we inline them we will create a loop (as in tests/pos/specialized-trait-inlining-causes-implementation-required-loop-bad.scala) @@ -285,7 +285,7 @@ object Inlines: tree3 end inlineCall - private def updateFlagsFromInlinedParent(child: FlagSet, parent: FlagSet): FlagSet = + private def updateFlagsFromInlinedParent(child: FlagSet, parent: FlagSet): FlagSet = var updatedFlags = child // Parent needs to be initialised so child must also as initialisers have been inlined if (!parent.is(NoInits)) @@ -296,8 +296,8 @@ object Inlines: updatedFlags &~= PureInterface updatedFlags - private def checkInnerClasses(tmpl: Template)(using Context) = - tmpl.body.foreach { + private def checkInnerClasses(tmpl: Template)(using Context) = + tmpl.body.foreach { // If we want to add these back, some work was done on this in the original Master Thesis // (https://infoscience.epfl.ch/server/api/core/bitstreams/9413f583-46bc-4106-b994-0be32f20eeba/content) case innerClass: TypeDef if innerClass.symbol.isClass => report.error("Inline traits may not define inner classes or traits.", innerClass.srcPos) @@ -319,13 +319,13 @@ object Inlines: end checkAndTransformInlineTrait - private def checkInlineTraitOverrides(clsSym: ClassSymbol)(using Context) = - // We need to enforce `override` modifier constraints to ensure that the behaviour is the same as ordinary traits. + private def checkInlineTraitOverrides(clsSym: ClassSymbol)(using Context) = + // We need to enforce `override` modifier constraints to ensure that the behaviour is the same as ordinary traits. // The usual checks only apply in refChecks which is too late for us. def checkInlineTraitOverride(member: Symbol, other: Symbol) = if !member.is(Override) && !other.is(Deferred) && member.owner == clsSym then report.error( - OverrideError("needs `override` modifier", + OverrideError("needs `override` modifier", other.info, member, other, @@ -335,11 +335,11 @@ object Inlines: ) else if member.owner != clsSym && other.owner != clsSym && !other.owner.derivesFrom(member.owner) - && !(member.isAnyOverride || member.hasAnnotation(defn.UncheckedOverrideAnnot)) - && (!other.is(Deferred) || other.isAllOf(Given | HasDefault)) - && !member.is(Deferred) + && !(member.isAnyOverride || member.hasAnnotation(defn.UncheckedOverrideAnnot)) + && (!other.is(Deferred) || other.isAllOf(Given | HasDefault)) + && !member.is(Deferred) && !other.name.is(DefaultGetterName) then - + report.error( OverrideError( s"${clsSym} inherits conflicting members:\n " @@ -354,7 +354,7 @@ object Inlines: , clsSym.srcPos ) - OverridingPairsChecker(clsSym, clsSym.thisType).checkAll(checkInlineTraitOverride) + OverridingPairsChecker(clsSym, clsSym.thisType).checkAll(checkInlineTraitOverride) def inlineParentInlineTraits(cls: Tree)(using Context): Tree = cls match { @@ -366,31 +366,31 @@ object Inlines: if cls.symbol.isAnonymousClass && ancestors.exists(tree => Specialization.unapply(tree.tpe).exists(anc => anc.isSpecialized || anc.isFullySpecializedToTopClassesOrNothing)) then // No need to inline into specialized trait anonymous class instances; these will later be replaced by $impl$ classes. return cls - + val cycleFound = ancestors.exists { parent => val parentSym = symbolFromParent(parent) - val errorPos = + val errorPos = // Trying to inline into the tree which defines parentSym (need to catch this separately - // as need to catch it before we inline the second time to avoid tripping an assertion) + // as need to catch it before we inline the second time to avoid tripping an assertion) if cls.symbol.ownersIterator.contains(parentSym) then - Some(cls.srcPos) - else if ctx.inlineTraitState.inlineOrigins(cls.symbol).contains(parentSym) then + Some(cls.srcPos) + else if ctx.inlineTraitState.inlineOrigins(cls.symbol).contains(parentSym) then // Select the user code that caused this error so we get two errors if there are two problematic inlines, not one - val userPos = tpd.enclosingInlineds.last.srcPos + val userPos = tpd.enclosingInlineds.last.srcPos // Trying to inline into the inlined body of parentSym not in the defn tree - Some(userPos) + Some(userPos) else None // Fine - + errorPos.foreach(pos => report.error(s"Inlining of inline traits looped. Tried to inline ${parentSym} into its own body.", pos) ) - + errorPos.nonEmpty } - if cycleFound then + if cycleFound then return cls - + val newDefs = inContext(ctx.withOwner(cls.symbol)) { ancestors.foldLeft((List.empty[Tree], impl.body)) { case ((inlineDefs, childDefs), parent) => @@ -399,10 +399,10 @@ object Inlines: val overriddenSymbols = clsOverriddenSyms ++ inlineDefs.flatMap(_.symbol.allOverriddenSymbols) // Need to put the new defs first because we process in linearization order to make overridees correct, // but we want parent definitions to come first so that if child inline traits refer to values defined in a parent - // inline trait these are defined. - val inlinedDefs1 = parentTraitInliner.expandDefs(overriddenSymbols) ::: inlineDefs + // inline trait these are defined. + val inlinedDefs1 = parentTraitInliner.expandDefs(overriddenSymbols) ::: inlineDefs cls.symbol.flags = updateFlagsFromInlinedParent(cls.symbol.flags, parent.symbol.flags) - + val childDefs1 = parentTraitInliner.adaptSuperCalls(childDefs) (parentTraitInliner.adaptSuperCalls(inlinedDefs1), childDefs1) } @@ -410,11 +410,11 @@ object Inlines: val newbody = newDefs._1 ::: newDefs._2 val paramAccessors = newbody.filter(_.symbol.is(ParamAccessor)) - + for pacc <- paramAccessors otherstat <- newbody if !otherstat.symbol.is(ParamAccessor) && otherstat.denot.matches(pacc.denot.asSingleDenotation) - do report.error(s"Inlining of inline trait created name conflict on ${pacc.denot.name}. Constructor parameters of inline receivers may not collide with members of inline traits.", pacc.srcPos) - + do report.error(s"Inlining of inline trait created name conflict on ${pacc.denot.name}. Constructor parameters of inline receivers may not collide with members of inline traits.", pacc.srcPos) + val impl1 = cpy.Template(impl)(body = newbody) cpy.TypeDef(cls)(rhs = impl1) @@ -720,7 +720,7 @@ object Inlines: /** The Inlined node representing the inlined call */ def expand(rhsToInline: Tree): Tree = - // Special handling of `requireConst` and `codeOf` + // Special handling of `requireConst`, `codeOf`, and `magic.Ok` callValueArgss match case (arg :: Nil) :: Nil => if inlinedMethod == defn.Compiletime_requireConst then @@ -730,6 +730,8 @@ object Inlines: return unitLiteral.withSpan(call.span) else if inlinedMethod == defn.Compiletime_codeOf then return Intrinsics.codeOf(arg, call.srcPos) + else if inlinedMethod == defn.Magic_OkApply && arg.tpe.isNotNullNorMaybe then + return arg case _ => // Special handling of `constValue[T]`, `constValueOpt[T]`, `constValueTuple[T]`, `summonInline[T]` and `summonAll[T]` @@ -917,11 +919,11 @@ object Inlines: } end expandDefs - def adaptSuperCalls(defs: List[Tree]) = + def adaptSuperCalls(defs: List[Tree]) = val ttmap = TreeTypeMap(treeMap = { // We go through all ancestor inline traits so eventually we will find the one with matching parentSym case sel@Select(Super(qual, mix), name) if sel.symbol.owner == parentSym => - // At that point either the method is overridden so needs mangling (and we just copied and mangled it in this inlining phase), + // At that point either the method is overridden so needs mangling (and we just copied and mangled it in this inlining phase), // or not, in which case call directly by original name. In both cases we are calling the method resulting from inlining, on the // inline receiver class. Select(This(ctx.owner.asClass), paramAccessorsMapper.getParamAccessorName(sel.symbol.owner, name).getOrElse(name)) @@ -977,11 +979,11 @@ object Inlines: } override protected val inlinerTypeMap: InlinerTypeMap = InlineTraitTypeMap() - + override protected val inlinerTreeMap: InlinerTreeMap = InlineTraitTreeMap() override protected def computeThisBindings(): Unit = () - + override protected def canElideThis(tpe: ThisType): Boolean = true override protected def inlineCtx(inlineTyper: InlineTyper)(using Context): Context = @@ -1024,7 +1026,7 @@ object Inlines: paramAccessorsMapper .getParamAccessorRhs(vdef.symbol.owner, vdef.symbol.name) .getOrElse(inlinedRhs(vdef, inlinedSym)) - + val rhs1 = rhs.changeNonLocalOwners(inlinedSym) tpd.ValDef(inlinedSym.asTerm, rhs1).withSpan(parent.span) @@ -1049,7 +1051,7 @@ object Inlines: ctx.typeAssigner.assignType(untpd.TypeDef(inlinedSym.name.asTypeName, TypeTree(inlinedRhsType)), inlinedSym).withSpan(parent.span) else tpd.TypeDef(inlinedSym.asType).withSpan(parent.span) - + private def inlinedRhs(vddef: ValOrDefDef, inlinedSym: Symbol)(using Context): Tree = val rhs = vddef.rhs.changeOwner(vddef.symbol, inlinedSym) @@ -1060,14 +1062,14 @@ object Inlines: rhs else val symbolMap = mutable.Map[Symbol, Symbol]() - // TODO: This inlines also some calls to inline defs that were made in the inline trait body, is that ok? + // TODO: This inlines also some calls to inline defs that were made in the inline trait body, is that ok? val rhs1 = Inlined(tpd.ref(parentSym).withSpan(parent.span), Nil, inlined(rhs)._2.withSpan(parent.span).cloneIn(parentSym.source)).withSpan(parent.span) - + // In case of nested inline trait inlines, because BodyAnnotation is out of date, // body inlined misses nested expansion, but we have the symbols for the items that should be there // Remove them so that they can be inlined properly later. val ttmap = TreeTypeMap(treeMap = { - case tree@TypeDef(name, tmpl: Template) if Inlines.needsInlining(tree) => + case tree@TypeDef(name, tmpl: Template) if Inlines.needsInlining(tree) => val newSym = tree.symbol.copy(coord = spanCoord(tree.span)) // Coord should correspond to original location because we will inline from there. newSym.info = ClassInfo(tree.symbol.owner.thisType, newSym.asClass, tree.symbol.asClass.parentTypes, Scopes.newScope) @@ -1153,7 +1155,7 @@ object Inlines: class InlineTraitState( // For a class symbol created during inlining of an inline trait, - // the chain of inlined traits which produced it. We don't actually care about the order. + // the chain of inlined traits which produced it. We don't actually care about the order. // Used as a "seen list" for cycle checking. Persists across invocations of InlineParentTrait val inlineOrigins: mutable.Map[Symbol, Set[Symbol]] = mutable.HashMap[Symbol, Set[Symbol]]().withDefaultValue(Set.empty), val inlineTraitsPhase: InlineTraitState.InlineContext = InlineTraitState.InlineContext.None diff --git a/compiler/src/dotty/tools/dotc/parsing/Parsers.scala b/compiler/src/dotty/tools/dotc/parsing/Parsers.scala index 161275146cca..0533d8f2c7f4 100644 --- a/compiler/src/dotty/tools/dotc/parsing/Parsers.scala +++ b/compiler/src/dotty/tools/dotc/parsing/Parsers.scala @@ -2093,6 +2093,11 @@ object Parsers { || !canStartInfixTypeTokens.contains(ahead.token) || ahead.lineOffset > 0 + private def isPostfixQmark(isType: Boolean) = + val ahead = in.lookahead + !(if isType then canStartInfixTypeTokens else canStartInfixExprTokens).contains(ahead.token) + || ahead.lineOffset > 0 + inline def gobbleHat(): Boolean = if Feature.ccEnabled && isIdent(nme.UPARROW) then in.nextToken() @@ -2105,12 +2110,15 @@ object Parsers { refinedTypeRest(atSpan(startOffset(t)) { RefinedTypeTree(rejectWildcardType(t), refinement(indentOK = true)) }) - else if Feature.ccEnabled && in.isIdent(nme.UPARROW) && isCaptureUpArrow then + else if in.isIdent(nme.UPARROW) && Feature.ccEnabled && isCaptureUpArrow then atSpan(t.span.start): in.nextToken() if in.token == LBRACE then makeRetaining(t, captureSet(), tpnme.retains) else makeRetaining(t, Nil, tpnme.retainsCap) + else if in.isIdent(nme.?) && Feature.magicEnabled && isPostfixQmark(isType = true) then + atSpan(t.span.start): + PostfixOp(t, typeIdent()) else t } @@ -2670,7 +2678,7 @@ object Parsers { def expr1(location: Location = Location.Elsewhere): Tree = in.token match case IF => - ifExpr(in.offset, If) + ifExpr(in.offset, If, canOmitThen = Feature.magicEnabled && location == Location.InBlock) case WHILE => atSpan(in.skipToken()) { val cond = condExpr(DO) @@ -2757,7 +2765,7 @@ object Parsers { val start = in.skipToken() in.token match case IF => - ifExpr(start, InlineIf) + ifExpr(start, InlineIf, canOmitThen = false) case _ => postfixExpr() match case t @ Match(scrut, cases) => @@ -2820,16 +2828,33 @@ object Parsers { /** `if' `(' Expr `)' {nl} Expr [[semi] else Expr] -- Scala 2 compat * `if' Expr `then' Expr [[semi] else Expr] + * ‘if’ Expr [‘else’ Expr] -- under magic, if in block */ - def ifExpr(start: Offset, mkIf: (Tree, Tree, Tree) => If): If = - atSpan(start, in.skipToken()) { - val cond = condExpr(THEN) - newLinesOpt() - val thenp = subExpr() - val elsep = if (in.token == ELSE) { in.nextToken(); subExpr() } - else EmptyTree - mkIf(cond, thenp, elsep) - } + def ifExpr(start: Offset, mkIf: (Tree, Tree, Tree) => If, canOmitThen: Boolean): If = + atSpan(start, in.skipToken()): + if canOmitThen then + val cond = expr() + val thenp = + if in.token == THEN then + in.nextToken() + subExpr() + else EmptyTree + val elsep = + if in.token == ELSE then + in.nextToken() + subExpr() + else EmptyTree + mkIf(cond, thenp, elsep) + else + val cond = condExpr(THEN) + newLinesOpt() + val thenp = subExpr() + val elsep = + if in.token == ELSE then + in.nextToken() + subExpr() + else EmptyTree + mkIf(cond, thenp, elsep) /* When parsing (what will become) a sub sub match, that is, * when in a guard of case of a match, in a guard of case of a match; @@ -3102,7 +3127,10 @@ object Parsers { case USCORE => atSpan(startOffset(t), in.skipToken()) { PostfixOp(t, Ident(nme.WILDCARD)) } case _ => - if in.isColon && location == Location.InParens && followingIsLambdaParams() then + if in.isIdent(nme.?) && Feature.magicEnabled && isPostfixQmark(isType = false) then + atSpan(t.span.start): + PostfixOp(t, termIdent()) + else if in.isColon && location == Location.InParens && followingIsLambdaParams() then t match case id @ Ident(name) => if name.is(WildcardParamName) then @@ -5239,6 +5267,7 @@ object Parsers { * | Annotations LocalModifiers TmplDef * | Extension * | Expr1 + * | ‘if’ Expr [‘else’ Expr] * | */ def blockStatSeq(outermost: Boolean = false): List[Tree] = checkNoEscapingPlaceholders { diff --git a/compiler/src/dotty/tools/dotc/parsing/Scanners.scala b/compiler/src/dotty/tools/dotc/parsing/Scanners.scala index 6e72db059c42..826a61310551 100644 --- a/compiler/src/dotty/tools/dotc/parsing/Scanners.scala +++ b/compiler/src/dotty/tools/dotc/parsing/Scanners.scala @@ -196,7 +196,9 @@ object Scanners { val rewrite = ctx.settings.rewrite.value val oldSyntax = ctx.settings.oldSyntax.value - val newSyntax = ctx.settings.newSyntax.value || sourceVersion.requiresNewSyntax + + private val newSyntaxSetting = ctx.settings.newSyntax.value + def newSyntax = newSyntaxSetting || sourceVersion.requiresNewSyntax val rewriteToIndent = ctx.settings.indent.value && rewrite val rewriteNoIndent = ctx.settings.noindent.value && rewrite diff --git a/compiler/src/dotty/tools/dotc/printing/RefinedPrinter.scala b/compiler/src/dotty/tools/dotc/printing/RefinedPrinter.scala index 31f2b2b8811e..e3c85eecbc94 100644 --- a/compiler/src/dotty/tools/dotc/printing/RefinedPrinter.scala +++ b/compiler/src/dotty/tools/dotc/printing/RefinedPrinter.scala @@ -274,6 +274,12 @@ class RefinedPrinter(_ctx: Context) extends PlainPrinter(_ctx) { } def appliedText(tp: Type): Text = tp match + case AppliedType(tycon, r :: e :: Nil) if tycon.isRef(defn.MagicMaybeClass) => + if e.isRef(defn.UnitClass) then + toText(r) ~ "?" + else + atPrec(InfixPrec): + toText(r) ~ " ? " ~ toText(e) case tp @ AppliedType(tycon, args) => val namedElems = try tp.namedTupleElementTypesUpTo(200, false, normalize = false) @@ -638,6 +644,12 @@ class RefinedPrinter(_ctx: Context) extends PlainPrinter(_ctx) { && !printDebug && tree.typeOpt.exists then toText(tree.typeOpt) + else if tpt.symbol == defn.MagicMaybeClass && args.length == 2 then + if unsplice(args(1)).symbol == defn.UnitClass then + toTextLocal(args(0)) ~ "?" + else + changePrec(InfixPrec): + toText(args(0)) ~ " ? " ~ toText(args(1)) else args match case arg :: _ if arg.isTerm => toTextLocal(tpt) ~ "(" ~ Text(args.map(argText), ", ") ~ ")" diff --git a/compiler/src/dotty/tools/dotc/transform/BetaReduce.scala b/compiler/src/dotty/tools/dotc/transform/BetaReduce.scala index 7a03cd2a15f5..84f18cafa68e 100644 --- a/compiler/src/dotty/tools/dotc/transform/BetaReduce.scala +++ b/compiler/src/dotty/tools/dotc/transform/BetaReduce.scala @@ -45,6 +45,12 @@ class BetaReduce extends MiniPhase: if app1 ne app then report.log(i"beta reduce $app -> $app1") app1 + /** Cleanup ifs after reduceEQ */ + override def transformIf(tree: If)(using Context): Tree = tree.cond match + case Literal(Constant(true)) => tree.thenp + case Literal(Constant(false)) => tree.elsep + case _ => tree + object BetaReduce: import ast.tpd.* @@ -71,6 +77,9 @@ object BetaReduce: * type X1 = T1; ...; type Xm = Tm;val/def x1 = e1; ...; val/def xn = en; b * * This beta-reduction preserves the integrity of `Inlined` tree nodes. + * + * Also, replace some == tests between constants with known outcomes by true/false. + * This is useful since such tests can arise though inlining, e.g. in maybe-translation.scala. */ def apply(tree: Tree)(using Context): Tree = val bindingsBuf = new ListBuffer[DefTree] @@ -111,7 +120,7 @@ object BetaReduce: case None => tree case _ => - tree + inlines.Inliner.reduceEQ(tree) /** Beta-reduces a call to `ddef` with arguments `args` and registers new bindings. * @return optionally, the expanded call, or none if the actual argument diff --git a/compiler/src/dotty/tools/dotc/transform/InterceptedMethods.scala b/compiler/src/dotty/tools/dotc/transform/InterceptedMethods.scala index ad91ddcaabc4..7ffbe4990c34 100644 --- a/compiler/src/dotty/tools/dotc/transform/InterceptedMethods.scala +++ b/compiler/src/dotty/tools/dotc/transform/InterceptedMethods.scala @@ -86,7 +86,7 @@ class InterceptedMethods extends MiniPhase { val sym = tree.fun.symbol if sym == defn.Any_!= then - qual.select(defn.Any_==).appliedToTermArgs(tree.args).select(defn.Boolean_!).withSpan(tree.span) + qual.select(defn.Any_==).appliedToTermArgs(tree.args).not.withSpan(tree.span) else if ctx.explicitNulls then if sym == defn.Any_toString && !qual.tpe.isNotNull then ref(defn.Objects_toString).appliedTo(qual) diff --git a/compiler/src/dotty/tools/dotc/transform/PatternMatcher.scala b/compiler/src/dotty/tools/dotc/transform/PatternMatcher.scala index 72c9f348f4ac..edb448f5c13f 100644 --- a/compiler/src/dotty/tools/dotc/transform/PatternMatcher.scala +++ b/compiler/src/dotty/tools/dotc/transform/PatternMatcher.scala @@ -202,6 +202,8 @@ object PatternMatcher { case object NonEmptyTest extends Test // !scrutinee.isEmpty case object NonNullTest extends Test // scrutinee ne null case object GuardTest extends Test // scrutinee + case object IsOkTest extends Test // x != null, and possibly && !x.isInstanceOf[Fail] + case object IsErrTest extends Test // x == null, and possible || scrutinee.isInstanceOf[Fail] val noLengthTest = LengthTest(0, exact = false) @@ -374,29 +376,55 @@ object PatternMatcher { matchArgsPlan(selectors.take(arity - 1), args.take(arity - 1), matchSeq) } + def tupleApp(i: Int, receiver: Tree) = // manually inlining the call to NonEmptyTuple#apply, because it's an inline method + ref(defn.RuntimeTuplesModule) + .select(defn.RuntimeTuples_apply) + .appliedTo( + receiver.ensureConforms(defn.NonEmptyTupleTypeRef), // If scrutinee is a named tuple, cast to underlying tuple + Literal(Constant(i))) + + def getOfGetMatch(gm: Tree, isErrMatch: Boolean = false) = + val getSelection = gm.select(nme.get, _.info.isParameterless) + if gm.tpe.widen.isRef(defn.MagicMaybeClass) then + if isErrMatch then + val MagicMaybeType(_, errArg, nullable) = gm.tpe.widen.runtimeChecked + if errArg.isRef(defn.UnitClass) then + unitLiteral + else + val select = gm.asInstance(defn.MagicFailClass.typeRef.appliedTo(defn.AnyType)) + .select(nme.elem) + if nullable then + If(gm.nullTest(cond = true), + unitLiteral.ensureConforms(errArg), + select) + else select + else + val validTpe = defn.MagicValidClass.typeRef + If(gm.isInstance(validTpe), + gm.asInstance(validTpe).select(nme.elem), + gm + ).asInstance(getSelection.tpe.widen) + else getSelection + /** Plan for matching the result of an unapply against argument patterns `args` */ def unapplyPlan(unapp: Tree, args: List[Tree]): Plan = { def caseClass = unapp.symbol.owner.linkedClass lazy val caseAccessors = caseClass.caseAccessors val unappType = unapp.tpe.widen.stripNamedTuple.simplified + //println(i"unapp $tree, $unapp, $unappType") def isSyntheticScala2Unapply(sym: Symbol) = sym.is(Synthetic) && sym.owner.is(Scala2x) - def tupleApp(i: Int, receiver: Tree) = // manually inlining the call to NonEmptyTuple#apply, because it's an inline method - ref(defn.RuntimeTuplesModule) - .select(defn.RuntimeTuples_apply) - .appliedTo( - receiver.ensureConforms(defn.NonEmptyTupleTypeRef), // If scrutinee is a named tuple, cast to underlying tuple - Literal(Constant(i))) - - def getOfGetMatch(gm: Tree) = gm.select(nme.get, _.info.isParameterless) - // Disable Scala2Unapply optimization if the argument is a named argument for a single-element named tuple to - // enable selecting the field. See i23131.scala for test cases. val wasUnaryNamedTupleSelectArgForNamedTuple = args.length == 1 && args.head.removeAttachment(FirstTransform.WasNamedArg).isDefined && isGetMatch(unappType) && getOfGetMatch(unapp).tpe.widenDealias.isNamedTupleType - if (isSyntheticScala2Unapply(unapp.symbol) && caseAccessors.length == args.length && !wasUnaryNamedTupleSelectArgForNamedTuple) + if isSyntheticScala2Unapply(unapp.symbol) + && caseAccessors.length == args.length + && !wasUnaryNamedTupleSelectArgForNamedTuple + // Disable Scala2Unapply optimization if the argument is a named argument for a single-element named tuple to + // enable selecting the field. See i23131.scala for test cases. + then def tupleSel(sym: Symbol) = // If scrutinee is a named tuple, cast to underlying tuple, so that we can // continue to select with _1, _2, ... @@ -410,61 +438,73 @@ object PatternMatcher { else if unappType.derivesFrom(defn.BooleanClass) then TestPlan(GuardTest, unapp, unapp.span, onSuccess) else - letAbstract(unapp) { unappResult => - val isUnapplySeq = unapp.symbol.name == nme.unapplySeq - if isProductMatch(unappType, args.length) && !isUnapplySeq then - val selectors = productSelectors(unappType).take(args.length) - .map(ref(unappResult).select(_)) - matchArgsPlan(selectors, args, onSuccess) - else if isUnapplySeq && unapplySeqTypeElemTp(unappType.finalResultType).exists then - unapplySeqPlan(unappResult, args) - else if isUnapplySeq && isProductSeqMatch(unappType, args.length, unapp.srcPos) then - val selectors = productSelectors(unappType).map(ref(unappResult).select(_)) - unapplyProductSeqPlan(selectors, args) - else if unappResult.info <:< defn.NonEmptyTupleTypeRef then - val components = - (0 until unappResult.denot.info.tupleElementTypes.getOrElse(Nil).length) - .toList.map(tupleApp(_, ref(unappResult))) - matchArgsPlan(components, args, onSuccess) - else { - assert(isGetMatch(unappType)) - val argsPlan = { - val get = getOfGetMatch(ref(unappResult)) - if (isUnapplySeq) - letAbstract(get) { getResult => - if unapplySeqTypeElemTp(get.tpe).exists then - unapplySeqPlan(getResult, args) - else if isGenericTuple(getResult.info) then - val elemTypes = getResult.info.tupleElementTypes.getOrElse(Nil) - val selectors = elemTypes.zipWithIndex.map { (tp, i) => - val tree = tupleApp(i, ref(getResult)) - if i == elemTypes.length - 1 then tree.cast(tp) else tree - } - unapplyProductSeqPlan(selectors, args) - else { - val selectors = productSelectors(getResult.info).map(ref(getResult).select(_)) - unapplyProductSeqPlan(selectors, args) - } - } - else - letAbstract(get) { getResult => - // Special case: Normally, we pull out the argument wholesale if - // there is only one. But if the argument is a named argument for - // a single-element named tuple, we have to select the field instead. - // NamedArg trees are eliminated in FirstTransform but for named arguments - // of patterns we add a WasNamedArg attachment, which is used to guide the - // logic here. See i22900.scala for test cases. - val selectors = args match - case arg :: Nil if !wasUnaryNamedTupleSelectArgForNamedTuple => - ref(getResult) :: Nil - case _ => - productSelectors(getResult.info).map(ref(getResult).select(_)) - matchArgsPlan(selectors, args, onSuccess) + unapp match + case Apply(fn, arg :: Nil) if fn.symbol == defn.Magic_OkUnapply => + unappResultPlan(unapp, args, arg.symbol, unappType, wasUnaryNamedTupleSelectArgForNamedTuple, IsOkTest) + case Apply(fn, arg :: Nil) if fn.symbol == defn.Magic_ErrUnapply => + unappResultPlan(unapp, args, arg.symbol, unappType, wasUnaryNamedTupleSelectArgForNamedTuple, IsErrTest) + case _ => + letAbstract(unapp): unappResult => + unappResultPlan(unapp, args, unappResult, unappType, wasUnaryNamedTupleSelectArgForNamedTuple, NonEmptyTest) + } + + def unappResultPlan( + unapp: Tree, args: List[Tree], unappResult: Symbol, unappType: Type, + wasUnaryNamedTupleSelectArgForNamedTuple: Boolean, + nonEmptyTest: Test): Plan = { + val isUnapplySeq = unapp.symbol.name == nme.unapplySeq + if isProductMatch(unappType, args.length) && !isUnapplySeq then + val selectors = productSelectors(unappType).take(args.length) + .map(ref(unappResult).select(_)) + matchArgsPlan(selectors, args, onSuccess) + else if isUnapplySeq && unapplySeqTypeElemTp(unappType.finalResultType).exists then + unapplySeqPlan(unappResult, args) + else if isUnapplySeq && isProductSeqMatch(unappType, args.length, unapp.srcPos) then + val selectors = productSelectors(unappType).map(ref(unappResult).select(_)) + unapplyProductSeqPlan(selectors, args) + else if unappResult.info <:< defn.NonEmptyTupleTypeRef then + val components = + (0 until unappResult.denot.info.tupleElementTypes.getOrElse(Nil).length) + .toList.map(tupleApp(_, ref(unappResult))) + matchArgsPlan(components, args, onSuccess) + else { + assert(isGetMatch(unappType)) + val argsPlan = { + val get = getOfGetMatch(ref(unappResult), nonEmptyTest == IsErrTest) + if (isUnapplySeq) + letAbstract(get) { getResult => + if unapplySeqTypeElemTp(get.tpe).exists then + unapplySeqPlan(getResult, args) + else if isGenericTuple(getResult.info) then + val elemTypes = getResult.info.tupleElementTypes.getOrElse(Nil) + val selectors = elemTypes.zipWithIndex.map { (tp, i) => + val tree = tupleApp(i, ref(getResult)) + if i == elemTypes.length - 1 then tree.cast(tp) else tree } + unapplyProductSeqPlan(selectors, args) + else { + val selectors = productSelectors(getResult.info).map(ref(getResult).select(_)) + unapplyProductSeqPlan(selectors, args) + } + } + else + letAbstract(get) { getResult => + // Special case: Normally, we pull out the argument wholesale if + // there is only one. But if the argument is a named argument for + // a single-element named tuple, we have to select the field instead. + // NamedArg trees are eliminated in FirstTransform but for named arguments + // of patterns we add a WasNamedArg attachment, which is used to guide the + // logic here. See i22900.scala for test cases. + val selectors = args match + case arg :: Nil if !wasUnaryNamedTupleSelectArgForNamedTuple => + ref(getResult) :: Nil + case _ => + productSelectors(getResult.info).map(ref(getResult).select(_)) + matchArgsPlan(selectors, args, onSuccess) } - TestPlan(NonEmptyTest, unappResult, unapp.span, argsPlan) - } } + TestPlan(nonEmptyTest, unappResult, unapp.span, argsPlan) + } } // begin patternPlan @@ -487,6 +527,10 @@ object PatternMatcher { patternPlan(casted, pat, onSuccess) }) case UnApply(extractor, implicits, args) => + val mt @ MethodType(_) = extractor.tpe.widen.runtimeChecked + val admitsNull = mt.paramInfos.headOption match + case Some(MagicMaybeType(_, _, nullable)) => nullable + case _ => false val unappPlan = if (scrutinee.info.isBottomType) // Generate a throwaway but type-correct plan. // This plan will never execute because it'll be guarded by a `NonNullTest`. @@ -501,12 +545,12 @@ object PatternMatcher { assert(implicits.isEmpty) acc } - val mt @ MethodType(_) = extractor.tpe.widen: @unchecked val unapp0 = extractor.appliedTo(ref(scrutinee).ensureConforms(mt.paramInfos.head)) val unapp = applyImplicits(unapp0, implicits, mt.resultType) unapplyPlan(unapp, args) } - if (scrutinee.info.isNotNull || nonNull(scrutinee)) unappPlan + if scrutinee.info.isNotNull || nonNull(scrutinee) || admitsNull + then unappPlan else TestPlan(NonNullTest, scrutinee, tree.span, unappPlan) case Bind(name, body) => if (name == nme.WILDCARD) patternPlan(scrutinee, body, onSuccess) @@ -528,7 +572,7 @@ object PatternMatcher { ) } // When match against a `this.type` (say case a: this.type => ???), - // the typer will transform the pattern to a `Bind(..., Typed(Ident(a), ThisType(...)))`, + // the typer will transform the pattern to a `Bind(..., Typed(Ident(a), ThisType(...)))`, // then post typer will change all the `Ident` with a `ThisType` to a `This`. // Therefore, after pattern matching, we will have the following tree `Bind(..., Typed(This(...), ThisType(...)))`. // We handle now here the case were the pattern was transformed to a `This`, relying on the fact that the logic for @@ -806,23 +850,111 @@ object PatternMatcher { Inliner(plan) } + /** Drop tests that are oposites of previously established tests. + * + * When we have the following shape: + * + * if testA then plan1 + * if testB then plan2 + * nextPlan? + * + * where testA is a dual of testB and plan1 is test-free, transform it to + * + * if testA then plan1 + * plan2 + * nextPlan? + * + * "Dual" means: + * - one of the tests is an IsOKTest, the other is an + * IsErrTest or an "== null" test, + * - the two tests have the same scrutinee. + */ + def dropOpposites(plan: Plan): Plan = { + + object Dropper extends PlanTransform { + + def isTestFree(plan: Plan): Boolean = plan match + case _: TestPlan => false + case _: ReturnPlan => true + case _: ResultPlan => true + case LetPlan(_, expr) => isTestFree(expr) + case LabeledPlan(_, expr) => isTestFree(expr) + case SeqPlan(hd, tl) => isTestFree(hd) && isTestFree(tl) + + def isDual(test1: Test, test2: Test) = test1 match + case IsOkTest => + test2 match + case IsErrTest => true + case EqualTest(tree) => tree.tpe.isRef(defn.NullClass) + case _ => false + case _ => + false + + override def apply(plan: SeqPlan): Plan = { + if Feature.magicEnabled then + plan.head = apply(plan.head) + plan.tail = apply(plan.tail) + plan.head match + case TestPlan(test1, scrut1, _, follow1) => + def tryDropTest(plan: Plan) = plan match + case TestPlan(test2, scrut2, _, follow2) => + (scrut1, scrut2) match + case (_: Ident, _: Ident) + if scrut1.symbol == scrut2.symbol + && (isDual(test1, test2) || isDual(test2, test1)) + && isTestFree(follow1) => + follow2 + case _ => plan + case _ => + plan + + plan.tail match + case tail @ SeqPlan(tailHead, tailTail) => + tail.head = tryDropTest(tailHead) + case tail => + plan.tail = tryDropTest(tail) + case _ => + plan + } + } + Dropper(plan) + } + // ----- Generating trees from plans --------------- /** The condition a test plan rewrites to */ private def emitCondition(plan: TestPlan): Tree = val scrutinee = plan.scrutinee (plan.test: @unchecked) match - case NonEmptyTest => - constToLiteral( - scrutinee - .select(nme.isEmpty, _.info.isParameterless) - .select(nme.UNARY_!, _.info.isParameterless)) + case NonEmptyTest | IsOkTest => + scrutinee.tpe.widenDealias match + case MagicMaybeType(_, errArg, _) => + val test = scrutinee.nullTest(cond = false) + if errArg.isRef(defn.UnitClass) + then test + else test.and(scrutinee.isInstance(defn.MagicFailClass.typeRef).not) + case _ => + constToLiteral( + scrutinee + .select(nme.isEmpty, _.info.isParameterless) + .select(nme.UNARY_!, _.info.isParameterless)) + case IsOkTest => + val MagicMaybeType(_, errArg, _) = scrutinee.tpe.widenDealias.runtimeChecked + val notNull = scrutinee.nullTest(cond = false) + if errArg.isRef(defn.UnitClass) + then notNull + else notNull.and(scrutinee.isInstance(defn.MagicFailClass.typeRef).not) + case IsErrTest => + val MagicMaybeType(_, _, nullable) = scrutinee.tpe.widen.runtimeChecked + val typeTest = scrutinee.isInstance(defn.MagicFailClass.typeRef) + if nullable then scrutinee.nullTest(cond = true).or(typeTest) + else typeTest case NonNullTest => scrutinee.testNotNull case GuardTest => scrutinee case EqualTest(tree) => - tree.equal(scrutinee) + inlines.Inliner.reduceEQ(tree.equal(scrutinee)) case LengthTest(len, exact) => val lengthCompareSym = defn.Seq_lengthCompare.matchingMember(scrutinee.tpe) if (lengthCompareSym.exists) @@ -1037,7 +1169,7 @@ object PatternMatcher { if (acc.isEmpty) emitCondWithPos(otherPlan) else acc.select(nme.ZAND).appliedTo(emitCondWithPos(otherPlan)) } - If(conditions, emit(plan.onSuccess), unitLiteral) + conditional(conditions, emit(plan.onSuccess), unitLiteral) } } emitWithMashedConditions(plan :: Nil) @@ -1144,7 +1276,8 @@ object PatternMatcher { val optimizations: List[(String, Plan => Plan)] = List( "mergeTests" -> mergeTests, - "inlineVars" -> inlineVars + "inlineVars" -> inlineVars, + "dropOpposites" -> dropOpposites ) /** Translate pattern match to sequence of tests. */ diff --git a/compiler/src/dotty/tools/dotc/transform/patmat/Space.scala b/compiler/src/dotty/tools/dotc/transform/patmat/Space.scala index 167fb9aad82b..587428ed9951 100644 --- a/compiler/src/dotty/tools/dotc/transform/patmat/Space.scala +++ b/compiler/src/dotty/tools/dotc/transform/patmat/Space.scala @@ -411,7 +411,16 @@ object SpaceEngine { Prod(erase(pat.tpe.stripAnnots, isValue = false), funRef, pats.take(arity - 1).map(project) :+ projectSeq(pats.drop(arity - 1))) } else - Prod(erase(pat.tpe.stripAnnots, isValue = false), funRef, pats.map(project)) + val prod = Prod(erase(pat.tpe.stripAnnots, isValue = false), funRef, pats.map(project)) + // `Err.unapply` succeeds on `null`, which is how `Err(())` is represented for + // a maybe type with a nullable error type. Since such a maybe type decomposes + // into a space of its own for `null` (see `maybeParts`), `Err...)` has to cover + // it explicitly. + pat.tpe.widen.dealias match + case MagicMaybeType(_, _, /*nullable=*/true) if fun.symbol == defn.Magic_ErrUnapply => + Or(prod :: nullSpace :: Nil) + case _ => + prod case Typed(pat @ UnApply(_, _, _), _) => project(pat) @@ -438,6 +447,17 @@ object SpaceEngine { case tp => Typ(tp, decomposed = true) } + /** The two spaces a maybe type `T ? E` decomposes into: the `Ok` values, which + * are represented by `T ? Nothing`, and the `Err` values. The latter are `null` + * if `E` is a supertype of `Unit` (the invalid value is then `null` itself), and + * are represented by `Nothing ? E` otherwise. + */ + private def maybeParts(resTp: Type, errTp: Type, nullable: Boolean)(using Context): List[Type] = + val errPart = + if nullable then ConstantType(Constant(null)) + else MagicMaybeType(defn.NothingType, errTp) + MagicMaybeType(resTp, defn.NothingType) :: errPart :: Nil + private def unapplySeqInfo(resTp: Type, pos: SrcPos)(using Context): (Int, Type, Type) = { var resultTp = resTp var elemTp = unapplySeqTypeElemTp(resultTp) @@ -561,12 +581,23 @@ object SpaceEngine { && tp1 =:= tp2 } + /** Return term parameter types of the extractor `unapp`. */ + def signature(unapp: TermRef, scrutineeTp: Type, argLen: Int)(using Context): List[Type] = + // `Ok.unapply` extracts the result component of a maybe type, `Err.unapply` its error + // component. We cannot infer these from the extractor's signature, since the type of + // the component that is not extracted cannot be constrained from the scrutinee type, + // which would make the inferred component type `Any`. + scrutineeTp.dealias match + case MagicMaybeType(resTp, _, _) if unapp.symbol == defn.Magic_OkUnapply => resTp :: Nil + case MagicMaybeType(_, errTp, _) if unapp.symbol == defn.Magic_ErrUnapply => errTp :: Nil + case _ => extractorSignature(unapp, scrutineeTp, argLen) + /** Return term parameter types of the extractor `unapp`. * Parameter types of the case class type `tp`. Adapted from `unapplyPlan` in patternMatcher */ - def signature(unapp: TermRef, scrutineeTp: Type, argLen: Int)(using Context): List[Type] = trace(i"signature($unapp, $scrutineeTp, $argLen)") { + private def extractorSignature(unapp: TermRef, scrutineeTp: Type, argLen: Int)(using Context): List[Type] = trace(i"signature($unapp, $scrutineeTp, $argLen)") { val unappSym = unapp.symbol - val mt: MethodType = unapp.widen match { + val mt: MethodType = unapp.widenDealias match { case mt: MethodType => mt case pt: PolyType => scrutineeTp match @@ -659,6 +690,14 @@ object SpaceEngine { val AppliedType(_, tp :: Nil) = unapp.prefix.widen.dealias: @unchecked scrutineeTp <:< tp } + || scrutineeTp.match + // `Ok(_)` covers a maybe type without error values, `Err(_)` covers a maybe type + // without result values. These are the two spaces a maybe type decomposes into, + // see `maybeParts`. + case MagicMaybeType(resTp, errTp, _) => + unapp.symbol == defn.Magic_OkUnapply && errTp.isNothingType + || unapp.symbol == defn.Magic_ErrUnapply && resTp.isNothingType + case _ => false } /** Decompose a type into subspaces -- assume the type can be decomposed */ @@ -677,6 +716,8 @@ object SpaceEngine { case tp if !TypeComparer.provablyDisjoint(tp, tpB) => AndType(tp, tpB) case OrType(tp1, tp2) => List(tp1, tp2) + case MagicMaybeType(resTp, errTp, nullable) + if !resTp.isNothingType && !errTp.isNothingType => maybeParts(resTp, errTp, nullable) case tp if tp.isRef(defn.BooleanClass) => List(ConstantType(Constant(true)), ConstantType(Constant(false))) case tp if tp.isRef(defn.UnitClass) => ConstantType(Constant(())) :: Nil case tp @ NamedType(Parts(parts), _) => if parts.exists(_ eq tp) then ListOfNoType else parts.map(tp.derivedSelect) @@ -871,6 +912,9 @@ object SpaceEngine { case Typ(tp: TermRef, _) => if (flattenList && tp <:< defn.NilType) "" else tp.symbol.showName + case Typ(MagicMaybeType(resTp, errTp, _), _) if resTp.isNothingType ^ errTp.isNothingType => + // the spaces a maybe type decomposes into, see `maybeParts` + if resTp.isNothingType then "Err(_)" else "Ok(_)" case Typ(tp, decomposed) => val cls = tp.classSymbol if ctx.definitions.isTupleNType(tp.stripNamedTuple) then @@ -1100,7 +1144,10 @@ object SpaceEngine { def checkReachability(m: Match)(using Context): Unit = trace(i"checkReachability($m)"): val selTyp = toUnderlying(m.selector.tpe).dealias - val isNullable = selTyp.isInstanceOf[FlexibleType] || selTyp.classSymbol.isNullableClass + val isNullable = selTyp match + case MagicMaybeType(_, _, nullable) => nullable + case _: FlexibleType => true + case _ => selTyp.classSymbol.isNullableClass val targetSpace = trace(i"targetSpace($selTyp)"): if isNullable && !ctx.mode.is(Mode.SafeNulls) then project(OrType(selTyp, ConstantType(Constant(null)), soft = false)) diff --git a/compiler/src/dotty/tools/dotc/typer/Checking.scala b/compiler/src/dotty/tools/dotc/typer/Checking.scala index 52798352e040..ce80d0eaaf50 100644 --- a/compiler/src/dotty/tools/dotc/typer/Checking.scala +++ b/compiler/src/dotty/tools/dotc/typer/Checking.scala @@ -129,11 +129,13 @@ object Checking { checkBounds(args, bounds, instantiate, tree.tpe, tpt) def checkWildcardApply(tp: Type): Unit = tp match { - case tp @ AppliedType(tycon, _) => + case tp @ AppliedType(tycon, _) if tp.hasWildcardArg => if tp.isUnreducibleWild then report.errorOrMigrationWarning( showInferred(UnreducibleApplication(tycon), tp, tpt), tree.srcPos, MigrationVersion.Scala2to3) + else if tp.typeSymbol == defn.MagicMaybeClass then + report.error(em"Maybe type may not contain wildcard arguments", tree.srcPos) case _ => } def checkValidIfApply(using Context): Unit = diff --git a/compiler/src/dotty/tools/dotc/typer/SpecStrings.scala b/compiler/src/dotty/tools/dotc/typer/SpecStrings.scala index 63b6f0df84ad..bd3c5e1b4c39 100644 --- a/compiler/src/dotty/tools/dotc/typer/SpecStrings.scala +++ b/compiler/src/dotty/tools/dotc/typer/SpecStrings.scala @@ -152,7 +152,7 @@ trait SpecStrings { this: Typer => extractBackquoted(strLit, closing + 1) case untpd.TypedSplice(splice) => extract( - untpd.TypedSplice(ref(defn.Compiletime_wrappedType) + untpd.TypedSplice(ref(defn.Magic_wrappedType) .appliedToTypeTree(splice))) case tree: untpd.TypedSplice => extract(tree) diff --git a/compiler/src/dotty/tools/dotc/typer/Typer.scala b/compiler/src/dotty/tools/dotc/typer/Typer.scala index 0b99ca4f2cb7..ae10464f9e00 100644 --- a/compiler/src/dotty/tools/dotc/typer/Typer.scala +++ b/compiler/src/dotty/tools/dotc/typer/Typer.scala @@ -925,7 +925,7 @@ class Typer(@constructorOnly nestingLevel: Int = 0) extends Namer // Otherwise, under magic, if selector is `$spec`, convert to spec string representation. def trySpecString(tree: untpd.Select, qual: Tree) = if selName == nme.SPEC then - ref(defn.Compiletime_spec).appliedTo(qual).withSpan(tree.span) + ref(defn.Magic_spec).appliedTo(qual).withSpan(tree.span) else EmptyTree // Otherwise, try a GADT approximation if we're trying to select a member @@ -1660,54 +1660,68 @@ class Typer(@constructorOnly nestingLevel: Int = 0) extends Namer } } - def typedIf(tree: untpd.If, pt: Type)(using Context): Tree = - if tree.isInline then checkInInlineContext("inline if", tree.srcPos) - val cond1 = typed(tree.cond, defn.BooleanType) + def typedIf(tree: untpd.If, pt: Type)(using Context): Tree = { + if tree.thenp.isEmpty then + assert(Feature.magicEnabled) + val elsep1 = + if tree.elsep.isEmpty then tpd.unitLiteral else typed(tree.elsep) + val labelType = defn.Magic_CanErr.typeRef.appliedTo(elsep1.tpe.widen) + inferImplicit(labelType, EmptyTree, tree.span) match + case fail: SearchFailure if !fail.isAmbiguous => + errorTree(tree, em"`if` without `then` is illegal here since no given of type $labelType is available.") + case _ => + val desugared = + if tree.elsep.isEmpty + then cpy.Apply(tree)(untpd.ref(defn.Magic_provided1), tree.cond :: Nil) + else cpy.Apply(tree)(untpd.ref(defn.Magic_provided2), tree.cond :: untpd.TypedSplice(elsep1) :: Nil) + typedApply(desugared, pt) + else + val cond1 = typed(tree.cond, defn.BooleanType) - def isIncomplete(tree: untpd.If): Boolean = tree.elsep match - case EmptyTree => true - case elsep: untpd.If => isIncomplete(elsep) - case _ => false + def isIncomplete(tree: untpd.If): Boolean = tree.elsep match + case EmptyTree => true + case elsep: untpd.If => isIncomplete(elsep) + case _ => false - // Insert a GADT cast if the type of the branch does not conform - // to the type assigned to the whole if tree. - // This happens when the computation of the type of the if tree - // uses GADT constraints. See #15646. - def gadtAdaptBranch(tree: Tree, branchPt: Type): Tree = - TypeComparer.testSubType(tree.tpe.widenExpr, branchPt) match { - case CompareResult.OKwithGADTUsed => - insertGadtCast(tree, tree.tpe.widen, branchPt) - case _ => tree - } + // Insert a GADT cast if the type of the branch does not conform + // to the type assigned to the whole if tree. + // This happens when the computation of the type of the if tree + // uses GADT constraints. See #15646. + def gadtAdaptBranch(tree: Tree, branchPt: Type): Tree = + TypeComparer.testSubType(tree.tpe.widenExpr, branchPt) match { + case CompareResult.OKwithGADTUsed => + insertGadtCast(tree, tree.tpe.widen, branchPt) + case _ => tree + } - val branchPt = if isIncomplete(tree) then defn.UnitType else pt.dropIfProto + val branchPt = if isIncomplete(tree) then defn.UnitType else pt.dropIfProto - val result = - if tree.elsep.isEmpty then - val thenp1 = typed(tree.thenp, branchPt)(using cond1.nullableContextIf(true)) - val elsep1 = tpd.unitLiteral.withSpan(tree.span.endPos) - cpy.If(tree)(cond1, thenp1, elsep1).withType(defn.UnitType) - else - val thenp1 :: elsep1 :: Nil = harmonic(harmonize, pt) { - val thenp0 = typed(tree.thenp, branchPt)(using cond1.nullableContextIf(true)) - val elsep0 = typed(tree.elsep, branchPt)(using cond1.nullableContextIf(false)) - thenp0 :: elsep0 :: Nil - }: @unchecked - - val resType = thenp1.tpe | elsep1.tpe - val thenp2 :: elsep2 :: Nil = - (thenp1 :: elsep1 :: Nil) map { t => - // Adapt each branch to ensure that their types conforms to the - // type assigned to the if tree by inserting GADT casts. - gadtAdaptBranch(t, resType) + val result = + if tree.elsep.isEmpty then + val thenp1 = typed(tree.thenp, branchPt)(using cond1.nullableContextIf(true)) + val elsep1 = tpd.unitLiteral.withSpan(tree.span.endPos) + cpy.If(tree)(cond1, thenp1, elsep1).withType(defn.UnitType) + else + val thenp1 :: elsep1 :: Nil = harmonic(harmonize, pt) { + val thenp0 = typed(tree.thenp, branchPt)(using cond1.nullableContextIf(true)) + val elsep0 = typed(tree.elsep, branchPt)(using cond1.nullableContextIf(false)) + thenp0 :: elsep0 :: Nil }: @unchecked - cpy.If(tree)(cond1, thenp2, elsep2).withType(resType) + val resType = thenp1.tpe | elsep1.tpe + val thenp2 :: elsep2 :: Nil = + (thenp1 :: elsep1 :: Nil) map { t => + // Adapt each branch to ensure that their types conforms to the + // type assigned to the if tree by inserting GADT casts. + gadtAdaptBranch(t, resType) + }: @unchecked - def thenPathInfo = cond1.notNullInfoIf(true).seq(result.thenp.notNullInfo) - def elsePathInfo = cond1.notNullInfoIf(false).seq(result.elsep.notNullInfo) - result.withNotNullInfo(thenPathInfo.alt(elsePathInfo)) - end typedIf + cpy.If(tree)(cond1, thenp2, elsep2).withType(resType) + + def thenPathInfo = cond1.notNullInfoIf(true).seq(result.thenp.notNullInfo) + def elsePathInfo = cond1.notNullInfoIf(false).seq(result.elsep.notNullInfo) + result.withNotNullInfo(thenPathInfo.alt(elsePathInfo)) + } /** Decompose function prototype into a list of parameter prototypes and a result * prototype tree, using WildcardTypes where a type is not known. @@ -3793,9 +3807,12 @@ class Typer(@constructorOnly nestingLevel: Int = 0) extends Namer val result = if (ctx.mode.is(Mode.Type)) typedAppliedTypeTree( - if op.name == tpnme.throws && Feature.enabled(Feature.saferExceptions) - then desugar.throws(l, op, r) - else cpy.AppliedTypeTree(tree)(op, l :: r :: Nil)) + if op.name == tpnme.throws && Feature.enabled(Feature.saferExceptions) then + desugar.throws(l, op, r) + else if op.name == tpnme.? && Feature.magicEnabled then + cpy.AppliedTypeTree(tree)(untpd.ref(defn.MagicMaybeClass.typeRef), l :: r :: Nil) + else + cpy.AppliedTypeTree(tree)(op, l :: r :: Nil)) else if (ctx.mode.is(Mode.Pattern)) typedUnApply(cpy.Apply(tree)(op, l :: r :: Nil), pt) else { diff --git a/compiler/src/dotty/tools/dotc/typer/TyperPhase.scala b/compiler/src/dotty/tools/dotc/typer/TyperPhase.scala index 7fb67db7d939..b3f57a6fdf22 100644 --- a/compiler/src/dotty/tools/dotc/typer/TyperPhase.scala +++ b/compiler/src/dotty/tools/dotc/typer/TyperPhase.scala @@ -8,6 +8,7 @@ import Phases.* import Contexts.* import Symbols.* import ImportInfo.withRootImports +import Decorators.em import parsing.{Parser => ParserPhase} import config.Printers.typr import inlines.PrepareInlineable @@ -45,6 +46,8 @@ class TyperPhase(addRootImports: Boolean = true) extends Phase { try if !unit.suspended then ctx.profiler.onUnit(ctx.phase, unit): unit.tpdTree = ctx.typer.typedExpr(unit.untpdTree) + if unit.magic && !ctx.explicitNulls then + report.error(em"magic can be enabled only if -Yexplicit-nulls is also set", unit.tpdTree.srcPos.startPos) typr.println("typed: " + unit.source) record("retained untyped trees", unit.untpdTree.treeSize) record("retained typed trees after typer", unit.tpdTree.treeSize) diff --git a/docs/_docs/internals/syntax.md b/docs/_docs/internals/syntax.md index 5c538679b897..75adf81f4de4 100644 --- a/docs/_docs/internals/syntax.md +++ b/docs/_docs/internals/syntax.md @@ -296,7 +296,8 @@ SimpleExpr ::= SimpleRef | SimpleExpr ArgumentExprs Apply(expr, args) | SimpleExpr ColonArgument -- under language.experimental.fewerBraces | SimpleExpr ‘_’ PostfixOp(expr, _) (to be dropped) - | XmlExpr -- to be dropped + | SimpleExpr '?' -- under language.experimental.magic + | XmlExpr -- to be dropped ColonArgument ::= colon {LambdaStart} indent (CaseClauses | Block) outdent | colon LambdaStart {LambdaStart} expr ENDlambda -- ENDlambda is inserted for each production at next EOL @@ -329,6 +330,7 @@ BlockStat ::= Import | Extension | Expr1 | EndMarker + | ‘if’ Expr [‘else’ Expr] TypeBlock ::= {TypeBlockStat semi} Type TypeBlockStat ::= ‘type’ {nl} TypeDef diff --git a/library/src-bootstrapped/scala/magic/package.scala b/library/src-bootstrapped/scala/magic/package.scala new file mode 100644 index 000000000000..7183e1efeec3 --- /dev/null +++ b/library/src-bootstrapped/scala/magic/package.scala @@ -0,0 +1,34 @@ +//> using options -Yexplicit-nulls +package scala +import language.experimental.magic +import scala.util.boundary, boundary.{Label, break} + +package object magic { + + type CanErr[E] = Label[Nothing ? E] + + inline def maybe[T, E](inline body: CanErr[E] ?=> T): T ? E = + boundary(Ok(body)) + + extension [T, E](x: T ? E) + transparent inline def ? (using CanErr[E]): T = x match + case Ok(y) => y + case Err(e) => break(Err(e)) + + def withErr[E1](e: E1): T ? E1 = x match + case Ok(y) => Ok(y) + case Err(_) => Err(e) + + def mapErr[E1](f: E => E1): T ? E1 = x match + case Ok(y) => Ok(y) + case Err(e) => Err(f(e)) + + inline def provided(inline cond: Boolean)(using CanErr[Unit]): Unit = + if !cond then boundary.break(Err(())) + + inline def provided[E](inline cond: Boolean, inline e: E)(using CanErr[E]): Unit = + if !cond then boundary.break(Err(e)) + + +} + diff --git a/library/src/scala/compiletime/package.scala b/library/src/scala/compiletime/package.scala index a45c53dbf25e..fd27aa8f8595 100644 --- a/library/src/scala/compiletime/package.scala +++ b/library/src/scala/compiletime/package.scala @@ -237,10 +237,3 @@ def byName[T](x: => T): T = x extension [T](x: T) transparent inline def asMatchable: x.type & Matchable = x.asInstanceOf[x.type & Matchable] -/** Used internally under magic: A wrapper for spec strings */ -inline def `$spec`(inline sc: StringContext)(inline args: Any*): Unit = () - -/** Used internally under magic: A wrapper for backquoted references to types from - * spec strings. - */ -def `$wrappedType`[T]: Unit = () \ No newline at end of file diff --git a/library/src/scala/magic/Err.scala b/library/src/scala/magic/Err.scala new file mode 100644 index 000000000000..b5f5daffd167 --- /dev/null +++ b/library/src/scala/magic/Err.scala @@ -0,0 +1,21 @@ +package scala.magic + +import language.experimental.magic +import scala.magic.runtime +import scala.magic.compiletime.Maybe +import annotation.experimental + +@experimental +object Err: + + /** `inline` needed since nonbootrapped 3.9.0 compiler + * uses a different erasure for Maybe than boostrapped + * 3.10.0 compiler. `inline` is also benefical since it can + * remove the condition and the `if` when the argument is known. + */ + inline def apply[E](e: E): Maybe[Nothing, E] = + (if e == () then null else new runtime.Fail(e)) + .asInstanceOf[Maybe[Nothing, E]] + + def unapply[E](x: Maybe[Any, E]): Maybe[E, Nothing] = ??? + diff --git a/library/src/scala/magic/Ok.scala b/library/src/scala/magic/Ok.scala new file mode 100644 index 000000000000..c3fc24cc7dc3 --- /dev/null +++ b/library/src/scala/magic/Ok.scala @@ -0,0 +1,15 @@ +package scala.magic + +import scala.magic.runtime.Valid +import scala.magic.compiletime.Maybe +import annotation.experimental + +@experimental +object Ok: + inline def apply[T](x: T): Maybe[T, Nothing] = { + if x == null then new Valid(null) + else if x.isInstanceOf[Valid] then new Valid(x) + else x + }.asInstanceOf[Maybe[T, Nothing]] + + def unapply(x: Maybe[Any, Any]): x.type = x diff --git a/library/src/scala/magic/compiletime/Maybe.scala b/library/src/scala/magic/compiletime/Maybe.scala new file mode 100644 index 000000000000..6e3216a25e3f --- /dev/null +++ b/library/src/scala/magic/compiletime/Maybe.scala @@ -0,0 +1,14 @@ +package scala.magic.compiletime + +import scala.magic.runtime.Valid +import annotation.experimental + +/** Under experimental.magic, a trait backing maybe types `T?` */ +@experimental +sealed trait Maybe[+T, +E] extends Any, Matchable: + def isEmpty: Boolean + def get: T + + + + diff --git a/library/src/scala/magic/compiletime/package.scala b/library/src/scala/magic/compiletime/package.scala new file mode 100644 index 000000000000..cd3fe8f5c7f6 --- /dev/null +++ b/library/src/scala/magic/compiletime/package.scala @@ -0,0 +1,16 @@ +package scala.magic + +import annotation.experimental + +package object compiletime { + + /** Used internally under magic: A wrapper for spec strings */ + @experimental + inline def `$spec`(inline sc: StringContext)(inline args: Any*): Unit = () + + /** Used internally under magic: A wrapper for backquoted references to types from + * spec strings. + */ + @experimental + def `$wrappedType`[T]: Unit = () +} diff --git a/library/src/scala/magic/runtime/Fail.scala b/library/src/scala/magic/runtime/Fail.scala new file mode 100644 index 000000000000..32238d3d6deb --- /dev/null +++ b/library/src/scala/magic/runtime/Fail.scala @@ -0,0 +1,7 @@ +package scala.magic.runtime + +import annotation.experimental + +@experimental +class Fail[+E](val elem: E): + override def toString = s"Fail($elem)" diff --git a/library/src/scala/magic/runtime/Valid.scala b/library/src/scala/magic/runtime/Valid.scala new file mode 100644 index 000000000000..551f2f00fac1 --- /dev/null +++ b/library/src/scala/magic/runtime/Valid.scala @@ -0,0 +1,6 @@ +package scala.magic.runtime + +import annotation.experimental + +@experimental +class Valid(val elem: Any) diff --git a/tests/explicit-nulls/pos/opt-maybe.scala b/tests/explicit-nulls/pos/opt-maybe.scala new file mode 100644 index 000000000000..ff4e6406ecb4 --- /dev/null +++ b/tests/explicit-nulls/pos/opt-maybe.scala @@ -0,0 +1,11 @@ + +import language.experimental.magic +import scala.magic.* +type Opt[+A] = A | Null +object Opt: + + def unapply[A](o: Opt[A]): A? = + if o != null then Ok(o.asInstanceOf[o.type & A]) + else null + +end Opt \ No newline at end of file diff --git a/tests/init-global/pos/match-complete-maybe.scala b/tests/init-global/pos/match-complete-maybe.scala new file mode 100644 index 000000000000..60c7d404058a --- /dev/null +++ b/tests/init-global/pos/match-complete-maybe.scala @@ -0,0 +1,114 @@ +//> using options -Yexplicit-nulls +import language.experimental.magic +object Matcher { + // Chained Match + val chained_match_xs: List[Any] = List(1, 2, 3) + val chained_match_x = chained_match_xs match { + case Nil => "empty" + case _ => "nonempty" + } match { + case "empty" => 0 + case "nonempty" => 1 + } + println(chained_match_x) + + // Vararg Splices + val vararg_arr = Array(0, 1, 2, 3) + val vararg_lst = List(vararg_arr*) // vararg splice argument + // Throws an exception? + val vararg_splice = vararg_lst match + case List(0, 1, xs*) => 1 // binds xs to Seq(2, 3) + case List(1, _*) => 0 // wildcard pattern + case _ => 2 + println(vararg_splice) + println(vararg_lst) + + // Pattern Definitions + val patter_def_xs: List[Any] = List(1, 2, 3) + val (patter_def_x: Any) :: _ = patter_def_xs : @unchecked + println(patter_def_x) + + val patter_def_pair = (1, true) + val (patter_def_a, patter_def_b) = patter_def_pair + println(patter_def_a) + + val elems: List[(Int, Int)] = List((1, 2), (3, 4), (5, 6)) + + for ((x,y) <- elems) do println(x) + + def main(args: Array[String]) = { + // println(chained_match_x) + println(vararg_splice) + // println(patter_def_x) + // println( + } +} + +// Patter Matching Using Extractors + +// Option Extractors +case class Person(name: String, age: Int) +object Person { + def unapply(person: Person): (String, Int)? = (person.name, person.age) +} + +object OptionMatcher { + val person = Person("Alice", 25) + + val result = person match { + case Person(name, age) => s"Name: $name, Age: $age" + case _ => "Not a person" + } + println(result) +} + +// Boolean Extractors +object Adult { + def unapply(person: Person): Boolean = person.age >= 18 +} + +object BooleanMatcher { + val person = Person("Charlie", 17) + + val adultResult = person match { + case Adult() => s"${person.name} is an adult" + case _ => s"${person.name} is not an adult" + } + + println(adultResult) +} + +// Variadic Extractors +// Add cases for exceptions +// +// Adding some warning test cases +// - + +object VariadicExtractor { + // Define an unapply method that takes a List and returns an Option of Seq + def unapplySeq[A](list: List[A]): Seq[A]? = list +} + +object PatternMatchExample { + def describeList(list: List[Int]): String = list match { + case VariadicExtractor(1, 2, rest @ _*) => + s"Starts with 1, 2 followed by: ${rest.mkString(", ")}" + case VariadicExtractor(1, rest @ _*) => + s"Starts with 1 followed by: ${rest.mkString(", ")}" + case VariadicExtractor(first, second, rest @ _*) => + s"Starts with $first, $second followed by: ${rest.mkString(", ")}" + case VariadicExtractor(single) => + s"Only one element: $single" + case VariadicExtractor() => + "Empty list" + case _ => + "Unknown pattern" + } + + // Test cases + println(describeList(List(1, 2, 3, 4, 5))) // Output: Starts with 1, 2 followed by: 3, 4, 5 + println(describeList(List(1, 3, 4, 5))) // Output: Starts with 1 followed by: 3, 4, 5 + println(describeList(List(2, 3, 4, 5))) // Output: Starts with 2, 3 followed by: 4, 5 + println(describeList(List(1))) // Output: Only one element: 1 + println(describeList(List())) // Output: Empty list +} diff --git a/tests/init-global/warn/unapply-implicit-arg2-maybe.check b/tests/init-global/warn/unapply-implicit-arg2-maybe.check new file mode 100644 index 000000000000..40a0e2cdc6ee --- /dev/null +++ b/tests/init-global/warn/unapply-implicit-arg2-maybe.check @@ -0,0 +1,12 @@ +-- Warning: tests/init-global/warn/unapply-implicit-arg2-maybe.scala:9:40 ---------------------------------------------- +9 | if i == 0 then f1.m1(i1) else f1.m2(i2) // warn + | ^^ + | Access uninitialized field value i2. Calling trace: + | ├── object Bar { [ unapply-implicit-arg2-maybe.scala:3 ] + | │ ^ + | ├── case Bar(i) => i [ unapply-implicit-arg2-maybe.scala:14 ] + | │ ^^^^^^ + | ├── def unapply(using f1: Foo)(i: Int): Int? = [ unapply-implicit-arg2-maybe.scala:8 ] + | │ ^ + | └── if i == 0 then f1.m1(i1) else f1.m2(i2) // warn [ unapply-implicit-arg2-maybe.scala:9 ] + | ^^ diff --git a/tests/init-global/warn/unapply-implicit-arg2-maybe.scala b/tests/init-global/warn/unapply-implicit-arg2-maybe.scala new file mode 100644 index 000000000000..04eff7686bb1 --- /dev/null +++ b/tests/init-global/warn/unapply-implicit-arg2-maybe.scala @@ -0,0 +1,16 @@ +//> using options -Yexplicit-nulls +import language.experimental.magic +object Bar { + class Foo { + def m1(i: Int) = i+1 + def m2(i: Int) = i+2 + } + def unapply(using f1: Foo)(i: Int): Int? = + if i == 0 then f1.m1(i1) else f1.m2(i2) // warn + + given Foo = new Foo + val i1: Int = 0 + val i2: Int = i1 match + case Bar(i) => i + case _ => 0 +} diff --git a/tests/neg/bad-unapplies-maybe.check b/tests/neg/bad-unapplies-maybe.check new file mode 100644 index 000000000000..c76f6b45d0c0 --- /dev/null +++ b/tests/neg/bad-unapplies-maybe.check @@ -0,0 +1,39 @@ +-- [E051] Reference Error: tests/neg/bad-unapplies-maybe.scala:24:9 ---------------------------------------------------- +24 | case A("2") => // error (cannot resolve overloading) + | ^ + | Ambiguous overload. The overloaded alternatives of method unapply in object A with types + | (x: B): String? + | (x: A): String? + | both match arguments (C) + | + | longer explanation available when compiling with `-explain` +-- [E127] Pattern Match Error: tests/neg/bad-unapplies-maybe.scala:25:9 ------------------------------------------------ +25 | case B("2") => // error (cannot be used as an extractor) + | ^ + |B cannot be used as an extractor in a pattern because it lacks an unapply or unapplySeq method with the appropriate signature + | + | longer explanation available when compiling with `-explain` +-- [E127] Pattern Match Error: tests/neg/bad-unapplies-maybe.scala:26:9 ------------------------------------------------ +26 | case D("2") => // error (cannot be used as an extractor) + | ^ + |D cannot be used as an extractor in a pattern because it lacks an unapply or unapplySeq method with the appropriate signature + | + | longer explanation available when compiling with `-explain` +-- [E050] Type Error: tests/neg/bad-unapplies-maybe.scala:27:9 --------------------------------------------------------- +27 | case E("2") => // error (value unapply in object E does not take parameters) + | ^ + | value unapply in object E does not take parameters + | + | longer explanation available when compiling with `-explain` +-- [E107] Syntax Error: tests/neg/bad-unapplies-maybe.scala:28:10 ------------------------------------------------------ +28 | case F("2") => // error (Wrong number of argument patterns for F; expected: ()) + | ^^^^^^ + | Wrong number of argument patterns for F; expected: () + | + | longer explanation available when compiling with `-explain` +-- [E189] Not Found Error: tests/neg/bad-unapplies-maybe.scala:29:9 ---------------------------------------------------- +29 | case G("2") => // error (Not found: G) + | ^ + | no pattern match extractor named G was found + | + | longer explanation available when compiling with `-explain` diff --git a/tests/neg/bad-unapplies-maybe.scala b/tests/neg/bad-unapplies-maybe.scala new file mode 100644 index 000000000000..db811c5ab09d --- /dev/null +++ b/tests/neg/bad-unapplies-maybe.scala @@ -0,0 +1,30 @@ +//> using options -Yexplicit-nulls +import language.experimental.magic +trait A +trait B +class C extends A, B +object A: + def unapply(x: A): String? = x.toString + def unapply(x: B): String? = x.toString + +object B + +object D: + def unapply(x: A, y: B): String? = x.toString + +object E: + val unapply: Option[String] = Some("") + +object F: + def unapply(x: Int): Boolean = true + + +@main def Test = + C() match + case A("2") => // error (cannot resolve overloading) + case B("2") => // error (cannot be used as an extractor) + case D("2") => // error (cannot be used as an extractor) + case E("2") => // error (value unapply in object E does not take parameters) + case F("2") => // error (Wrong number of argument patterns for F; expected: ()) + case G("2") => // error (Not found: G) +end Test diff --git a/tests/neg/experimentalUnapply-maybe.scala b/tests/neg/experimentalUnapply-maybe.scala new file mode 100644 index 000000000000..c1dce46b7b85 --- /dev/null +++ b/tests/neg/experimentalUnapply-maybe.scala @@ -0,0 +1,22 @@ +//> using options -Yexplicit-nulls + + +import language.experimental.magic +import scala.annotation.experimental + +@experimental +class A + +object Extractor1: + def unapply(s: Any): A? = ??? // error + +object Extractor2: + @experimental + def unapply(s: Any): Int? = ??? + +def test: Unit = + (??? : Any) match + case _: A => // error // error + case Extractor1(_) => // error + case Extractor2(_) => // error + () diff --git a/tests/neg/i14896-maybe.scala b/tests/neg/i14896-maybe.scala new file mode 100644 index 000000000000..c881fc970f93 --- /dev/null +++ b/tests/neg/i14896-maybe.scala @@ -0,0 +1,4 @@ +//> using options -Yexplicit-nulls +import language.experimental.magic +object Ex { def unapply(p: Any): (? <: Int)? = null } // error +object Foo { val Ex(_) = null: @unchecked } \ No newline at end of file diff --git a/tests/neg/i1793-maybe.scala b/tests/neg/i1793-maybe.scala new file mode 100644 index 000000000000..cb4952e2791b --- /dev/null +++ b/tests/neg/i1793-maybe.scala @@ -0,0 +1,9 @@ +//> using options -Yexplicit-nulls +import language.experimental.magic +object Test { + import scala.ref.WeakReference + def unapply[T <: AnyVal](wr: WeakReference[T]): T? = { + val x = wr.underlying.get + if x != null then x else null // error + } +} diff --git a/tests/neg/i21841-maybe.check b/tests/neg/i21841-maybe.check new file mode 100644 index 000000000000..101175423ff8 --- /dev/null +++ b/tests/neg/i21841-maybe.check @@ -0,0 +1,6 @@ +-- [E108] Declaration Error: tests/neg/i21841-maybe.scala:22:13 -------------------------------------------------------- +22 | case v[T](l, r) => () // error + | ^^^^^^^^^^ + | (Test.Expr[Test.T], Test.Expr[Test.T])? is not a valid result type of an unapplySeq method of an extractor. + | + | longer explanation available when compiling with `-explain` diff --git a/tests/neg/i21841-maybe.scala b/tests/neg/i21841-maybe.scala new file mode 100644 index 000000000000..9303c79bde66 --- /dev/null +++ b/tests/neg/i21841-maybe.scala @@ -0,0 +1,24 @@ +//> using options -Yexplicit-nulls +import language.experimental.magic +object Test { + + sealed trait T + sealed trait Arrow[A, B] + + type ArgsTo[S1, Target] <: NonEmptyTuple = S1 match { + case Arrow[a, Target] => Tuple1[Expr[a]] + case Arrow[a, b] => Expr[a] *: ArgsTo[b, Target] + } + + sealed trait Expr[S] : + def unapplySeq[Target](e: Expr[Target]): ArgsTo[S, Target]? = ??? + + case class Variable[S](id: String) extends Expr[S] + + val v = Variable[Arrow[T, Arrow[T, T]]]("v") + val e : Expr[T] = ??? + + e match + case v[T](l, r) => () // error + case _ => () +} diff --git a/tests/neg/i2378-maybe.scala b/tests/neg/i2378-maybe.scala new file mode 100644 index 000000000000..05d0e90c7d5f --- /dev/null +++ b/tests/neg/i2378-maybe.scala @@ -0,0 +1,32 @@ +//> using options -Yexplicit-nulls +import language.experimental.magic +trait Cap + +trait Toolbox { + type Tree + + val tpd: TypedTrees + trait TypedTrees { + type Tree + } + + val Apply: ApplyImpl + + trait ApplyImpl { + def unapply(tree: Tree): (Tree, Seq[Tree])? + def unapply(tree: tpd.Tree)(using c: Cap): (tpd.Tree, Seq[tpd.Tree])? + } +} + +class Test(val tb: Toolbox) { + import tb.* + given cap: Cap = null.asInstanceOf[Cap] + + def foo(tree: Tree): Int = (tree: Any) match { + case tb.Apply(fun, args) => 3 // error: ambiguous overload of unapply + } + + def bar(tree: tpd.Tree): Int = (tree: Any) match { + case Apply(fun, args) => 3 // error: ambiguous overload of unapply + } +} diff --git a/tests/neg/i24168-maybe.scala b/tests/neg/i24168-maybe.scala new file mode 100644 index 000000000000..bb792a4c470e --- /dev/null +++ b/tests/neg/i24168-maybe.scala @@ -0,0 +1,14 @@ +//> using options -Yexplicit-nulls +import language.experimental.magic +trait Generic extends Selectable: + def applyDynamic(name: String)(args: Any*): Any = () + +val foo: Generic { + def unapply(x: Int): Unit? +} = new Generic: + def unapply(x: Int): Unit? = () + +def x = + 42 match + case foo(()) => println("lol") // error + diff --git a/tests/neg/i8530-b-maybe.check b/tests/neg/i8530-b-maybe.check new file mode 100644 index 000000000000..a45bf861440f --- /dev/null +++ b/tests/neg/i8530-b-maybe.check @@ -0,0 +1,7 @@ +-- [E007] Type Mismatch Error: tests/neg/i8530-b-maybe.scala:10:25 ----------------------------------------------------- +10 | case Some(xs) => xs // error + | ^^ + | Found: (xs : List[String | Null]) + | Required: List[String]? + | + | longer explanation available when compiling with `-explain` diff --git a/tests/neg/i8530-b-maybe.scala b/tests/neg/i8530-b-maybe.scala new file mode 100644 index 000000000000..14b38813004f --- /dev/null +++ b/tests/neg/i8530-b-maybe.scala @@ -0,0 +1,11 @@ +//> using options -Yexplicit-nulls +import language.experimental.magic +import scala.compiletime.erasedValue + +class MyRegex[Pattern <: String & Singleton/*Literal constant*/]: + inline def unapplySeq(s: CharSequence): List[String]? = + inline erasedValue[Pattern] match + case "foo" => if s == "foo" then Nil else null + case _ => valueOf[Pattern].r.unapplySeq(s) match + case Some(xs) => xs // error + case None => null diff --git a/tests/neg/i8894-maybe.scala b/tests/neg/i8894-maybe.scala new file mode 100644 index 000000000000..7479b04ea265 --- /dev/null +++ b/tests/neg/i8894-maybe.scala @@ -0,0 +1,15 @@ +//> using options -Yexplicit-nulls +import language.experimental.magic +trait Extractor { + inline def unapplySeq(inline tn: String): Seq[String]? +} + +extension (inline sc: StringContext) transparent inline def poql: Extractor = new Extractor { + inline def unapplySeq(inline tn: String): Seq[String]? = ??? // error: Implementation restriction: nested inline methods are not supported +} + +object x { + "x" match { + case poql" $x" => x // error: Deferred inline method unapplySeq in trait Extractor cannot be invoked + } +} diff --git a/tests/neg/if-without-then.check b/tests/neg/if-without-then.check new file mode 100644 index 000000000000..bb1ed6010e94 --- /dev/null +++ b/tests/neg/if-without-then.check @@ -0,0 +1,12 @@ +-- Error: tests/neg/if-without-then.scala:5:2 -------------------------------------------------------------------------- +5 | if (true) println("yes") // error + | ^^^^^^^^^^^^^^^^^^^^^^^^ + | `if` without `then` is illegal here since no given of type scala.magic.CanErr[Unit] is available. +-- Error: tests/neg/if-without-then.scala:7:2 -------------------------------------------------------------------------- +7 | if (true) // error + | ^^^^^^^^^ + | `if` without `then` is illegal here since no given of type scala.magic.CanErr[Unit] is available. +-- Error: tests/neg/if-without-then.scala:10:2 ------------------------------------------------------------------------- +10 | if true else "error" // error + | ^^^^^^^^^^^^^^^^^^^^ + | `if` without `then` is illegal here since no given of type scala.magic.CanErr[String] is available. diff --git a/tests/neg/if-without-then.scala b/tests/neg/if-without-then.scala new file mode 100644 index 000000000000..0f331db016eb --- /dev/null +++ b/tests/neg/if-without-then.scala @@ -0,0 +1,12 @@ +//> using options -Yexplicit-nulls +import language.experimental.magic + +def foo = + if (true) println("yes") // error + + if (true) // error + println("yes") + + if true else "error" // error + + diff --git a/tests/neg/orelse-subtyping.check b/tests/neg/orelse-subtyping.check new file mode 100644 index 000000000000..392f6f7ffe04 --- /dev/null +++ b/tests/neg/orelse-subtyping.check @@ -0,0 +1,9 @@ +-- [E007] Type Mismatch Error: tests/neg/orelse-subtyping.scala:6:25 --------------------------------------------------- +6 |val x: String ? String = null // error + | ^^^^ + | Found: Null + | Required: String ? String + | Note that implicit conversions were not tried because the result of an implicit conversion + | must be more specific than String ? String + | + | longer explanation available when compiling with `-explain` diff --git a/tests/neg/orelse-subtyping.scala b/tests/neg/orelse-subtyping.scala new file mode 100644 index 000000000000..02135564212f --- /dev/null +++ b/tests/neg/orelse-subtyping.scala @@ -0,0 +1,13 @@ +//> using options -Yexplicit-nulls + +import language.experimental.magic +import scala.magic.* + +val x: String ? String = null // error + +val y: String ? Unit = null // ok + +def foo[E](x: String ? E): String ? E = x + +val z = foo(null) +val _: String? = z // Unit is inferred diff --git a/tests/neg/patmat2-maybe.scala b/tests/neg/patmat2-maybe.scala new file mode 100644 index 000000000000..b93d9013be6d --- /dev/null +++ b/tests/neg/patmat2-maybe.scala @@ -0,0 +1,36 @@ +//> using options -Yexplicit-nulls +import language.experimental.magic +import java.io.IOException +import java.lang.NullPointerException +import java.lang.IllegalArgumentException + +object IAE { + def unapply(e: Exception): String? = + if e.isInstanceOf[IllegalArgumentException] then e.getMessage + else null +} + +object EX extends Exception + +trait ExceptionTrait extends Exception + +object Test { + def main(args: Array[String]): Unit = { + var a: Int = 1 + try { + throw new IllegalArgumentException() + } catch { + case e: IOException if e.getMessage == null => + case e: NullPointerException => + case e: IndexOutOfBoundsException => + case _: NoSuchElementException => + case _: ExceptionTrait => + case _: NoSuchElementException if a <= 1 => + case _: NullPointerException | _:IOException => + case e: Int => // error: unrelated + case EX => + case IAE(msg) => + case e: IllegalArgumentException => + } + } +} diff --git a/tests/neg/refutable-pattern-binding-messages-maybe.check b/tests/neg/refutable-pattern-binding-messages-maybe.check new file mode 100644 index 000000000000..309d29dc2a3b --- /dev/null +++ b/tests/neg/refutable-pattern-binding-messages-maybe.check @@ -0,0 +1,48 @@ +-- Error: tests/neg/refutable-pattern-binding-messages-maybe.scala:6:14 ------------------------------------------------ +6 | val Positive(p) = 5 // error: refutable extractor + | ^^^^^^^^^^^^^^^ + | pattern binding uses refutable extractor `Test.Positive` + | + | If this usage is intentional, this can be communicated by adding `.runtimeChecked` after the expression, + | which may result in a MatchError at runtime. + | This patch can be rewritten automatically under -rewrite -source 3.8-migration. +-- Error: tests/neg/refutable-pattern-binding-messages-maybe.scala:7:14 ------------------------------------------------ +7 | for Positive(i) <- List(1, 2, 3) do () // error: refutable extractor + | ^^^^^^^^^^^ + | pattern binding uses refutable extractor `Test.Positive` + | + | If this usage is intentional, this can be communicated by adding the `case` keyword before the full pattern, + | which will result in a filtering for expression (using `withFilter`). + | This patch can be rewritten automatically under -rewrite -source 3.2-migration. +-- Error: tests/neg/refutable-pattern-binding-messages-maybe.scala:11:20 ----------------------------------------------- +11 | val i :: is = List(1, 2, 3) // error: pattern type more specialized + | ^^^^^^^^^^^^^ + | pattern's type ::[Int] is more specialized than the right hand side expression's type List[Int] + | + | If the narrowing is intentional, this can be communicated by adding `.runtimeChecked` after the expression, + | which may result in a MatchError at runtime. + | This patch can be rewritten automatically under -rewrite -source 3.8-migration. +-- Error: tests/neg/refutable-pattern-binding-messages-maybe.scala:12:11 ----------------------------------------------- +12 | for ((x: String) <- xs) do () // error: pattern type more specialized + | ^^^^^^ + | pattern's type String is more specialized than the right hand side expression's type AnyRef + | + | If the narrowing is intentional, this can be communicated by adding the `case` keyword before the full pattern, + | which will result in a filtering for expression (using `withFilter`). + | This patch can be rewritten automatically under -rewrite -source 3.2-migration. +-- Error: tests/neg/refutable-pattern-binding-messages-maybe.scala:16:13 ----------------------------------------------- +16 | for none @ None <- ys do () // error: pattern type does not match + | ^^^^ + | pattern's type None.type does not match the right hand side expression's type (x$1 : Option[?]) + | + | If the narrowing is intentional, this can be communicated by adding the `case` keyword before the full pattern, + | which will result in a filtering for expression (using `withFilter`). + | This patch can be rewritten automatically under -rewrite -source 3.2-migration. +-- Error: tests/neg/refutable-pattern-binding-messages-maybe.scala:17:10 ----------------------------------------------- +17 | val 1 = 2 // error: pattern type does not match + | ^ + | pattern's type (1 : Int) does not match the right hand side expression's type (2 : Int) + | + | If the narrowing is intentional, this can be communicated by adding `.runtimeChecked` after the expression, + | which may result in a MatchError at runtime. + | This patch can be rewritten automatically under -rewrite -source 3.8-migration. diff --git a/tests/neg/refutable-pattern-binding-messages-maybe.scala b/tests/neg/refutable-pattern-binding-messages-maybe.scala new file mode 100644 index 000000000000..0269fff29b6e --- /dev/null +++ b/tests/neg/refutable-pattern-binding-messages-maybe.scala @@ -0,0 +1,18 @@ +//> using options -Yexplicit-nulls +import language.experimental.magic +object Test { + // refutable extractor + object Positive { def unapply(i: Int): Int? = if i > 0 then i else null } + val Positive(p) = 5 // error: refutable extractor + for Positive(i) <- List(1, 2, 3) do () // error: refutable extractor + + // more specialized + val xs: List[AnyRef] = ??? + val i :: is = List(1, 2, 3) // error: pattern type more specialized + for ((x: String) <- xs) do () // error: pattern type more specialized + + // does not match + val ys: List[Option[?]] = ??? + for none @ None <- ys do () // error: pattern type does not match + val 1 = 2 // error: pattern type does not match +} diff --git a/tests/neg/t7868-maybe.scala b/tests/neg/t7868-maybe.scala new file mode 100644 index 000000000000..d57cda9f6ca4 --- /dev/null +++ b/tests/neg/t7868-maybe.scala @@ -0,0 +1,15 @@ +//> using options -Yexplicit-nulls +import language.experimental.magic +object A { + def unapply(n: Int): Int? = n + + def run = (0: Short) match { + case A(_) => // error: this case is unreachable since class Short is not a subclass of class Int + case _ => + } + + def run2 = (0: Short) match { + case x: Int => // error: this case is unreachable since class Short is not a subclass of class Int + case _ => + } +} diff --git a/tests/neg/t8128-maybe.check b/tests/neg/t8128-maybe.check new file mode 100644 index 000000000000..1c2467c64dec --- /dev/null +++ b/tests/neg/t8128-maybe.check @@ -0,0 +1,4 @@ +-- Error: tests/neg/t8128-maybe.scala:6:23 ----------------------------------------------------------------------------- +6 | def unapply(m: Any): Maybe[?, Unit] = Ok("") // error + | ^^^^^^^^^^^^^^ + | Maybe type may not contain wildcard arguments diff --git a/tests/neg/t8128-maybe.scala b/tests/neg/t8128-maybe.scala new file mode 100644 index 000000000000..6340658af1ce --- /dev/null +++ b/tests/neg/t8128-maybe.scala @@ -0,0 +1,8 @@ +//> using options -Yexplicit-nulls +import language.experimental.magic +import scala.magic.* +import compiletime.Maybe +object G { + def unapply(m: Any): Maybe[?, Unit] = Ok("") // error +} + diff --git a/tests/neg/unchecked-patterns-maybe.scala b/tests/neg/unchecked-patterns-maybe.scala new file mode 100644 index 000000000000..ed14f24082a8 --- /dev/null +++ b/tests/neg/unchecked-patterns-maybe.scala @@ -0,0 +1,28 @@ +//> using options -Werror -Yexplicit-nulls + +import language.experimental.magic +object Test { + + val (y1: Some[Int]) = Some(1): Option[Int] @unchecked // OK + val y2: Some[Int] @unchecked = Some(1): Option[Int] // error + + val x :: xs = List(1, 2, 3) // error + val (1, c) = (1, 2) // error + val 1 *: cs = 1 *: Tuple() // error + + val (_: Int | _: AnyRef) = ??? : AnyRef // error + + val 1 = 2 // error + + object Positive { def unapply(i: Int): Int? = if i > 0 then i else null } + object Always1 { def unapply(i: Int): Some[Int] = Some(i) } + object Pair { def unapply(t: (Int, Int)): t.type = t } + object Triple { def unapply(t: (Int, Int, Int)): (Int, Int, Int) = t } + + val Positive(p) = 5 // error + val Some(s1) = Option(1) // error + val Some(s2) = Some(1) // OK + val Always1(p1) = 5 // OK + val Pair(t1, t2) = (5, 5) // OK + val Triple(u1, u2, u3) = (5, 5, 5) // OK +} diff --git a/tests/neg/yimports-stable.check b/tests/neg/yimports-stable.check index ba87116955c2..8ffb6b04bb06 100644 --- a/tests/neg/yimports-stable.check +++ b/tests/neg/yimports-stable.check @@ -1,13 +1,13 @@ error: bad preamble import hello.world.potions -- [E006] Not Found Error: tests/neg/yimports-stable/C_2.scala:4:9 ----------------------------------------------------- -4 | val v: Numb = magic // error // error +4 | val v: Numb = magix // error // error | ^^^^ | Not found: type Numb - did you mean Null? | | longer explanation available when compiling with `-explain` -- [E006] Not Found Error: tests/neg/yimports-stable/C_2.scala:4:16 ---------------------------------------------------- -4 | val v: Numb = magic // error // error +4 | val v: Numb = magix // error // error | ^^^^^ - | Not found: magic - did you mean main? + | Not found: magix - did you mean main? | | longer explanation available when compiling with `-explain` diff --git a/tests/neg/yimports-stable/C_2.scala b/tests/neg/yimports-stable/C_2.scala index 6cd8c17f6620..3343400e446b 100644 --- a/tests/neg/yimports-stable/C_2.scala +++ b/tests/neg/yimports-stable/C_2.scala @@ -1,7 +1,7 @@ //> using options -Yimports:scala,scala.Predef,hello.world.potions // class C { - val v: Numb = magic // error // error + val v: Numb = magix // error // error def greet() = println("hello, world!") } // nopos-error diff --git a/tests/neg/yimports-stable/minidef_1.scala b/tests/neg/yimports-stable/minidef_1.scala index b3ea7445df24..97e3944e34c8 100644 --- a/tests/neg/yimports-stable/minidef_1.scala +++ b/tests/neg/yimports-stable/minidef_1.scala @@ -3,7 +3,7 @@ package hello trait stuff { type Numb = Int - val magic = 42 + val magix = 42 } object world { diff --git a/tests/new/test.scala b/tests/new/test.scala index 3dc239d18b14..cd280dd87364 100644 --- a/tests/new/test.scala +++ b/tests/new/test.scala @@ -1,16 +1,16 @@ -mport language.experimental.erasedDefinitions +import language.experimental.magic +import language.future +import scala.magic.* +import scala.util.Either -class CanSerialize[T] +def toEither[T, E](x: T ? E): Either[E, T] = x match + case Ok(y) => Right(y) + case Err(e) => Left(e) -inline given CanSerialize[String] = CanSerialize() -inline given [T: CanSerialize] => CanSerialize[List[T]] = CanSerialize() -def safeWriteObject[T <: java.io.Serializable](out: java.io.ObjectOutputStream, x: T)(using erased CanSerialize[T]) = - out.writeObject(x) +object Extract: + def unapply[T](x: T): T ? String = Ok(x) -def writeList[T](out: java.io.ObjectOutputStream, xs: List[T])(using erased CanSerialize[T]) = - safeWriteObject(out, xs) - -@main def Test(out: java.io.ObjectOutputStream) = - writeList(out, List("a", "b")) // ok - writeList(out, List[Int => Int](x => x + 1, y => y * 2)) // error \ No newline at end of file +@main def Test = 22 match + case Extract(s) => + if (true) println(s) diff --git a/tests/patmat/i2363-maybe.check b/tests/patmat/i2363-maybe.check new file mode 100644 index 000000000000..2372f149ba18 --- /dev/null +++ b/tests/patmat/i2363-maybe.check @@ -0,0 +1,16 @@ +-- [E029] Pattern Match Exhaustivity Warning: tests/patmat/i2363-maybe.scala:17:32 +17 | def foo(x: List[Expr]): Int = x match { + | ^ + | match may not be exhaustive. + | + | It would fail on pattern case: List(_, _*) + | + | longer explanation available when compiling with `-explain` +-- [E029] Pattern Match Exhaustivity Warning: tests/patmat/i2363-maybe.scala:23:26 +23 | def bar(x: Expr): Int = x match { + | ^ + | match may not be exhaustive. + | + | It would fail on pattern case: _: IntExpr, _: BooleanExpr + | + | longer explanation available when compiling with `-explain` diff --git a/tests/patmat/i2363-maybe.scala b/tests/patmat/i2363-maybe.scala new file mode 100644 index 000000000000..374450427c4f --- /dev/null +++ b/tests/patmat/i2363-maybe.scala @@ -0,0 +1,27 @@ +//> using options -Yexplicit-nulls +import language.experimental.magic +sealed trait Expr +class IntExpr extends Expr +class BooleanExpr extends Expr + +object IntExpr { + def unapply(expr: Expr): IntExpr? = ??? +} + +object BooleanExpr { + def unapply(expr: Expr): BooleanExpr? = ??? +} + + +class Test { + def foo(x: List[Expr]): Int = x match { + case IntExpr(_) :: xs => 1 + case BooleanExpr(_) :: xs => 1 + case Nil => 2 + } + + def bar(x: Expr): Int = x match { + case IntExpr(_) => 1 + case BooleanExpr(_) => 2 + } +} diff --git a/tests/patmat/optionless-maybe.check b/tests/patmat/optionless-maybe.check new file mode 100644 index 000000000000..6293a09e2e0d --- /dev/null +++ b/tests/patmat/optionless-maybe.check @@ -0,0 +1,8 @@ +-- [E029] Pattern Match Exhaustivity Warning: tests/patmat/optionless-maybe.scala:30:41 +30 | def qux(t: Tree)(using c: Cap): Unit = t match { + | ^ + | match may not be exhaustive. + | + | It would fail on pattern case: Ident(_) + | + | longer explanation available when compiling with `-explain` diff --git a/tests/patmat/optionless-maybe.scala b/tests/patmat/optionless-maybe.scala new file mode 100644 index 000000000000..29ef2c9de7c7 --- /dev/null +++ b/tests/patmat/optionless-maybe.scala @@ -0,0 +1,34 @@ +//> using options -Yexplicit-nulls +import language.experimental.magic +sealed trait Tree +case class Ident(name: String) extends Tree + +object Ident1 { + def unapply(tree: Tree): Ident = ??? +} + +trait Cap +object Ident2 { + def unapply(tree: Tree)(using any: Cap): Ident = ??? +} + +object Ident3 { + def unapply(tree: Tree)(using any: Cap): Ident? = ??? +} + + + +class Test { + def foo(t: Tree): Unit = t match { + case Ident1(t) => + } + + def bar(t: Tree)(using c: Cap): Unit = t match { + case Ident2(t) => + } + + def qux(t: Tree)(using c: Cap): Unit = t match { + case Ident3(t) => + } + +} \ No newline at end of file diff --git a/tests/pending/pos-custom-args/captures/i26586.scala b/tests/pending/pos-custom-args/captures/i26586.scala new file mode 100644 index 000000000000..6ad9a2e77626 --- /dev/null +++ b/tests/pending/pos-custom-args/captures/i26586.scala @@ -0,0 +1,18 @@ +import caps.* + +class Ref extends Mutable: + private var i = 0 + def get: Int = i + update def put(x: Int): Unit = i = x + +class A0[CS^] + +class A1 + +def test0(): Unit = + val r: Ref^ = Ref() + val a: A0[{r}]^{r} = A0[{r}] // error + +def test1(): Unit = + val r: Ref^ = Ref() + val a: A1^{r} = A1() // ok! \ No newline at end of file diff --git a/tests/pos-custom-args/captures/i24729-maybe.scala b/tests/pos-custom-args/captures/i24729-maybe.scala new file mode 100644 index 000000000000..2066a8f62a6a --- /dev/null +++ b/tests/pos-custom-args/captures/i24729-maybe.scala @@ -0,0 +1,23 @@ +//> using options -Yexplicit-nulls +import language.experimental.magic +import scala.collection.SeqOps + +inline def foo[A, B](self: A, y: B): (A, B) = (self, y) +def bar[A, B](self: A, y: B): (A, B) = (self, y) + +object Test: + def test[A, CC[_] <: Seq[?], C <: SeqOps[A, CC, C]](t: (C & SeqOps[A, CC, C])^) = + foo(t.head, t.tail) + bar(t.head, t.tail) + +extension [A](self: A) inline def --> [B](y: B): (A, B) = (self, y) +extension [A](inline self: A) inline def ---> [B](inline y: B): (A, B) = (self, y) + +object +: { + def unapply[A, CC[_] <: Seq[?], C <: SeqOps[A, CC, C]](t: (C & SeqOps[A, CC, C])^): (A, C^{t})? = + if t.isEmpty then null + else t.head --> t.tail + def unapply2[A, CC[_] <: Seq[?], C <: SeqOps[A, CC, C]](t: (C & SeqOps[A, CC, C])^): Option[(A, C^{t})] = + if t.isEmpty then None + else Some(t.head ---> t.tail) +} diff --git a/tests/pos/StringContext-maybe.scala b/tests/pos/StringContext-maybe.scala new file mode 100644 index 000000000000..7217fe991450 --- /dev/null +++ b/tests/pos/StringContext-maybe.scala @@ -0,0 +1,490 @@ +//> using options -Yexplicit-nulls +/* + * Scala (https://www.scala-lang.org) + * + * Copyright EPFL and Lightbend, Inc. + * + * Licensed under Apache License 2.0 + * (http://www.apache.org/licenses/LICENSE-2.0). + * + * See the NOTICE file distributed with this work for + * additional information regarding copyright ownership. + */ + +package scala + +import language.experimental.magic +import java.lang.StringBuilder as JLSBuilder +import scala.annotation.tailrec + +/** This class provides the basic mechanism to do String Interpolation. + * String Interpolation allows users + * to embed variable references directly in *processed* string literals. + * Here's an example: + * {{{ + * val name = "James" + * println(s"Hello, \$name") // Hello, James + * }}} + * + * Any processed string literal is rewritten as an instantiation and + * method call against this class. For example: + * {{{ + * s"Hello, \$name" + * }}} + * + * is rewritten to be: + * + * {{{ + * StringContext("Hello, ", "").s(name) + * }}} + * + * By default, this class provides the `raw`, `s` and `f` methods as + * available interpolators. + * + * To provide your own string interpolator, create an implicit class + * which adds a method to `StringContext`. Here's an example: + * {{{ + * implicit class JsonHelper(private val sc: StringContext) extends AnyVal { + * def json(args: Any*): JSONObject = ... + * } + * val x: JSONObject = json"{ a: \$a }" + * }}} + * + * Here the `JsonHelper` extension class implicitly adds the `json` method to + * `StringContext` which can be used for `json` string literals. + * + * @param parts The parts that make up the interpolated string, + * without the expressions that get inserted by interpolation. + */ +case class StringContext(parts: String*) { + + import StringContext.{checkLengths as scCheckLengths, glob, standardInterpolator as scStandardInterpolator} + + @deprecated("use same-named method on StringContext companion object", "2.13.0") + def checkLengths(args: scala.collection.Seq[Any]): Unit = scCheckLengths(args, parts) + + /** The simple string interpolator. + * + * It inserts its arguments between corresponding parts of the string context. + * It also treats standard escape sequences as defined in the Scala specification. + * Here's an example of usage: + * {{{ + * val name = "James" + * println(s"Hello, \$name") // Hello, James + * }}} + * In this example, the expression \$name is replaced with the `toString` of the + * variable `name`. + * The `s` interpolator can take the `toString` of any arbitrary expression within + * a `\${}` block, for example: + * {{{ + * println(s"1 + 1 = \${1 + 1}") + * }}} + * will print the string `1 + 1 = 2`. + * + * @param `args` The arguments to be inserted into the resulting string. + * @throws IllegalArgumentException + * if the number of `parts` in the enclosing `StringContext` does not exceed + * the number of arguments `arg` by exactly 1. + * @throws StringContext.InvalidEscapeException + * if a `parts` string contains a backslash (`\`) character + * that does not start a valid escape sequence. + * @note The Scala compiler may replace a call to this method with an equivalent, but more efficient, + * use of a StringBuilder. + */ + def s(args: Any*): String = ??? // fasttracked to scala.tools.reflect.FastStringInterpolator::interpolateS + object s { + /** The simple string matcher. + * + * Attempts to match the input string to the given interpolated patterns via + * a naive globbing, that is the reverse of the simple interpolator. + * + * Here is an example usage: + * + * {{{ + * val s"Hello, \$name" = "Hello, James" + * println(name) // "James" + * }}} + * + * In this example, the string "James" ends up matching the location where the pattern + * `\$name` is positioned, and thus ends up bound to that variable. + * + * Multiple matches are supported: + * + * {{{ + * val s"\$greeting, \$name" = "Hello, James" + * println(greeting) // "Hello" + * println(name) // "James" + * }}} + * + * And the `s` matcher can match an arbitrary pattern within the `\${}` block, for example: + * + * {{{ + * val TimeSplitter = "([0-9]+)[.:]([0-9]+)".r + * val s"The time is \${TimeSplitter(hours, mins)}" = "The time is 10.50" + * println(hours) // 10 + * println(mins) // 50 + * }}} + * + * Here, we use the `TimeSplitter` regex within the `s` matcher, further splitting the + * matched string "10.50" into its constituent parts + */ + def unapplySeq(s: String): Seq[String]? = glob(parts, s) + } + /** The raw string interpolator. + * + * It inserts its arguments between corresponding parts of the string context. + * As opposed to the simple string interpolator `s`, this one does not treat + * standard escape sequences as defined in the Scala specification. + * + * For example, the raw processed string `raw"a\nb"` is equal to the scala string `"a\\nb"`. + * + * ''Note:'' Even when using the raw interpolator, Scala will process Unicode escapes. + * Unicode processing in the raw interpolator is deprecated as of scala 2.13.2 and + * will be removed in the future + * For example: + * {{{ + * scala> raw"\u005cu0023" + * res0: String = # + * }}} + * + * @param `args` The arguments to be inserted into the resulting string. + * @throws IllegalArgumentException + * if the number of `parts` in the enclosing `StringContext` does not exceed + * the number of arguments `arg` by exactly 1. + * @note The Scala compiler may replace a call to this method with an equivalent, but more efficient, + * use of a StringBuilder. + */ + def raw(args: Any*): String = ??? // fasttracked to scala.tools.reflect.FastStringInterpolator::interpolateRaw + + @deprecated("Use the static method StringContext.standardInterpolator instead of the instance method", "2.13.0") + def standardInterpolator(process: String => String, args: Seq[Any]): String = scStandardInterpolator(process, args, parts) + + /** The formatted string interpolator. + * + * It inserts its arguments between corresponding parts of the string context. + * It also treats standard escape sequences as defined in the Scala specification. + * Finally, if an interpolated expression is followed by a `parts` string + * that starts with a formatting specifier, the expression is formatted according to that + * specifier. All specifiers allowed in Java format strings are handled, and in the same + * way they are treated in Java. + * + * For example: + * {{{ + * val height = 1.9d + * val name = "James" + * println(f"\$name%s is \$height%2.2f meters tall") // James is 1.90 meters tall + * }}} + * + * @param `args` The arguments to be inserted into the resulting string. + * @throws IllegalArgumentException + * if the number of `parts` in the enclosing `StringContext` does not exceed + * the number of arguments `arg` by exactly 1. + * @throws StringContext.InvalidEscapeException + * if a `parts` string contains a backslash (`\`) character + * that does not start a valid escape sequence. + * + * Note: The `f` method works by assembling a format string from all the `parts` strings and using + * `java.lang.String.format` to format all arguments with that format string. The format string is + * obtained by concatenating all `parts` strings, and performing two transformations: + * + * 1. Let a _formatting position_ be a start of any `parts` string except the first one. + * If a formatting position does not refer to a `%` character (which is assumed to + * start a format specifier), then the string format specifier `%s` is inserted. + * + * 2. Any `%` characters not in formatting positions must begin one of the conversions + * `%%` (the literal percent) or `%n` (the platform-specific line separator). + */ + def f[A >: Any](args: A*): String = ??? // fasttracked to scala.tools.reflect.FormatInterpolator::interpolateF +} + +object StringContext { + /** + * Linear time glob-matching implementation. + * Adapted from https://research.swtch.com/glob + * + * @param patternChunks The non-wildcard portions of the input pattern, + * separated by wildcards + * @param input The input you wish to match against + * @return None if there is no match, Some containing the sequence of matched + * wildcard strings if there is a match + */ + def glob(patternChunks: Seq[String], input: String): Seq[String]? = { + var patternIndex = 0 + var inputIndex = 0 + var nextPatternIndex = 0 + var nextInputIndex = 0 + + val numWildcards = patternChunks.length - 1 + val matchStarts = Array.fill(numWildcards)(-1) + val matchEnds = Array.fill(numWildcards)(-1) + + val nameLength = input.length + // The final pattern is as long as all the chunks, separated by 1-character + // glob-wildcard placeholders + val patternLength = { + var n = numWildcards + for chunk <- patternChunks do { + n += chunk.length + } + n + } + + // Convert the input pattern chunks into a single sequence of shorts; each + // non-negative short represents a character, while -1 represents a glob wildcard + val pattern = { + val arr = new Array[Short](patternLength) + var i = 0 + var first = true + for chunk <- patternChunks do { + if first then first = false + else { + arr(i) = -1 + i += 1 + } + for c <- chunk do { + arr(i) = c.toShort + i += 1 + } + } + arr + } + + // Lookup table for each character in the pattern to check whether or not + // it refers to a glob wildcard; a non-negative integer indicates which + // glob wildcard it represents, while -1 means it doesn't represent any + val matchIndices = { + val arr = Array.fill(patternLength + 1)(-1) + var i = 0 + var j = 0 + for chunk <- patternChunks do { + if j < numWildcards then { + i += chunk.length + arr(i) = j + i += 1 + j += 1 + } + } + arr + } + + while patternIndex < patternLength || inputIndex < nameLength do { + matchIndices(patternIndex) match { + case -1 => // do nothing + case n => + matchStarts(n) = matchStarts(n) match { + case -1 => inputIndex + case s => math.min(s, inputIndex) + } + matchEnds(n) = matchEnds(n) match { + case -1 => inputIndex + case s => math.max(s, inputIndex) + } + } + + val continue = if patternIndex < patternLength then { + val c = pattern(patternIndex) + c match { + case -1 => // zero-or-more-character wildcard + // Try to match at nx. If that doesn't work out, restart at nx+1 next. + nextPatternIndex = patternIndex + nextInputIndex = inputIndex + 1 + patternIndex += 1 + true + case _ => // ordinary character + if inputIndex < nameLength && input(inputIndex) == c then { + patternIndex += 1 + inputIndex += 1 + true + } else { + false + } + } + } else false + + // Mismatch. Maybe restart. + if !continue then { + if 0 < nextInputIndex && nextInputIndex <= nameLength then { + patternIndex = nextPatternIndex + inputIndex = nextInputIndex + } else { + return null + } + } + } + + // Matched all of pattern to all of name. Success. + collection.immutable.ArraySeq.unsafeWrapArray( + Array.tabulate(patternChunks.length - 1)(n => input.slice(matchStarts(n), matchEnds(n))) + ) + } + + /** An exception that is thrown if a string contains a backslash (`\`) character + * that does not start a valid escape sequence. + * @param str The offending string + * @param index The index of the offending backslash character in `str`. + */ + class InvalidEscapeException(str: String, val index: Int) extends IllegalArgumentException( + s"""invalid escape ${ + require(index >= 0 && index < str.length) + val ok = s"""[\\b, \\t, \\n, \\f, \\r, \\\\, \\", \\', \\uxxxx]""" + if index == str.length - 1 then "at terminal" else s"'\\${str(index + 1)}' not one of $ok at" + } index $index in "$str". Use \\\\ for literal \\.""" + ) + + protected[scala] class InvalidUnicodeEscapeException(str: String, val escapeStart: Int, val index: Int) extends IllegalArgumentException( + s"""invalid unicode escape at index $index of $str""" + ) + + private def readUEscape(src: String, startindex: Int): (Char, Int) = { + val len = src.length() + def loop(uindex: Int): (Char, Int) = { + def loopCP(dindex: Int, codepoint: Int): (Char, Int) = { + //supports BMP + surrogate escapes + //but only in four hex-digit code units (uxxxx) + if dindex >= 4 then { + val usRead = uindex - startindex + val digitsRead = dindex + (codepoint.asInstanceOf[Char], usRead + digitsRead) + } + else if dindex + uindex >= len then + throw new InvalidUnicodeEscapeException(src, startindex, uindex + dindex) + else { + val ch = src(dindex + uindex) + val e = ch.asDigit + if e >= 0 && e <= 15 then loopCP(dindex + 1, (codepoint << 4) + e) + else throw new InvalidUnicodeEscapeException(src, startindex, uindex + dindex) + } + } + if uindex >= len then throw new InvalidUnicodeEscapeException(src, startindex, uindex - 1) + //allow one or more `u` characters between the + //backslash and the code unit + else if src(uindex) == 'u' then loop(uindex + 1) + else loopCP(0, 0) + } + loop(startindex) + } + + /** Expands standard Scala escape sequences in a string. + * Escape sequences are: + * control: `\b`, `\t`, `\n`, `\f`, `\r` + * escape: `\\`, `\"`, `\'` + * + * @param str A string that may contain escape sequences + * @return The string with all escape sequences expanded. + */ + @deprecated("use processEscapes", "2.13.0") + def treatEscapes(str: String): String = processEscapes(str) + + /** Expands standard Scala escape sequences in a string. + * Escape sequences are: + * control: `\b`, `\t`, `\n`, `\f`, `\r` + * escape: `\\`, `\"`, `\'` + * + * @param str A string that may contain escape sequences + * @return The string with all escape sequences expanded. + */ + def processEscapes(str: String): String = + str.indexOf('\\') match { + case -1 => str + case i => replace(str, i) + } + + protected[scala] def processUnicode(str: String): String = + str.indexOf("\\") match { + case i if i == -1 || i >= (str.length() - 5) => str + case i => replaceU(str, i) + } + + //replace escapes with given first escape + private def replace(str: String, first: Int): String = { + val len = str.length() + val b = new JLSBuilder + // append replacement starting at index `i`, with `next` backslash + @tailrec def loop(i: Int, next: Int): String = { + if next >= 0 then { + //require(str(next) == '\\') + if next > i then b.append(str, i, next) + var idx = next + 1 + if idx >= len then throw new InvalidEscapeException(str, next) + val c = str(idx) match { + case 'u' => 'u' + case 'b' => '\b' + case 't' => '\t' + case 'n' => '\n' + case 'f' => '\f' + case 'r' => '\r' + case '"' => '"' + case '\'' => '\'' + case '\\' => '\\' + case _ => throw new InvalidEscapeException(str, next) + } + val (ch, advance) = if c == 'u' then readUEscape(str, idx) + else (c, 1) + idx += advance + b.append(ch) + loop(idx, str.indexOf('\\', idx)) + } else { + if i < len then b.append(str, i, len) + b.toString + } + } + loop(0, first) + } + + //replace escapes with given first escape + private def replaceU(str: String, first: Int): String = { + val len = str.length() + val b = new JLSBuilder + // append replacement starting at index `i`, with `next` backslash + @tailrec def loop(i: Int, next: Int): String = { + if next >= 0 then { + //require(str(next) == '\\') + if next > i then b.append(str, i, next) + var idx = next + 1 + if idx >= len then { + if idx == len then b.append('\\') + b.toString() + } + else { + val (ch, advance) = str(idx) match { + case 'u' => readUEscape(str, idx) + case chr => { + b.append('\\') + (chr, 1) + } + } + idx += advance + b.append(ch) + loop(idx, str.indexOf('\\', idx)) + } + } else { + if i < len then b.append(str, i, len) + b.toString() + } + } + loop(0, first) + } + + def standardInterpolator(process: String => String, args: scala.collection.Seq[Any], parts: Seq[String]): String = { + StringContext.checkLengths(args, parts) + val pi = parts.iterator + val ai = args.iterator + val bldr = new JLSBuilder(process(pi.next())) + while ai.hasNext do { + bldr.append(ai.next()) + bldr.append(process(pi.next())) + } + bldr.toString + } + + /** Checks that the length of the given argument `args` is one less than the number + * of `parts` supplied to the `StringContext`. + * + * @throws IllegalArgumentException if this is not the case. + */ + def checkLengths(args: scala.collection.Seq[Any], parts: Seq[String]): Unit = + if parts.length != args.length + 1 then + throw new IllegalArgumentException("wrong number of arguments ("+ args.length + +") for interpolated string with "+ parts.length +" parts") + +} diff --git a/tests/pos/first-class-patterns-maybe.scala b/tests/pos/first-class-patterns-maybe.scala new file mode 100644 index 000000000000..5ddd5510c4bd --- /dev/null +++ b/tests/pos/first-class-patterns-maybe.scala @@ -0,0 +1,25 @@ +//> using options -Yexplicit-nulls + + // Trait of all extractors with unapply methods +import language.experimental.magic + trait Matcher[A, B]: + def unapply(x: A): B? + + // An extractor defined by an unappy method + object Even extends Matcher[Int, Int]: + def unapply(x: Int): Int? = + if x % 2 == 0 then x else null + + // Method using a given extractor in pattern position + def collect[A, B](xs: List[A], m: Matcher[A, B]): List[B] = + xs match + case Nil => Nil + case m(x) :: xs1 => x :: collect(xs1, m) + case _ :: xs1 => collect(xs1, m) + + @main def test = + val xs = List(1, 2, 3, 4) + val ys = collect(xs, Even) + println(ys) + + diff --git a/tests/pos/i17525-maybe.scala b/tests/pos/i17525-maybe.scala new file mode 100644 index 000000000000..39844e08d30f --- /dev/null +++ b/tests/pos/i17525-maybe.scala @@ -0,0 +1,7 @@ +//> using options -Yexplicit-nulls +import language.experimental.magic +object Extract { + transparent inline def unapply(value: String): Tuple? = (1, "two") +} +def fail(): Unit = "" match { case Extract(a, b) => f(a, b) } +def f(n: Int, s: String): Unit = () diff --git a/tests/pos/i1793-maybe.scala b/tests/pos/i1793-maybe.scala new file mode 100644 index 000000000000..f55c9cece97c --- /dev/null +++ b/tests/pos/i1793-maybe.scala @@ -0,0 +1,9 @@ +//> using options -Yexplicit-nulls +import language.experimental.magic +object Test { + import scala.ref.WeakReference + def unapply[T <: AnyRef](wr: WeakReference[T]): T? = { + val x: T = wr.underlying.get + if x != null then x else null + } +} diff --git a/tests/pos/i18175-maybe.scala b/tests/pos/i18175-maybe.scala new file mode 100644 index 000000000000..08bda78ac292 --- /dev/null +++ b/tests/pos/i18175-maybe.scala @@ -0,0 +1,108 @@ +//> using options -Yexplicit-nulls +import language.experimental.magic +import scala.compiletime.ops.int.{ +, -, Max } +import scala.compiletime.ops.string.{ Substring, Length, Matches, CharAt } + +class Regex[P] private() extends Serializable: + def unapply(s: CharSequence)(using n: Regex.Sanitizer[P]): P? = ??? + +object Regex: + def apply[R <: String & Singleton](regex: R): Regex[Compile[R]] = ??? + + abstract class Sanitizer[T] + object Sanitizer: + given Sanitizer[EmptyTuple] = ??? + given stringcase: [T <: Tuple: Sanitizer] => Sanitizer[String *: T] = ??? + given optioncase: [T <: Tuple: Sanitizer] => Sanitizer[Option[String] *: T] = ??? + given Sanitizer[String] = ??? + given Sanitizer[Option[String]] = ??? + + type Compile[R <: String] = Matches["", R] match + case _ => Reverse[EmptyTuple, Loop[R, 0, Length[R], EmptyTuple, IsPiped[R, 0, Length[R], 0]]] + + type Loop[R <: String, Lo <: Int, Hi <: Int, Acc <: Tuple, Opt <: Int] <: Tuple = Lo match + case Hi => Acc + case _ => CharAt[R, Lo] match + case '\\' => CharAt[R, Lo + 1] match + case 'Q' => Loop[R, ToClosingQE[R, Lo + 2], Hi, Acc, Opt] + case _ => Loop[R, Lo + 2, Hi, Acc, Opt] + case '[' => Loop[R, ToClosingBracket[R, Lo + 1, 0], Hi, Acc, Opt] + case ')' => Loop[R, Lo + 1, Hi, Acc, Max[0, Opt - 1]] + case '(' => Opt match + case 0 => IsMarked[R, ToClosingParenthesis[R, Lo + 1, 0], Hi] match + case true => IsCapturing[R, Lo + 1] match + case false => Loop[R, Lo + 1, Hi, Acc, 1] + case true => Loop[R, Lo + 1, Hi, Option[String] *: Acc, 1] + case false => IsCapturing[R, Lo + 1] match + case false => Loop[R, Lo + 1, Hi, Acc, IsPiped[R, Lo + 1, Hi, 0]] + case true => Loop[R, Lo + 1, Hi, String *: Acc, IsPiped[R, Lo + 1, Hi, 0]] + case _ => IsCapturing[R, Lo + 1] match + case false => Loop[R, Lo + 1, Hi, Acc, Opt + 1] + case true => Loop[R, Lo + 1, Hi, Option[String] *: Acc, Opt + 1] + case _ => Loop[R, Lo + 1, Hi, Acc, Opt] + + type IsCapturing[R <: String, At <: Int] <: Boolean = CharAt[R, At] match + case '?' => CharAt[R, At + 1] match + case '<' => CharAt[R, At + 2] match + case '=' | '!' => false + case _ => true + case _ => false + case _ => true + + type IsMarked[R <: String, At <: Int, Hi <: Int] <: Boolean = At match + case Hi => false + case _ => CharAt[R, At] match + case '?' | '*' => true + case '{' => CharAt[R, At + 1] match + case '0' => true + case _ => false + case _ => false + + type IsPiped[R <: String, At <: Int, Hi <: Int, Lvl <: Int] <: Int = At match + case Hi => 0 + case _ => CharAt[R, At] match + case '\\' => CharAt[R, At + 1] match + case 'Q' => IsPiped[R, ToClosingQE[R, At + 2], Hi, Lvl] + case _ => IsPiped[R, At + 2, Hi, Lvl] + case '[' => IsPiped[R, ToClosingBracket[R, At + 1, 0], Hi, Lvl] + case '(' => IsPiped[R, ToClosingParenthesis[R, At + 1, 0], Hi, Lvl + 1] + case '|' => 1 + case ')' => 0 + case _ => IsPiped[R, At + 1, Hi, Lvl] + + type ToClosingParenthesis[R <: String, At <: Int, Lvl <: Int] <: Int = CharAt[R, At] match + case '\\' => CharAt[R, At + 1] match + case 'Q' => ToClosingParenthesis[R, ToClosingQE[R, At + 2], Lvl] + case _ => ToClosingParenthesis[R, At + 2, Lvl] + case '[' => ToClosingParenthesis[R, ToClosingBracket[R, At + 1, 0], Lvl] + case ')' => Lvl match + case 0 => At + 1 + case _ => ToClosingParenthesis[R, At + 1, Lvl - 1] + case '(' => ToClosingParenthesis[R, At + 1, Lvl + 1] + case _ => ToClosingParenthesis[R, At + 1, Lvl] + + type ToClosingBracket[R <: String, At <: Int, Lvl <: Int] <: Int = CharAt[R, At] match + case '\\' => CharAt[R, At + 1] match + case 'Q' => ToClosingBracket[R, ToClosingQE[R, At + 2], Lvl] + case _ => ToClosingBracket[R, At + 2, Lvl] + case '[' => ToClosingBracket[R, At + 1, Lvl + 1] + case ']' => Lvl match + case 0 => At + 1 + case _ => ToClosingBracket[R, At + 1, Lvl - 1] + case _ => ToClosingBracket[R, At + 1, Lvl] + + type ToClosingQE[R <: String, At <: Int] <: Int = CharAt[R, At] match + case '\\' => CharAt[R, At + 1] match + case 'E' => At + 2 + case _ => ToClosingQE[R, At + 2] + case _ => ToClosingQE[R, At + 1] + + type Reverse[Acc <: Tuple, X <: Tuple] <: Tuple = X match + case x *: xs => Reverse[x *: Acc, xs] + case EmptyTuple => Acc + +object Test: + def main(args: Array[String]): Unit = + val r75 = Regex("(x|y|z[QW])*(longish|loquatious|excessive|overblown[QW])*") + "xyzQzWlongishoverblownW" match + case r75((Some(g0), Some(g1))) => ??? // failure diff --git a/tests/pos/i18601-maybe.scala b/tests/pos/i18601-maybe.scala new file mode 100644 index 000000000000..4ddfcf339151 --- /dev/null +++ b/tests/pos/i18601-maybe.scala @@ -0,0 +1,20 @@ +//> using options -Werror -Yexplicit-nulls +import language.experimental.magic +extension (sc: StringContext) + def m: StringContext = sc + def unapply(string: String): String? = + val pattern = sc.parts.head + if string.length == pattern.length then string else null + +class Test: + def parse(x: PartialFunction[String, String]) = x + + val pf = parse { + case m"x$s" => s + case m"xx$s" => s // was: unreachable + } + + // proof that the second case isn't unreachable (matches "ab") + def t1 = pf.applyOrElse("a", _ => ".") // "a" + def t2 = pf.applyOrElse("ab", _ => ".") // "ab" + def t3 = pf.applyOrElse("abc", _ => ".") // "." diff --git a/tests/pos/i20107-maybe.scala b/tests/pos/i20107-maybe.scala new file mode 100644 index 000000000000..f13204cef044 --- /dev/null +++ b/tests/pos/i20107-maybe.scala @@ -0,0 +1,9 @@ +//> using options -Yexplicit-nulls +import language.experimental.magic +import scala.magic.* +object foo: + transparent inline def unapply[F](e: F): F? = Ok(e.asInstanceOf[F]) + +class A: + def test(x: Int) = x match + case foo(e) => e diff --git a/tests/pos/i25663-maybe.scala b/tests/pos/i25663-maybe.scala new file mode 100644 index 000000000000..139ce5731905 --- /dev/null +++ b/tests/pos/i25663-maybe.scala @@ -0,0 +1,7 @@ +//> using options -Yexplicit-nulls +import language.experimental.magic +object Foo: + def unapplySeq(f: Int): (String *: Seq[Int] *: EmptyTuple)? = ??? + +def foo(f: Int) = f match + case Foo(name, ns*) => ??? diff --git a/tests/pos/i8083-maybe.scala b/tests/pos/i8083-maybe.scala new file mode 100644 index 000000000000..775bcbc675c9 --- /dev/null +++ b/tests/pos/i8083-maybe.scala @@ -0,0 +1,28 @@ +//> using options -Yexplicit-nulls +import language.experimental.magic +trait A { + class B +} +abstract class Test2 { val a: A; val b: a.B } +object Test2 { + def unapply(that: Test2): (that.a.type, that.a.B)? = (that.a, that.b) +} +object anA extends A +object Test extends App { + val t: Test2 { val a: anA.type; val b: anA.B } = new Test2 { val a: anA.type = anA; val b = new a.B } + t match { + case Test2(u, v) => + u: A + u: t.a.type // error + v: t.a.B // error + } +} +object Test1 extends App { + object t extends Test2 { val a = anA; val b = new a.B } + t match { + case Test2(u, v) => + u: A + u: t.a.type // error + v: t.a.B // error + } +} \ No newline at end of file diff --git a/tests/pos/i8530-maybe.scala b/tests/pos/i8530-maybe.scala new file mode 100644 index 000000000000..5be76b1be05d --- /dev/null +++ b/tests/pos/i8530-maybe.scala @@ -0,0 +1,29 @@ +//> using options -Yexplicit-nulls +import language.experimental.magic +import scala.magic.* +object MyBoooleanUnapply: + inline def unapply(x: Int): Boolean = true + +object MyOptionUnapply: + inline def unapply(x: Int): Long? = x + +object MyUnapplyImplicits: + inline def unapply(x: Int)(using DummyImplicit): Long? = x + +object MyPolyUnapply: + inline def unapply[T](x: T): T? = Ok(x) + +object MySeqUnapply: + inline def unapplySeq(x: Int): Seq[Int] = Seq(x, x) + +object MyWhiteboxUnapply: + transparent inline def unapply(x: Int): Any? = Ok(x) + +def test: Unit = + val x = 5 match + case MyBoooleanUnapply() => + case MyOptionUnapply(y) => y: Long + case MyUnapplyImplicits(y) => y: Long + case MyPolyUnapply(a) => a: Int + case MySeqUnapply(a, b) => (a: Int, b: Int) + case MyWhiteboxUnapply(x) => x: Int diff --git a/tests/pos/i8577-maybe.scala b/tests/pos/i8577-maybe.scala new file mode 100644 index 000000000000..30576b9162ba --- /dev/null +++ b/tests/pos/i8577-maybe.scala @@ -0,0 +1,23 @@ +//> using options -Yexplicit-nulls +package i8577 + +import language.experimental.magic +type A; given A: A = ???; +type B; given B: B = ???; +type C; given C: C = ???; +type D; given D: D = ???; +type E; given E: E = ???; +type F; given F: F = ???; + + +object Macro: + opaque type StrCtx = StringContext + def apply(ctx: StringContext): StrCtx = ctx + def unapply(ctx: StrCtx): StringContext? = ctx + +def main: Unit = + extension (ctx: StringContext) def mac: Macro.StrCtx = Macro(ctx) + extension [T] (using A)(inline ctx: Macro.StrCtx)(using B) inline def unapplySeq[U](using C)(inline input: T)(using D)(using F): Seq[T]? = ??? + + (??? : Int) match + case mac"${x}" => 1 diff --git a/tests/pos/i8972-maybe.scala b/tests/pos/i8972-maybe.scala new file mode 100644 index 000000000000..aa8083de4ba3 --- /dev/null +++ b/tests/pos/i8972-maybe.scala @@ -0,0 +1,11 @@ +//> using options -Yexplicit-nulls +import language.experimental.magic +trait Num: + type Nat + +object IsInt: + def unapply(using num: Num)(sc: num.Nat): Int? = ??? + +def test(using num: Num)(x: num.Nat) = + x match + case IsInt(i) => diff --git a/tests/pos/inline-unapply-maybe.scala b/tests/pos/inline-unapply-maybe.scala new file mode 100644 index 000000000000..7123d6fc62f3 --- /dev/null +++ b/tests/pos/inline-unapply-maybe.scala @@ -0,0 +1,17 @@ +//> using options -Yexplicit-nulls +import language.experimental.magic +object Test { + + class C(val x: Int, val y: Int) + + inline def unapply(c: C): Some[(Int, Int)] = Some((c.x, c.y)) + +} +object Test2 { + + class C(x: Int, y: Int) + + inline def unapply(c: C): (Int, Int)? = inline c match { + case x: C => (1, 1) + } +} diff --git a/tests/pos/maybe-conversions.scala b/tests/pos/maybe-conversions.scala new file mode 100644 index 000000000000..133fedc93060 --- /dev/null +++ b/tests/pos/maybe-conversions.scala @@ -0,0 +1,24 @@ +//> using options -Yexplicit-nulls +import language.experimental.magic +import scala.magic.* +import scala.util.Either + +def toOptionAny[T](x: Any): Option[Any] = x match + case Ok(y) => Some(y) + case null => None + +def toOption[T](x: T?): Option[T] = x match + case Ok(y) => Some(y) + case null => None + +def toOptionStr(x: String?): Option[String] = x match + case Ok(y) => Some(y) + case null => None + +def toEither[T, E](x: T ? E): Either[E, T] = x match + case Ok(y) => Right(y) + case Err(e) => Left(e) + +def toEitherIntStr[T, E](x: Int ? String): Either[String, Int] = x match + case Ok(y) => Right(y) + case Err(e) => Left(e) diff --git a/tests/pos/maybe-subtyping.scala b/tests/pos/maybe-subtyping.scala new file mode 100644 index 000000000000..15440006d575 --- /dev/null +++ b/tests/pos/maybe-subtyping.scala @@ -0,0 +1,19 @@ +//> using options -Yexplicit-nulls +import language.experimental.magic + +var x: String? = "hi" +var y: String | Null = "ho" + +def Test = + x = y + y = x + + val z1 = if ??? then x else y + val _: String? = z1 + val z2 = if ??? then y else x + val _: String? = z2 + + def foo[T](x: T, y: T): T = x + + val z3 = foo(x, y) + val _: String? = z3 diff --git a/tests/pos/maybe-translation.scala b/tests/pos/maybe-translation.scala new file mode 100644 index 000000000000..6d9f659c08c4 --- /dev/null +++ b/tests/pos/maybe-translation.scala @@ -0,0 +1,97 @@ +//> using options -Yexplicit-nulls +import language.experimental.magic +import scala.magic.* + +def foo(x: Int): Int? = + if x > 0 && x < 10 then x else null + +def bar(x: Int?) = + maybe: + val y = x? + if y > 0 + y + +/* With -Vprint:erasure should produce something like: + + (boundary1[Object]: + { + val y: Int = + matchResult1[Int]: + { + case val x1: Object = x + if (null == x1).unary_!() then + { + case val x4: Int = + Int.unbox( + (if x1.isInstanceOf[scala.magic.runtime.Valid] then + x1.asInstanceOf[scala.magic.runtime.Valid].elem() + else x1):Object + ) + case val y: Int = x4 + return[matchResult1] y:Int + } + else () + { + case val x3: scala.runtime.BoxedUnit = + scala.runtime.BoxedUnit.UNIT + case val e: scala.runtime.BoxedUnit = x3 + return[matchResult1] return[boundary1] null:Object:Object + } + throw new MatchError(x1) + } + if (y > 0).unary_!() then return[boundary1] null:Object:Object else () + Int.box(y:Int) + } + ):Object:Object +*/ + +def baz(x: Int ? String) = + maybe: + val y = x.withErr("not an int")? + if y > 0 else "not positive" + y + +/* With -Vprint:erasure should produce something like: + + (boundary2[Object]: + { + val y: Int = + { + val x$proxy1: Object = scala.magic.withErr(x, "not an int") + matchResult2[Int]: + { + case val x6: Object = x$proxy1 + if + (null == x6).unary_!() && + x6.isInstanceOf[scala.magic.runtime.Fail].unary_!() + then + { + case val x9: Int = + Int.unbox( + (if x6.isInstanceOf[scala.magic.runtime.Valid] then + x6.asInstanceOf[scala.magic.runtime.Valid].elem() + else x6):Object + ) + case val y: Int = x9 + return[matchResult2] y:Int + } + else () + { + case val x7: Object = + x6.asInstanceOf[scala.magic.runtime.Fail].elem() + case val e: String = x7.asInstanceOf[String] + return[matchResult2] + return[boundary2] + new scala.magic.runtime.Fail(e):Object:Object + } + throw new MatchError(x6) + } + } + if (y > 0).unary_!() then + return[boundary2] + new scala.magic.runtime.Fail("not positive"):Object:Object + else () + Int.box(y:Int) + } + ):Object:Object +*/ diff --git a/tests/pos/misc-unapply_pos-maybe.scala b/tests/pos/misc-unapply_pos-maybe.scala new file mode 100644 index 000000000000..f9bf55dd093d --- /dev/null +++ b/tests/pos/misc-unapply_pos-maybe.scala @@ -0,0 +1,29 @@ +//> using options -Yexplicit-nulls +import language.experimental.magic +object Test { + val xs = List(1) + val f: Int = { + xs match { + case List(x) => x + } + } +} + +// the following comes from ticket #230 +trait Foo { + def name: String + def unapply(x: String): Unit? = { + if x == name then () else null + } +} +object Bar extends Foo { def name = "bar" } +object Baz extends Foo { def name = "baz" } + +object Test_ { + def matcher(s: String) = s match { + case Bar(x) => println("bar") + case Baz(x) => println("baz") +// ^ +// error: unreachable code + } + } diff --git a/tests/pos/simpleExtractors-1-maybe.scala b/tests/pos/simpleExtractors-1-maybe.scala new file mode 100644 index 000000000000..e47faf0fba21 --- /dev/null +++ b/tests/pos/simpleExtractors-1-maybe.scala @@ -0,0 +1,31 @@ +//> using options -Yexplicit-nulls +import language.experimental.magic +import scala.magic.* +class Foo { + def bar(x: Any): Unit = x match { + case Bar(a) => println(a) + case BarSeq(a) => println(a) + case BarSeq(a, b) => println(a) + } + def baz(x: Any): Unit = x match { + case Baz(a) => println(a) + case BazSeq(a) => println(a) + case BazSeq(a, b) => println(a) + } +} + +object Bar { + def unapply(arg: Any): Any? = Ok(arg) +} + +object BarSeq { + def unapplySeq(arg: Any): Seq[Any]? = List(arg) +} + +object Baz { + def unapply[T](arg: T): T? = Ok(arg) +} + +object BazSeq { + def unapplySeq[T](arg: T): Seq[T]? = List(arg) +} diff --git a/tests/pos/spec-strings.scala b/tests/pos/spec-strings.scala index eb3f47f13c86..157e5c648a0a 100644 --- a/tests/pos/spec-strings.scala +++ b/tests/pos/spec-strings.scala @@ -1,3 +1,4 @@ +//> using options -Yexplicit-nulls import language.experimental.magic class Date diff --git a/tests/pos/t1260-maybe.scala b/tests/pos/t1260-maybe.scala new file mode 100644 index 000000000000..661338374a24 --- /dev/null +++ b/tests/pos/t1260-maybe.scala @@ -0,0 +1,20 @@ +//> using options -Yexplicit-nulls +import language.experimental.magic +case class Foo(a: String, b: String) + +object Bar { + def unapply(s: String): Long? = + try { s.toLong } catch { case _ => null } +} + +object Test { + def main(args: Array[String]): Unit = { + val f = Foo("1", "2") + f match { + case Foo(Bar(1), Bar(2)) => 1 + case Foo(Bar(i), Bar(j)) if i >= 0 => 2 + case _ => 3 + } + } +} + diff --git a/tests/pos/t6675-maybe.scala b/tests/pos/t6675-maybe.scala new file mode 100644 index 000000000000..b1db21b66360 --- /dev/null +++ b/tests/pos/t6675-maybe.scala @@ -0,0 +1,23 @@ +//> using options -Yexplicit-nulls +import language.experimental.magic +import scala.magic.* +object LeftOrRight { + def unapply[A](value: Either[A, A]): A? = value match { + case scala.Left(x) => Ok(x) + case scala.Right(x) => Ok(x) + } +} + +object Test { + (Left((0, 0)): Either[(Int, Int), (Int, Int)]) match { + case LeftOrRight(pair @ (a, b)) => a // false -Wshadow warning: "extractor pattern binds a single value to a Product2 of type (Int, Int)" + } + + (Left((0, 0)): Either[(Int, Int), (Int, Int)]) match { + case LeftOrRight((a, b)) => a // false -Wshadow warning: "extractor pattern binds a single value to a Product2 of type (Int, Int)" + } + + (Left((0, 0)): Either[(Int, Int), (Int, Int)]) match { + case LeftOrRight(a, b) => a // false -Wshadow warning: "extractor pattern binds a single value to a Product2 of type (Int, Int)" + } +} diff --git a/tests/pos/t6994-maybe.scala b/tests/pos/t6994-maybe.scala new file mode 100644 index 000000000000..fe9ba9565b0f --- /dev/null +++ b/tests/pos/t6994-maybe.scala @@ -0,0 +1,10 @@ +//> using options -Yexplicit-nulls +import language.experimental.magic +object Test { + object NF { + def unapply(t: Throwable): Throwable? = null + } + val x = (try { None } catch { case NF(ex) => None }) getOrElse 0 + // Was emitting a spurious warning post typer: + // "This catches all Throwables. If this is really intended, use `case ex6 : Throwable` to clear this warning." +} diff --git a/tests/pos/t8128-maybe.scala b/tests/pos/t8128-maybe.scala new file mode 100644 index 000000000000..888390e8a1f2 --- /dev/null +++ b/tests/pos/t8128-maybe.scala @@ -0,0 +1,19 @@ +//> using options -Yexplicit-nulls +import language.experimental.magic +import scala.magic.* +import compiletime.Maybe +object G { + def unapply(m: Any): Maybe[Any, Unit] = Ok("") +} + +object H { + def unapplySeq(m: Any): Seq[?]? = null +} + +object Test { + (0: Any) match { + case G(v) => v + case H(v) => v + case _ => + } +} diff --git a/tests/run-bootstrapped/orelse.check b/tests/run-bootstrapped/orelse.check new file mode 100644 index 000000000000..0bf2c3133cd0 --- /dev/null +++ b/tests/run-bootstrapped/orelse.check @@ -0,0 +1,22 @@ +Right(s) +Left(()) +Right(s) +Left(bad) +Right(1) +Right(22) +Left(bad) +Right(ss) +Left(BAD) +pos 1 1 +neg -1 +nonempty abc bc +empty +poly abc = bc +poly 22 = 22 +nopoly +nopoly -1 +poly2 abc = bc +poly2 22 = 22 +nopoly2 +nopoly2 -1 +nopoly2 true diff --git a/tests/run-bootstrapped/parse-dates.check b/tests/run-bootstrapped/parse-dates.check new file mode 100644 index 000000000000..3bb4f8ecd846 --- /dev/null +++ b/tests/run-bootstrapped/parse-dates.check @@ -0,0 +1,20 @@ +1/1/2000 +Date(1,1,2000) +Date(1,1,2000) +Date(1,1,2000) +Date(1,1,2000) +1/1-2000 +null +Fail(Date not in format day/month/year) +Fail(Date not in format day/month/year) +null +1/jan/2000 +null +Fail(malformed month: jan) +Fail(malformed month: jan) +null +1/13/2000 +Date(1,13,2000) +Date(1,13,2000) +Fail(month 13 outside allowed range 1..12) +null diff --git a/tests/run-bootstrapped/parse-dates.scala b/tests/run-bootstrapped/parse-dates.scala new file mode 100644 index 000000000000..29e4b5bd2cc1 --- /dev/null +++ b/tests/run-bootstrapped/parse-dates.scala @@ -0,0 +1,79 @@ +//> using options -Yexplicit-nulls +import language.experimental.magic +import scala.magic.* + +extension (str: String) def parseInt: Int? = + try str.toInt + catch case ex: NumberFormatException => null + +case class Date(day: Int, month: Int, year: Int) + +def parseDate(str: String) = + str.split("/") match + case Array(d, m, y) => + maybe: + Date(d.parseInt?, m.parseInt?, y.parseInt?) + case _ => + null + +def parseDate2(str: String): Date ? String = + str.split("/") match + case Array(d, m, y) => + maybe: + val day = d.parseInt.withErr(s"malformed day: $d")? + val month = m.parseInt.withErr(s"malformed month: $m")? + val year = y.parseInt.withErr(s"malformed year: $y")? + Date(day, month, year) + case _ => + Err("Date not in format day/month/year") + +def parseDate3(str: String): Date ? String = + str.split("/") match + case Array(d, m, y) => + maybe: + val day = d.parseInt.withErr(s"malformed day: $d")? + val month = m.parseInt.withErr(s"malformed month: $m")? + val year = y.parseInt.withErr(s"malformed year: $y")? + if 1 <= day && day <= 31 else s"day $day outside allowed range 1..31" + if 1 <= month && month <= 12 else s"month $month outside allowed range 1..12" + Date(day, month, year) + case _ => + Err("Date not in format day/month/year") + +def parseDate4(str: String): Date? = + str.split("/") match + case Array(d, m, y) => + maybe: + val day = d.parseInt? + val month = m.parseInt? + val year = y.parseInt? + if 1 <= day && day <= 31 + if 1 <= month && month <= 12 + Date(day, month, year) + case _ => + null + +@main def Test = + println("1/1/2000") + println(parseDate("1/1/2000")) + println(parseDate2("1/1/2000")) + println(parseDate3("1/1/2000")) + println(parseDate4("1/1/2000")) + + println("1/1-2000") + println(parseDate("1/1-2000")) + println(parseDate2("1/1-2000")) + println(parseDate3("1/1-2000")) + println(parseDate4("1/1-2000")) + + println("1/jan/2000") + println(parseDate("1/jan/2000")) + println(parseDate2("1/jan/2000")) + println(parseDate3("1/jan/2000")) + println(parseDate4("1/jan/2000")) + + println("1/13/2000") + println(parseDate("1/13/2000")) + println(parseDate2("1/13/2000")) + println(parseDate3("1/13/2000")) + println(parseDate4("1/13/2000")) diff --git a/tests/run-tasty-inspector/stdlibExperimentalDefinitions.scala b/tests/run-tasty-inspector/stdlibExperimentalDefinitions.scala index a22d88f7848b..0bbba019688f 100644 --- a/tests/run-tasty-inspector/stdlibExperimentalDefinitions.scala +++ b/tests/run-tasty-inspector/stdlibExperimentalDefinitions.scala @@ -96,10 +96,21 @@ val experimentalDefinitionInLibrary = Set( // New feature: Erased trait "scala.compiletime.Erased", - + // New feature: Specialized traits "scala.specialize.Specialized", - "scala.specialize.Specialized$" + "scala.specialize.Specialized$", + + // New feature: magic + "scala.magic.Ok", + "scala.magic.Ok$", + "scala.magic.compiletime.Maybe", + "scala.magic.compiletime.package$.$spec", + "scala.magic.compiletime.package$.$wrappedType", + "scala.magic.runtime.Valid", + "scala.magic.Err", + "scala.magic.Err$", + "scala.magic.runtime.Fail", ) diff --git a/tests/run/LazyLists-maybe.scala b/tests/run/LazyLists-maybe.scala new file mode 100644 index 000000000000..711599697ee5 --- /dev/null +++ b/tests/run/LazyLists-maybe.scala @@ -0,0 +1,112 @@ +//> using options -Yexplicit-nulls +import language.experimental.magic +package xcollections: + import annotation.unchecked.uncheckedVariance + import compiletime.uninitialized + + abstract class LazyList[+T]: + + private var myHead: T = uninitialized + private var myTail: LazyList[T] = uninitialized + private var myForced: LazyList[T] | Null = null + + protected def force(): LazyList[T] + + protected def set(hd: T @uncheckedVariance, tl: LazyList[T] @uncheckedVariance): LazyList[T] = + assert(myForced == null, "implementation error: attempting to re-define existing LazyList") + myHead = hd + myTail = tl + this + + def forced(): LazyList[T] = + var myForced = this.myForced + if myForced == null then + myForced = force() + this.myForced = myForced + assert(myForced.myForced != null, "implementation error: LazyList was not forced") + myForced + else myForced + + def isEmpty: Boolean = forced() eq LazyList.empty + + def head: T = + val e = forced() + require(!e.isEmpty, "head on empty LazyList") + e.myHead + + def tail: LazyList[T] = + val e = forced() + require(!e.isEmpty, "tail on empty LazyList") + e.myTail + + def fromIterable[T](xs: Iterable[T]) = xs match + case xs: LazyList[T] @unchecked => xs + case _ => LazyList.fromIterator(xs.iterator) + + object LazyList: + + val empty: LazyList[Nothing] = new: + protected def force(): LazyList[Nothing] = this + + object `#::`: + def unapply[T](xs: LazyList[T]): (T, LazyList[T])? = + if xs.isEmpty then null + else (xs.head, xs.tail) + + def fromIterator[T](it: Iterator[T]): LazyList[T] = new: + protected def force() = + if it.hasNext then set(it.next, fromIterator (it)) + else empty + + extension [T, U >: T](xs: LazyList[T]) + def #::(x: U): LazyList[U] = new: + protected def force(): LazyList[U] = + set(x, xs) + + def ++(ys: LazyList[U]): LazyList[U] = new: + protected def force() = + if xs.isEmpty then ys.forced() + else set(xs.head, xs.tail ++ ys) + + extension [T, U](xs: LazyList[T]) + def map(f: T => U): LazyList[U] = new: + protected def force() = + if xs.isEmpty then empty + else set(f(xs.head), xs.tail.map(f)) + + def flatMap(f: T => LazyList[U]): LazyList[U] = new: + protected def force(): LazyList[U] = + if xs.isEmpty then empty + else f(xs.head) ++ xs.tail.flatMap(f) + + def foldLeft(z: U)(f: (U, T) => U): U = + if xs.isEmpty then z + else xs.tail.foldLeft(f(z, xs.head))(f) + + extension [T](xs: LazyList[T]) + def filter(p: T => Boolean): LazyList[T] = new: + protected def force(): LazyList[T] = + if xs.isEmpty then empty + else if p(xs.head) then set(xs.head, xs.tail.filter(p)) + else xs.tail.filter(p) + + def take(n: Int): LazyList[T] = + if n <= 0 then empty + else new: + protected def force(): LazyList[T] = + if xs.isEmpty then xs + else set(xs.head, xs.tail.take(n - 1)) + + def drop(n: Int): LazyList[T] = + if n <= 0 then xs + else new: + protected def force(): LazyList[T] = + def advance(xs: LazyList[T], n: Int): LazyList[T] = + if n <= 0 || xs.isEmpty then xs + else advance(xs.tail, n - 1) + advance(xs, n) + + end LazyList +end xcollections + +@main def Test() = () \ No newline at end of file diff --git a/tests/run/Typeable-maybe.check b/tests/run/Typeable-maybe.check new file mode 100644 index 000000000000..671b06e6ab94 --- /dev/null +++ b/tests/run/Typeable-maybe.check @@ -0,0 +1,8 @@ +1 is a Int +List(1, 2, 3) is a List[Int] +List(1, 2, 3) is not a Int +1 is not a List[Int] +a is not a Int +List() is a List[Int] +List(1, a) is not a List[Int] +List(1, 2) is a List[Int] diff --git a/tests/run/Typeable-maybe.scala b/tests/run/Typeable-maybe.scala new file mode 100644 index 000000000000..395f6b4557ea --- /dev/null +++ b/tests/run/Typeable-maybe.scala @@ -0,0 +1,62 @@ +//> using options -Yexplicit-nulls +/** A test that shows how to use shapeless.Typeable in extractors without the + * TypeLevel Scala 4 extensions. + * In essence you have to write + * + * case Typeable.instanceOf[T](x) => + * + * instead of + * + * case Typeable[T](x) + * + * The first idiom would be nice to have but it requires more backtracking + * in Typer that we allow now. Essentially, given + * + * case C[T](x) + * + * it's unclear whether this should expand to `C[T].unapply(x)`, (as it does now) + * or to `C.unapply[T](x)` (which is what TypeLevel Scala 4 did, I believe) + */ +import language.experimental.magic +import scala.magic.* +trait Typeable[T]: + def cast(x: Any): Option[T] + def describe: String + override def toString = s"Typeable[$describe]" + +object Typeable: + def apply[T: Typeable]: Typeable[T] = summon + + class instanceOf[T: Typeable]: + def unapply(x: Any): T? = Typeable[T].cast(x) match + case Some(t) => Ok(t) + case None => null + + given int: Typeable[Int]: + def cast(x: Any): Option[Int] = x match + case x: Int => Some(x) + case _ => None + def describe = "Int" + + given list: [T: Typeable] => Typeable[List[T]]: + def cast(x: Any): Option[List[T]] = x match + case x: List[_] if x.forall(Typeable[T].cast(_).isDefined) => Some(x.asInstanceOf[List[T]]) + case _ => None + def describe = s"List[${Typeable[T].describe}]" +end Typeable + +def testInstance[T: Typeable](x: Any): Unit = + val isa = x match + case Typeable.instanceOf[T](x) => "is a" + case _ => "is not a" + println(s"$x $isa ${Typeable[T].describe}") + +@main def Test() = + testInstance[Int](1) + testInstance[List[Int]](List(1, 2, 3)) + testInstance[Int](List(1, 2, 3)) + testInstance[List[Int]](1) + testInstance[Int]("a") + testInstance[List[Int]](Nil) + testInstance[List[Int]](1 :: "a" :: Nil) + testInstance[List[Int]](1 :: 2 :: Nil) diff --git a/tests/run/errorhandling/magicTest.scala b/tests/run/errorhandling/magicTest.scala new file mode 100644 index 000000000000..5fd23906f0e9 --- /dev/null +++ b/tests/run/errorhandling/magicTest.scala @@ -0,0 +1,171 @@ +//> using options -Yexplicit-nulls +package magicTest + +import scala.util.*, boundary.break +import caps.any +import language.experimental.magic + +/** boundary/break as a replacement for non-local returns */ +def indexOf[T](xs: List[T], elem: T): Int = + boundary: + for (x, i) <- xs.zipWithIndex do + if x == elem then break(i) + -1 + +def breakTest() = + println("breakTest") + assert(indexOf(List(1, 2, 3), 2) == 1) + assert(indexOf(List(1, 2, 3), 0) == -1) + +/** traverse becomes trivial to write */ +def traverse[T](xs: List[Option[T]]): Option[List[T]] = + optional(xs.map(_?)) + +def optTest() = + println("optTest") + assert(traverse(List(Some(1), Some(2), Some(3))) == Some(List(1, 2, 3))) + assert(traverse(List(Some(1), None, Some(3))) == None) + +/** A check function returning a Result[Unit, _] */ +inline def check[E](p: Boolean, err: E): Result[Unit, E] = + if p then Ok(()) else Err(err) + +/** Another variant of a check function that returns directly to the given + * label in case of error. + */ +inline def check_![E](p: Boolean, err: E)(using l: boundary.Label[Err[E]]): Unit = + if p then () else break(Err(err)) + +/** Use `Result` to convert exceptions to `Err` values */ +def parseDouble(s: String): Result[Double, Exception] = + Result(s.toDouble) + +def parseDoubles(ss: List[String]): Result[List[Double], Exception] = + respond: + ss.map(parseDouble(_)?) + +/** Demonstrate combination of `check` and `?`. */ +def trySqrt(x: Double) = // inferred: Result[Double, String] + respond: + check(x >= 0, s"cannot take sqrt of negative $x")? // direct jump + math.sqrt(x) + +/** Instead of `check(...)?` one can also use `check_!(...)`. + * Note use of `mapErr` to convert Exception errors to String errors. + */ +def sumRoots(xs: List[String]) = // inferred: Result[Double, String] + respond: + check_!(xs.nonEmpty, "list is empty") // direct jump + val ys = parseDoubles(xs).mapErr(_.toString)? // direct jump + ys.reduce((x, y) => x + trySqrt(y)?) // need exception to propagate `Err` + +def resultTest() = { + println("resultTest") + def assertFail(value: Any, s: String) = value match + case Err(msg: String) => assert(msg.contains(s)) + assert(sumRoots(List("1", "4", "9")) == Ok(6)) + assertFail(sumRoots(List("1", "-2", "4")), "cannot take sqrt of negative") + assertFail(sumRoots(List()), "list is empty") + assertFail(sumRoots(List("1", "3ab")), "NumberFormatException") + + val xs = sumRoots(List("1", "-2", "4")) *: sumRoots(List()) *: sumRoots(List("1", "3ab")) *: Result.empty + xs match + case Err(msgs) => assert(msgs.length == 3) + case _ => assert(false) + + val ys = sumRoots(List("1", "2", "4")) *: sumRoots(List("1")) *: sumRoots(List("2")) *: Result.empty + ys match + case Ok((a, b, c)) => // ok + case _ => assert(false) +} + +def validateTest() = { + println("validateTest") + import Validation.validate + + case class Email private (value: String) + object Email: + def from(raw: String): Result[Email, String] = + if raw.contains("@") then Ok(Email(raw)) // demo!! + else Err(s"Invalid email ${raw}") + case class Form(name: String, email: Email, age: Int) + + def validatedForm(name: String, rawEmail: String, age: Int, confirmed: Boolean): Result[Form, List[String]] = + validate: v => + v.require(!name.isEmpty, "Missing name") + v.test(name.head.isUpper, s"${name} does not start with uppercase letter") + val email = v.test(Email.from(rawEmail)) + v.test(age >= 18, s"Age ${age} is below minimum age 18") + v.test(confirmed, "Missing confirmation") + Form(name, email.valid, age) + + // Good stuff: can no longer edit the scope if you leak it + // def leak(): Unit = + // val leaked: Result[Validation[String]^, List[String]] = Validation.validate: v => + // v + // leaked match + // case Ok(scope) => + // scope.appendOne("leaked error") // error + // ??? + // case Err(errs) => () + + type JsonDict = Map[String, String] + def jsonDict(elems: (String, String)*): JsonDict = Map(elems*) + + enum InvoiceOrRefund: + case Invoice(customerId: String, amount: BigInt) + case Refund(invoiceId: String, reason: String) + + def validateJson(json: JsonDict): Result[InvoiceOrRefund, List[String]] = + validate: v => + val kind = v.require(Result.fromOption(json.get("kind"), "missing 'kind'")) + v.require(kind == "invoice" || kind == "refund", s"invalid 'kind' ${kind}") + if kind == "invoice" then + val customerId = v.test(Result.fromOption(json.get("customerId"), s"Missing customerId")) + val amount = v.test { + respond: + val amount = Result.fromOption(json.get("amount"), s"Missing amount")? + Result(BigInt(amount)).mapErr(err => s"Invalid amount: ${err.getMessage}")? + } + InvoiceOrRefund.Invoice(customerId.valid, amount.valid) + else + val invoiceId = v.test(Result.fromOption(json.get("invoiceId"), s"Missing invoiceId")) + val reason = v.test(Result.fromOption(json.get("reason"), s"Missing reason")) + InvoiceOrRefund.Refund(invoiceId.valid, reason.valid) + + def printResult[T, E](result: Result[T, List[E]]): Unit = result match + case Ok(value) => println(s"ok: $value") + case Err(errs) => println(s"err: ${errs.zipWithIndex.map { case (e, i) => s"[$i]: $e" }.mkString(", ")}") + + val p1 = validatedForm("Bob", "Bob@example.com", 21, true) // TTTT + val p2 = validatedForm("", "Bob@example.com", 21, true) // F (abort early) + val p3 = validatedForm("bob", "Bob@example.com", 21, true) // FTTT + val p4 = validatedForm("Bob", "bad-email", 21, true) // TFTT + val p5 = validatedForm("Bob", "Bob@example.com", 16, true) // TTFT + val p6 = validatedForm("Bob", "Bob@example.com", 21, false) // TTTF + val p7 = validatedForm("bob", "bad-email", 16, false) // FFFF + printResult(p1) + printResult(p2) + printResult(p3) + printResult(p4) + printResult(p5) + printResult(p6) + printResult(p7) + + println("----") + + val j1 = validateJson(jsonDict("kind" -> "invoice", "customerId" -> "c1", "amount" -> "100")) // ok + val j2 = validateJson(jsonDict("kind" -> "refund", "invoiceId" -> "i1", "reason" -> "bad product")) // ok + val j3 = validateJson(jsonDict("kind" -> "invoice")) // [0]: missing customerId, [1]: missing amount + val j4 = validateJson(jsonDict("kind" -> "invoice", "customerId" -> "c1", "amount" -> "bad amount")) // bad amount + val j5 = validateJson(jsonDict("kind" -> "refund")) // [0]: missing invoiceId, [1]: missing reason + val j6 = validateJson(jsonDict("kind" -> "other")) // invalid kind + val j7= validateJson(jsonDict()) // missing kind + printResult(j1) + printResult(j2) + printResult(j3) + printResult(j4) + printResult(j5) + printResult(j6) + printResult(j7) +} diff --git a/tests/run/fully-abstract-nat-3-maybe.check b/tests/run/fully-abstract-nat-3-maybe.check new file mode 100644 index 000000000000..37bcfe2c2448 --- /dev/null +++ b/tests/run/fully-abstract-nat-3-maybe.check @@ -0,0 +1,13 @@ +CaseNums +ok +ok +ok - unchecked error +None +Some((SuccClass(ZeroObj),SuccClass(ZeroObj))) + +IntNums +ok +ok +ok - unchecked error +None +Some((1,1)) diff --git a/tests/run/fully-abstract-nat-3-maybe.scala b/tests/run/fully-abstract-nat-3-maybe.scala new file mode 100644 index 000000000000..7ed8697191df --- /dev/null +++ b/tests/run/fully-abstract-nat-3-maybe.scala @@ -0,0 +1,159 @@ +//> using options -Yexplicit-nulls + +import language.experimental.magic +object Test { + def main(args: Array[String]): Unit = { + println("CaseNums") + test(CaseNums) + println() + println("IntNums") + test(IntNums) + } + + def test(numbers: Numbers) = { + import numbers.* + + val zero: Nat = Zero() + val one: Nat = Succ(zero) + val two: Nat = Succ(one) + val three: Nat = Succ(two) + + zero match { + case Succ(p) => println("error") + case Zero() => println("ok") + } + + one match { + case Zero() => println("error") + case Succ(p) => println("ok") + } + + zero match { + case s: Succ => println("ok - unchecked error") + case z: Zero => println("ok - unchecked no error") + } + + def divOpt(a: Nat, b: Nat): Option[(Nat, Nat)] = b match { + case SuccRefine(s @ Succ(_)) => Some(safeDiv(a, s)) + case _ => None + } + + println(divOpt(one, zero)) + println(divOpt(three, two)) + } +} + +trait Numbers { + + type Nat + type Zero <: Nat + type Succ <: Nat + + val Zero: ZeroExtractor + trait ZeroExtractor { + def apply(): Zero + def unapply(nat: Nat): Boolean + } + + val Succ: SuccExtractor + trait SuccExtractor { + def apply(nat: Nat): Succ + def unapply(nat: Nat): Nat? + } + + val SuccRefine: SuccRefineExtractor + trait SuccRefineExtractor { + def unapply(nat: Nat): Succ? + } + + def SuccDeco(succ: Succ): SuccAPI + trait SuccAPI { + def pred: Nat + } + + def safeDiv(a: Nat, b: Succ): (Nat, Nat) +} + +object CaseNums extends Numbers { + + trait NatClass + case object ZeroObj extends NatClass + case class SuccClass(pred: NatClass) extends NatClass + + type Nat = NatClass + type Zero = ZeroObj.type + type Succ = SuccClass + + object Zero extends ZeroExtractor { + def apply(): Zero = ZeroObj + def unapply(nat: Nat): Boolean = nat == ZeroObj + } + + object Succ extends SuccExtractor { + def apply(nat: Nat): Succ = SuccClass(nat) + def unapply(nat: Nat): Nat? = nat match { + case SuccClass(pred) => pred + case _ => null + } + } + + object SuccRefine extends SuccRefineExtractor { + def unapply(nat: Nat): Succ? = nat match { + case succ @ SuccClass(_) => succ + case _ => null + } + } + + def SuccDeco(succ: Succ): SuccAPI = new SuccAPI { + def pred: Nat = succ.pred + } + + def safeDiv(a: Nat, b: Succ): (Nat, Nat) = { + def sdiv(div: Nat, rem: Nat): (Nat, Nat) = + if lessOrEq(rem, b) then (div, rem) + else sdiv(Succ(div), minus(rem, b)) + sdiv(Zero(), a) + } + + private def lessOrEq(a: Nat, b: Nat): Boolean = (a, b) match { + case (Succ(a1), Succ(b1)) => lessOrEq(a1, b1) + case (Zero(), _) => true + case _ => false + } + + // assumes a >= b + private def minus(a: Nat, b: Nat): Nat = (a, b) match { + case (Succ(a1), Succ(b1)) => minus(a1, b1) + case _ => a + } + +} + +object IntNums extends Numbers { + type Nat = Int + type Zero = Int // 0 + type Succ = Int // n > 0 + + object Zero extends ZeroExtractor { + def apply(): Int = 0 + def unapply(nat: Nat): Boolean = nat == 0 + } + + object Succ extends SuccExtractor { + def apply(nat: Nat): Int = nat + 1 + def unapply(nat: Nat): Int? = + if nat > 0 then nat - 1 else null + } + + + object SuccRefine extends SuccRefineExtractor { + def unapply(nat: Nat): Succ? = + if nat > 0 then nat else null + } + + def SuccDeco(succ: Succ): SuccAPI = new SuccAPI { + def pred: Int = succ - 1 + } + + def safeDiv(a: Nat, b: Succ): (Nat, Nat) = (a / b, a % b) +} diff --git a/tests/run/fully-abstract-nat-maybe.check b/tests/run/fully-abstract-nat-maybe.check new file mode 100644 index 000000000000..bb36ec67992e --- /dev/null +++ b/tests/run/fully-abstract-nat-maybe.check @@ -0,0 +1,35 @@ +CaseClassImplementation +underlying rep: Z +test1 OK +test2 OK + +underlying rep: S(S(S(Z))) +test3 OK +Succ(S(S(Z))) = 3 +test4 OK +Succ(S(S(Z))) = 3 + +IntImplementation +underlying rep: 0 +test1 OK +test2 OK + +underlying rep: 3 +test3 OK +Succ(2) = 3 +test4 OK +Succ(2) = 3 + +UnboundedIntImplementation +underlying rep: 0 +test1 OK +test2 OK + +underlying rep: 3 +test3 OK +Succ(2) = 3 +test4 OK +Succ(2) = 3 + +test OK +Succ(1267650600228229401496703205374) = 1267650600228229401496703205376 diff --git a/tests/run/fully-abstract-nat-maybe.scala b/tests/run/fully-abstract-nat-maybe.scala new file mode 100644 index 000000000000..ee76205fe1f5 --- /dev/null +++ b/tests/run/fully-abstract-nat-maybe.scala @@ -0,0 +1,295 @@ +//> using options -Yexplicit-nulls +import language.experimental.magic +import scala.magic.* +import scala.reflect.ClassTag +import language.implicitConversions + +object Test { + def main(args: Array[String]): Unit = { + println("CaseClassImplementation") + testInterface(CaseClassImplementation) + + println() + + println("IntImplementation") + testInterface(IntImplementation) + + println() + + println("UnboundedIntImplementation") + testInterface(UnboundedIntImplementation) + + println() + + { + import UnboundedIntImplementation.{*, given} + val large = (BigInt(1) << 100).asInstanceOf[Succ] + large match { + case Zero() => println("test fail") + case s @ Succ(pred) => + println("test OK") + println(s"Succ(${pred.pred}) = $s") + } + } + } + + def testInterface(numbers: Numbers): Unit = { + import numbers.{*, given} + given ClassTag[Nat] = numbers.natClassTag + given ClassTag[Zero] = numbers.zeroClassTag + given ClassTag[Succ] = numbers.succClassTag + val zero = Zero() + println("underlying rep: " + zero) + + zero match { + case Succ(_) => println("test1 fail") + case Zero() => println("test1 OK") + } + + zero match { + case _: Succ => println("test2 fail") + case _: Zero => println("test2 OK") + } + + println() + + val three = Succ(Succ(zero)).succ + + println("underlying rep: " + three) + + three match { + case Zero() => println("test3 fail") + case s @ Succ(pred) => + println("test3 OK") + println(s"Succ($pred) = ${s.value}") + } + + three match { + case _: Zero => println("test4 fail") + case s: Succ => + println("test4 OK") + println(s"Succ(${s.pred}) = ${s.value}") + } + } + +} + +abstract class Numbers { + + // === Nat ========================================== + // Represents: + // trait Nat + // case object Zero extends Nat + // case class Succ(pred: Nat) extends Nat + + type Nat + def natClassTag: ClassTag[Nat] + + trait AbstractNat { + def value: Int + def succ: Succ + } + def NatDeco(nat: Nat): AbstractNat + given Conversion[Nat, AbstractNat] = NatDeco(_) + + // --- Zero ---------------------------------------- + + type Zero <: Nat + + def zeroClassTag: ClassTag[Zero] + + val Zero: ZeroExtractor + abstract class ZeroExtractor { + def apply(): Zero + def unapply(zero: Zero): Boolean + } + + // --- Succ ---------------------------------------- + + type Succ <: Nat + + def succClassTag: ClassTag[Succ] + + val Succ: SuccExtractor + abstract class SuccExtractor { + def apply(nat: Nat): Succ + def unapply(x: Succ): Nat? + } + + trait AbstractSucc { + def pred: Nat + } + def SuccDeco(succ: Succ): AbstractSucc + given Conversion[Succ, AbstractSucc] = SuccDeco(_) + +} + +object CaseClassImplementation extends Numbers { + + sealed trait N + final object Z extends N { override def toString: String = "Z" } + final case class S(n: N) extends N + + // === Nat ========================================== + + type Nat = N + + def natClassTag: ClassTag[Nat] = implicitly + + def NatDeco(nat: Nat): AbstractNat = new AbstractNat { + def value: Int = nat match { + case Succ(n) => 1 + n.value + case _ => 0 + } + def succ: Succ = Succ(nat) + } + + // --- Zero ---------------------------------------- + + type Zero = Z.type + + def zeroClassTag: ClassTag[Zero] = implicitly + + val Zero: ZeroExtractor = new ZeroExtractor { + def apply(): Zero = Z + def unapply(zero: Zero): Boolean = true // checked by class tag before calling the unapply + } + + // --- Succ ---------------------------------------- + + type Succ = S + + def succClassTag: ClassTag[Succ] = implicitly + + val Succ: SuccExtractor = new SuccExtractor { + def apply(nat: Nat): Succ = S(nat) + def unapply(succ: Succ): Nat? = succ.n // checked by class tag before calling the unapply + } + + def SuccDeco(succ: Succ): AbstractSucc = new AbstractSucc { + def pred: Nat = succ.n + } + +} + + +object IntImplementation extends Numbers { + + // === Nat ========================================== + + type Nat = Int + + def natClassTag: ClassTag[Nat] = intClassTag(_ >= 0) + + def NatDeco(nat: Nat): AbstractNat = new AbstractNat { + def value: Int = nat + def succ: Succ = nat + 1 + } + + // --- Zero ---------------------------------------- + + type Zero = Int + + def zeroClassTag: ClassTag[Zero] = intClassTag(_ == 0) + + val Zero: ZeroExtractor = new ZeroExtractor { + def apply(): Zero = 0 + def unapply(zero: Zero): Boolean = true // checked by class tag before calling the unapply + } + + // --- Succ ---------------------------------------- + + type Succ = Int + + def succClassTag: ClassTag[Succ] = intClassTag(_ > 0) + + val Succ: SuccExtractor = new SuccExtractor { + def apply(nat: Nat): Succ = nat + 1 + def unapply(succ: Succ): Nat? = succ - 1 // checked by class tag before calling the unapply + } + + def SuccDeco(succ: Succ): AbstractSucc = new AbstractSucc { + def pred: Nat = succ - 1 + } + + private def intClassTag(cond: Int => Boolean): ClassTag[Int] = new ClassTag[Int] { + def runtimeClass: Class[?] = classOf[Int] + override def unapply(x: Any): Option[Int] = x match { + case i: Int if cond(i) => Some(i) + case _ => None + } + } + +} + +object UnboundedIntImplementation extends Numbers { + + // === Nat ========================================== + + type Nat = Any // Int | BigInt + + def natClassTag: ClassTag[Nat] = new ClassTag[Any] { + def runtimeClass: Class[?] = classOf[Any] + override def unapply(x: Any): Option[Nat] = x match { + case i: Int if i >= 0 => Some(i) + case i: BigInt if i > Int.MaxValue => Some(i) + case _ => None + } + } + + def NatDeco(nat: Nat): AbstractNat = new AbstractNat { + def value: Int = nat match { + case nat: Int => nat + case _ => throw new Exception("Number too large: " + nat) + } + def succ: Succ = nat match { + case nat: Int => + if nat == Integer.MAX_VALUE then BigInt(nat) + 1 + else nat + 1 + case nat: BigInt => nat + 1 + } + } + + // --- Zero ---------------------------------------- + + type Zero = Int + + def zeroClassTag: ClassTag[Zero] = new ClassTag[Int] { + def runtimeClass: Class[?] = classOf[Int] + override def unapply(x: Any): Option[Int] = if x == 0 then Some(0) else None + } + + object Zero extends ZeroExtractor { + def apply(): Zero = 0 + def unapply(zero: Zero): Boolean = true // checked by class tag before calling the unapply + } + + // --- Succ ---------------------------------------- + + type Succ = Any // Int | BigInt + + def succClassTag: ClassTag[Succ] = new ClassTag[Any] { + def runtimeClass: Class[?] = classOf[Any] + override def unapply(x: Any): Option[Succ] = x match { + case i: Int if i > 0 => Some(i) + case i: BigInt if i > Int.MaxValue => Some(i) + case _ => None + } + } + + object Succ extends SuccExtractor { + def apply(nat: Nat): Succ = nat.succ + def unapply(succ: Succ): Nat? = Ok(succ.pred) // succ > 0 checked by class tag before calling the unapply + } + + def SuccDeco(succ: Succ): AbstractSucc = new AbstractSucc { + def pred: Nat = succ match { + case succ: Int => succ - 1 // succ > 0 checked by class tag before calling the unapply + case succ: BigInt => + val n = succ - 1 + if n.isValidInt then n.intValue() + else n + } + } + +} diff --git a/tests/run/i13968-maybe.scala b/tests/run/i13968-maybe.scala new file mode 100644 index 000000000000..e753e2683f5e --- /dev/null +++ b/tests/run/i13968-maybe.scala @@ -0,0 +1,28 @@ +//> using options -Yexplicit-nulls +import language.experimental.magic +object Bar { + def unapply(x: Any): (Int *: Int *: EmptyTuple)? = 1 *: 2 *: Tuple() +} + +object Bar23 { + def unapply(x: Any): Option[ + Int *: Int *: Int *: Int *: Int *: Int *: Int *: Int *: Int *: Int *: + Int *: Int *: Int *: Int *: Int *: Int *: Int *: Int *: Int *: Int *: + Int *: Int *: Int *: EmptyTuple + ] = Some( + 1 *: 2 *: 3 *: 4 *: 5 *: 6 *: 7 *: 8 *: 9 *: 10 *: + 11 *: 12 *: 13 *: 14 *: 15 *: 16 *: 17 *: 18 *: 19 *: 20 *: + 21 *: 22 *: 23 *: Tuple() + ) +} + +@main def Test() = + "" match + case Bar((a, b)) => assert(a == 1 && b == 2, (a, b)) + + "" match + case Bar23(( + u1, u2, u3, u4, u5, u6, u7, u8, u9, u10, + u11, u12, u13, u14, u15, u16, u17, u18, u19, u20, + u21, u22, u23 + )) => assert(u1 == 1 && u23 == 23, (u1, u23)) diff --git a/tests/run/i1773-maybe.check b/tests/run/i1773-maybe.check new file mode 100644 index 000000000000..888299747af9 --- /dev/null +++ b/tests/run/i1773-maybe.check @@ -0,0 +1,2 @@ +class + extends diff --git a/tests/run/i1773-maybe.scala b/tests/run/i1773-maybe.scala new file mode 100644 index 000000000000..6f40e67a9f8c --- /dev/null +++ b/tests/run/i1773-maybe.scala @@ -0,0 +1,18 @@ +//> using options -Yexplicit-nulls +import language.experimental.magic +import language.implicitConversions +object Test { + into class Foo(sc: StringContext) { + object q { + def unapply(arg: Any): (Any, Any)? = + (sc.parts(0), sc.parts(1)) + } + } + given Conversion[StringContext, Foo] = Foo(_) + + def main(args: Array[String]): Unit = { + val q"class ${name: String} extends ${parent: String}" = (new Object).runtimeChecked + println(name) + println(parent) + } +} diff --git a/tests/run/i4177-maybe.scala b/tests/run/i4177-maybe.scala new file mode 100644 index 000000000000..c8cdaf93bdc4 --- /dev/null +++ b/tests/run/i4177-maybe.scala @@ -0,0 +1,20 @@ +//> using options -Yexplicit-nulls +import language.experimental.magic +object Test { + private var count = 0 + + def test(x: Int) = { count += 1; true } + + object Foo { + def unapply(x: Int): Int? = { count += 1; x } + } + + def main(args: Array[String]): Unit = { + val res = List(1, 2).collect { case x if test(x) => x } + assert(count == 2) + + count = 0 + val res2 = List(1, 2).collect { case Foo(x) => x } + assert(count == 2) + } +} diff --git a/tests/run/i8530-b-maybe.scala b/tests/run/i8530-b-maybe.scala new file mode 100644 index 000000000000..94a3fb4fdf94 --- /dev/null +++ b/tests/run/i8530-b-maybe.scala @@ -0,0 +1,23 @@ +//> using options -Yexplicit-nulls +import language.experimental.magic +import scala.magic.Ok +import scala.compiletime.erasedValue + +class MyRegex[Pattern <: String & Singleton/*Literal constant*/]: + inline def unapplySeq(s: CharSequence): List[String?]? = + inline erasedValue[Pattern] match + case "foo" => if s == "foo" then Nil else null + case _ => valueOf[Pattern].r.unapplySeq(s) match + case Some(xs) => Ok(xs) + case None => null + +@main def Test: Unit = + val myRegexp1 = new MyRegex["foo"] + val myRegexp2 = new MyRegex["f(o+)"] + "foo" match + case myRegexp1() => // Match ok + case myRegexp2(x) => ??? + "foooo" match + case myRegexp1() => ??? + case myRegexp2(x) => + assert(x == "oooo") \ No newline at end of file diff --git a/tests/run/i8530-maybe.check b/tests/run/i8530-maybe.check new file mode 100644 index 000000000000..34749b91c683 --- /dev/null +++ b/tests/run/i8530-maybe.check @@ -0,0 +1,7 @@ +MyBoooleanUnapply +2 +3 +(4,5) +5 +6 +7 diff --git a/tests/run/i8530-maybe.scala b/tests/run/i8530-maybe.scala new file mode 100644 index 000000000000..3299e36efcf4 --- /dev/null +++ b/tests/run/i8530-maybe.scala @@ -0,0 +1,48 @@ +//> using options -Yexplicit-nulls +import language.experimental.magic +import scala.magic.* +object MyBoooleanUnapply: + inline def unapply(x: Int): Boolean = true + +object MyOptionUnapply: + inline def unapply(x: Int): Long? = x.toLong + +object MyPolyUnapply: + inline def unapply[T](x: T): T? = Ok(x) + +object MySeqUnapply: + inline def unapplySeq(x: Int): Seq[Int] = Seq(x, x + 1) + +object MyWhiteboxUnapply: + transparent inline def unapply(x: Int): Any? = Ok(x) + +object MyWhiteboxUnapply1: + transparent inline def unapply(using DummyImplicit)(x: Int)(using DummyImplicit): Any? = Ok(x) + +object MyWhiteboxUnapply2: + transparent inline def unapply(using DummyImplicit)(using DummyImplicit)(x: Int)(using DummyImplicit)(using DummyImplicit): Any? = Ok(x) + + +@main def Test = + 1 match + case MyBoooleanUnapply() => println("MyBoooleanUnapply") + + 2 match + case MyOptionUnapply(y) => println(y) + + 3 match + case MyPolyUnapply(a) => println(a) + + 4 match + case MySeqUnapply(a, b) => println((a, b)) + + 5 match + case MyWhiteboxUnapply(x) => println(x: Int) + + 6 match + case MyWhiteboxUnapply1(x) => println(x: Int) + + 7 match + case MyWhiteboxUnapply2(x) => println(x: Int) + +end Test diff --git a/tests/run/i8577a-maybe.scala b/tests/run/i8577a-maybe.scala new file mode 100644 index 000000000000..9b91f614f3fe --- /dev/null +++ b/tests/run/i8577a-maybe.scala @@ -0,0 +1,15 @@ +//> using options -Yexplicit-nulls +import language.experimental.magic +object Macro: + opaque type StrCtx = StringContext + def apply(ctx: StringContext): StrCtx = ctx + def unapply(ctx: StrCtx): StringContext? = ctx + +extension (ctx: StringContext) def mac: Macro.StrCtx = Macro(ctx) +extension (inline ctx: Macro.StrCtx) inline def unapplySeq(inline input: Int): Seq[Int]? = + Seq(input) + +@main def Test: Unit = + val mac"$x" = 1.runtimeChecked + val y: Int = x + assert(x == 1) diff --git a/tests/run/i8577i-maybe.scala b/tests/run/i8577i-maybe.scala new file mode 100644 index 000000000000..b38c8d862fc0 --- /dev/null +++ b/tests/run/i8577i-maybe.scala @@ -0,0 +1,16 @@ +//> using options -Yexplicit-nulls +import language.experimental.magic +object Macro: + opaque type StrCtx = StringContext + def apply(ctx: StringContext): StrCtx = ctx + def unapply(ctx: StrCtx): StringContext? = ctx + +extension (ctx: StringContext) def mac: Macro.StrCtx = Macro(ctx) +extension (inline ctx: Macro.StrCtx) transparent inline def unapplySeq(inline input: String): Seq[Any]? = + Seq(123) + +@main def Test: Unit = + "abc" match + case mac"$x" => + val y: Int = x + assert(x == 123) diff --git a/tests/run/maybe-numeric-widening.check b/tests/run/maybe-numeric-widening.check new file mode 100644 index 000000000000..d3c284e753f4 --- /dev/null +++ b/tests/run/maybe-numeric-widening.check @@ -0,0 +1 @@ +got: 3 diff --git a/tests/run/maybe-numeric-widening.scala b/tests/run/maybe-numeric-widening.scala new file mode 100644 index 000000000000..c611c84b310d --- /dev/null +++ b/tests/run/maybe-numeric-widening.scala @@ -0,0 +1,12 @@ +//> using options -Yexplicit-nulls +import language.experimental.magic +import scala.magic.* + +object WidenBug: + def f(x: Int): Long? = x // no numeric widening inserted + +object Test: + def main(args: Array[String]): Unit = + WidenBug.f(3) match + case Ok(v) => println("got: " + (v: Long)) + case null => println("null") diff --git a/tests/run/maybe-widening.check b/tests/run/maybe-widening.check new file mode 100644 index 000000000000..1e3e8f58856e --- /dev/null +++ b/tests/run/maybe-widening.check @@ -0,0 +1 @@ +matched: 2 diff --git a/tests/run/maybe-widening.scala b/tests/run/maybe-widening.scala new file mode 100644 index 000000000000..10fbbaec2a03 --- /dev/null +++ b/tests/run/maybe-widening.scala @@ -0,0 +1,19 @@ +//> using options -Yexplicit-nulls +import language.experimental.magic + +trait Peano: + type Nat + val Succ: SuccExtractor + trait SuccExtractor: + def unapply(nat: Nat): Nat? + +object IntNums extends Peano: + type Nat = Int + object Succ extends SuccExtractor: + def unapply(nat: Nat) = nat - 1 + +object Test: + def main(args: Array[String]): Unit = + (3: IntNums.Nat) match + case IntNums.Succ(v) => println("matched: " + v) + case _ => println("no match") diff --git a/tests/run/maybe.check b/tests/run/maybe.check new file mode 100644 index 000000000000..8b57e8864bec --- /dev/null +++ b/tests/run/maybe.check @@ -0,0 +1,19 @@ +Some(s) +None +Some(s) +Some(s) +None +Some(ss) +pos 1 1 +neg -1 +nonempty abc bc +empty +poly abc = bc +poly 22 = 22 +nopoly +nopoly -1 +poly2 abc = bc +poly2 22 = 22 +nopoly2 +nopoly2 -1 +nopoly2 true diff --git a/tests/run/maybe.scala b/tests/run/maybe.scala new file mode 100644 index 000000000000..854c532e0af5 --- /dev/null +++ b/tests/run/maybe.scala @@ -0,0 +1,103 @@ +//> using options -Yexplicit-nulls +import language.experimental.magic +import scala.magic.* + + +class C: + def toOptionAny[T](x: Any): Option[Any] = x match + case Ok(y) => Some(y) + case null => None + +def toOptionStr(x: String?): Option[String] = x match + case null => None + case Ok(y) => Some(y) + +def toOption[T](x: T?): Option[T] = x match + case Ok(y) => Some(y) + case null => None + +def toOptionAny[T](x: Any): Option[Any] = x match + case Ok(y) => Some(y) + case null => None + +object Pos: + def unapply(x: Int): Int? = + if x >= 0 then x else null + +object WithTail: + def unapply(s: String): String? = + if s.isEmpty then null else s.substring(1) + +object Poly: + def unapply[T](x: T): T? = x match + case Pos(y: T @unchecked) => Ok(y) + case WithTail(s: T @unchecked) => Ok(s) + case _ => null + +def f[T](x: T) = + val x1 = toOption(Ok("s")) + val x2 = toOption(null) + val x3 = toOption("s") + + val y1 = toOptionStr(Ok("s")) + val y2 = toOptionStr(null) + + val z1 = toOption(Ok(x)) + println(x1) + println(x2) + println(x3) + println(y1) + println(y2) + println(z1) + +def posTest(x: Int) = x match + case Pos(y) => println(s"pos $x $y") + case _ => println(s"neg $x") + +def strTest(x: String) = x match + case WithTail(y) => println(s"nonempty $x $y") + case _ => println(s"empty $x") + +def polyTest(s: String, n: Int) = + s match + case Poly(s1) => + val _: String = s1 + println(s"poly $s = $s1") + case _ => + println(s"nopoly $s") + n match + case Poly(n1) => + val _: Int = n1 + println(s"poly $n = $n1") + case _ => + println(s"nopoly $n") + true match + case Poly(x) => + assert(false) + case _ => + +def polyTest2[T](x: T) = x match + case Poly(x1) => + println(s"poly2 $x = $x1") + case _ => + println(s"nopoly2 $x") + +@main def Test = + f("ss") + posTest(1) + posTest(-1) + strTest("abc") + strTest("") + polyTest("abc", 22) + polyTest("", -1) + polyTest2("abc") + polyTest2(22) + polyTest2("") + polyTest2(-1) + polyTest2(true) + + + + + + diff --git a/tests/run/named-patterns-maybe.check b/tests/run/named-patterns-maybe.check new file mode 100644 index 000000000000..9ccc08d67069 --- /dev/null +++ b/tests/run/named-patterns-maybe.check @@ -0,0 +1,20 @@ +name Bob, age 22 +name Bob +age 22 +age 22, name Bob +Bob, 22 +name Bob, age 22 +name Bob +age 22 +age 22, name Bob +Bob, 22 +1003 Lausanne, Rue de la Gare 44 +1003 Lausanne +Rue de la Gare in Lausanne +1003 Lausanne, Rue de la Gare 44 +1003 Lausanne, Rue de la Gare 44 +Bob, aged 22, in 1003 Lausanne, Rue de la Gare 44 +Bob in 1003 Lausanne +aged 22 in Rue de la Gare in Lausanne +Bob, aged 22 in 1003 Lausanne, Rue de la Gare 44 +Bob, aged 22 in 1003 Lausanne, Rue de la Gare 44 diff --git a/tests/run/named-patterns-maybe.scala b/tests/run/named-patterns-maybe.scala new file mode 100644 index 000000000000..01495966ebf7 --- /dev/null +++ b/tests/run/named-patterns-maybe.scala @@ -0,0 +1,75 @@ +//> using options -Yexplicit-nulls + +import language.experimental.magic +object Test1: + class Person(val name: String, val age: Int) + + object Person: + def unapply(p: Person): (name: String, age: Int) = (p.name, p.age) + + class Person2(val name: String, val age: Int) + object Person2: + def unapply(p: Person2): (name: String, age: Int)? = (p.name, p.age) + + case class Address(city: String, zip: Int, street: String, number: Int) + + @main def Test = + val bob = Person("Bob", 22) + bob match + case Person(name = n, age = a) => println(s"name $n, age $a") + bob match + case Person(name = n) => println(s"name $n") + bob match + case Person(age = a) => println(s"age $a") + bob match + case Person(age = a, name = n) => println(s"age $a, name $n") + bob match + case Person(age, name) => println(s"$age, $name") + + val bob2 = Person2("Bob", 22) + bob2 match + case Person2(name = n, age = a) => println(s"name $n, age $a") + bob2 match + case Person2(name = n) => println(s"name $n") + bob2 match + case Person2(age = a) => println(s"age $a") + bob2 match + case Person2(age = a, name = n) => println(s"age $a, name $n") + bob2 match + case Person2(age, name) => println(s"$age, $name") + + val addr = Address("Lausanne", 1003, "Rue de la Gare", 44) + addr match + case Address(city = c, zip = z, street = s, number = n) => + println(s"$z $c, $s $n") + addr match + case Address(zip = z, city = c) => + println(s"$z $c") + addr match + case Address(city = c, street = s) => + println(s"$s in $c") + addr match + case Address(number = n, street = s, zip = z, city = c) => + println(s"$z $c, $s $n") + addr match + case Address(c, z, s, number) => + println(s"$z $c, $s $number") + + type Person3 = (p: Person2, addr: Address) + + val p3 = (p = bob2, addr = addr) + p3 match + case (addr = Address(city = c, zip = z, street = s, number = n), p = Person2(name = nn, age = a)) => + println(s"$nn, aged $a, in $z $c, $s $n") + p3 match + case (p = Person2(name = nn), addr = Address(zip = z, city = c)) => + println(s"$nn in $z $c") + p3 match + case (p = Person2(age = a), addr = Address(city = c, street = s)) => + println(s"aged $a in $s in $c") + p3 match + case (Person2(age = a, name = nn), Address(number = n, street = s, zip = z, city = c)) => + println(s"$nn, aged $a in $z $c, $s $n") + p3 match + case (Person2(nn, a), Address(c, z, s, number)) => + println(s"$nn, aged $a in $z $c, $s $number") diff --git a/tests/run/orelse.check b/tests/run/orelse.check new file mode 100644 index 000000000000..a9612c94c3af --- /dev/null +++ b/tests/run/orelse.check @@ -0,0 +1,33 @@ +==== x +Right(s) +Left(()) +Right(s) +Left(bad) +==== y +Right(1) +Right(22) +Left(bad) +==== z +Right(ss) +Left(BAD) +==== a +Right(true) +Left(not a boolean) +Left(bad) +==== pos +pos 1 1 +neg -1 +==== str +nonempty abc bc +empty +==== poly +poly abc = bc +poly 22 = 22 +nopoly +nopoly -1 +==== poly2 +poly2 abc = bc +poly2 22 = 22 +nopoly2 +nopoly2 -1 +nopoly2 true diff --git a/tests/run/orelse.scala b/tests/run/orelse.scala new file mode 100644 index 000000000000..b5abf5957ffd --- /dev/null +++ b/tests/run/orelse.scala @@ -0,0 +1,134 @@ +// scalajs: --skip +// scalajs needs to be diasabled since `null` gives an Err(undefined) instead of an Err(()) +// TODO: Figure out how to fix this under scalajs +//> using options -Yexplicit-nulls +import language.experimental.magic +import scala.magic.* +import scala.util.Either + +class C: + def toEitherAny[T](x: Any): Either[Any, Any] = x match + case Ok(y) => Right(y) + case Err(e) => Left(e) + +def toEitherIntStr(x: Int ? String): Either[String, Int] = x match + case Ok(y) => Right(y) + case Err(e) => Left(e) + +def toEither[T, E](x: T ? E): Either[E, T] = x match + case Err(e) => Left(e) + case Ok(y) => Right(y) + +def toEitherBooleanStr[T](x: T ? String): Either[String, Boolean] = x match + case Ok(x: Boolean) => Right(x) + case Err(e) => Left(e) + case _ => Left("not a boolean") + +extension [T, E](x: T ? E) + def withErr[E1](e1: => E1): T ? E1 = x match + case Ok(y) => Ok(y) + case Err(e) => Err(e1) + +object Pos: + def unapply(x: Int): Int? = + if x >= 0 then x else null + +object WithTail: + def unapply(s: String): String? = + if s.isEmpty then null else s.substring(1) + +object Poly: + def unapply[T](x: T): T ? String = x match + case Pos(y: T @unchecked) => Ok(y) + case WithTail(s: T @unchecked) => Ok(s) + case _ => Err("no match") + +def f[T, E](x: T, e: E) = + val x1 = toEither(Ok("s")) + val x2 = toEither(null) + val x3 = toEither("s") + val x4 = toEither(Err("bad")) + + val y1 = toEitherIntStr(Ok(1)) + val y3 = toEitherIntStr(22) + val y4 = toEitherIntStr(Err("bad")) + + val z1 = toEither(Ok(x)) + val z2 = toEither(Err(e)) + + val a1 = toEitherBooleanStr(Ok(true)) + val a2 = toEitherBooleanStr(Ok(1)) + val a3 = toEitherBooleanStr(Err("bad")) + + println("==== x") + println(x1) + println(x2) + println(x3) + println(x4) + println("==== y") + println(y1) + println(y3) + println(y4) + println("==== z") + println(z1) + println(z2) + println("==== a") + println(a1) + println(a2) + println(a3) + +def posTest(x: Int) = x match + case Pos(y) => println(s"pos $x $y") + case _ => println(s"neg $x") + +def strTest(x: String) = x match + case WithTail(y) => println(s"nonempty $x $y") + case _ => println(s"empty $x") + +def polyTest(s: String, n: Int) = + s match + case Poly(s1) => + val _: String = s1 + println(s"poly $s = $s1") + case _ => + println(s"nopoly $s") + n match + case Poly(n1) => + val _: Int = n1 + println(s"poly $n = $n1") + case _ => + println(s"nopoly $n") + true match + case Poly(x) => + assert(false) + case _ => + +def polyTest2[T](x: T) = x match + case Poly(x1) => + println(s"poly2 $x = $x1") + case _ => + println(s"nopoly2 $x") + +@main def Test = + f("ss", "BAD") + println("==== pos") + posTest(1) + posTest(-1) + println("==== str") + strTest("abc") + strTest("") + println("==== poly") + polyTest("abc", 22) + polyTest("", -1) + println("==== poly2") + polyTest2("abc") + polyTest2(22) + polyTest2("") + polyTest2(-1) + polyTest2(true) + + + + + + diff --git a/tests/run/postfix-qmark.scala b/tests/run/postfix-qmark.scala new file mode 100644 index 000000000000..8ad843bc07f5 --- /dev/null +++ b/tests/run/postfix-qmark.scala @@ -0,0 +1,22 @@ +//> using options -Yexplicit-nulls +import language.experimental.magic + +case class C(elem: Int): + def ? (y: Int) = C(elem + y) + def ? = C(elem) + +def foo(x: C, y: C) = () + +@main def Test = + val c = C(1) + val x = c ? 1 + assert(x.elem == 2) + val y = c ? + identity(1) + assert(y.elem == 1) + val z = c + ? 1 + assert(z.elem == 2) + foo(c?, c?) + val xx = c? + println("done") diff --git a/tests/run/string-extractor-maybe.check b/tests/run/string-extractor-maybe.check new file mode 100644 index 000000000000..47f3722c86d8 --- /dev/null +++ b/tests/run/string-extractor-maybe.check @@ -0,0 +1,9 @@ +by +BY +oTheClown +nope +1: ob +2: obby +2: OBBY +3: BOBO +3: TomTomTheClown diff --git a/tests/run/string-extractor-maybe.scala b/tests/run/string-extractor-maybe.scala new file mode 100644 index 000000000000..84dedd606f52 --- /dev/null +++ b/tests/run/string-extractor-maybe.scala @@ -0,0 +1,65 @@ +//> using options -Yexplicit-nulls +import language.experimental.magic +final class StringExtract(val s: String) extends AnyVal { + def length = s.length + def lengthCompare(n: Int) = s.length compare n + def apply(idx: Int): Char = s.charAt(idx) + def head: Char = s.charAt(0) + def tail: String = s drop 1 + def drop(n: Int): Seq[Char] = toSeq.drop(n) + def toSeq: Seq[Char] = s.toSeq + + override def toString = s +} + +final class ThreeStringExtract(val s: String) extends AnyVal { + def get: (List[Int], Double, Seq[Char]) = ((s.length :: Nil, s.length.toDouble, toSeq)) + def length = s.length + def lengthCompare(n: Int) = s.length compare n + def apply(idx: Int): Char = s.charAt(idx) + def head: Char = s.charAt(0) + def tail: String = s drop 1 + def drop(n: Int): Seq[Char] = toSeq.drop(n) + def toSeq: Seq[Char] = s.toSeq + + override def toString = s +} + + +object Bippy { + def unapplySeq(x: Any): StringExtract? = + if (x == null) || (x == "") then null + else new StringExtract("" + x) +} +object TripleBippy { + def unapplySeq(x: Any): (List[Int], Double, Seq[Char])? = + if (x == null) || (x == "") then null + else new ThreeStringExtract("" + x).get +} + +object Test { + def f(x: Any) = x match { + case Bippy('B' | 'b', 'O' | 'o', 'B' | 'b', xs *) => xs + case _ => "nope" + } + + def g(x: Any): String = x match { + case TripleBippy(3 :: Nil, 3.0, 'b', chars *) => "1: " + chars + case TripleBippy(5 :: Nil, 5.0, 'b' | 'B', chars *) => "2: " + chars + case TripleBippy(_, _, chars *) => "3: " + chars + case _ => "nope" + } + + def main(args: Array[String]): Unit = { + println(f("Bobby")) + println(f("BOBBY")) + println(f("BoBoTheClown")) + println(f("TomTomTheClown")) + + println(g("bob")) + println(g("bobby")) + println(g("BOBBY")) + println(g("BOBO")) + println(g("TomTomTheClown")) + } +} diff --git a/tests/run/t6111-maybe.check b/tests/run/t6111-maybe.check new file mode 100644 index 000000000000..7fd2e33526c3 --- /dev/null +++ b/tests/run/t6111-maybe.check @@ -0,0 +1,2 @@ +(8,8) +(x,x) diff --git a/tests/run/t6111-maybe.scala b/tests/run/t6111-maybe.scala new file mode 100644 index 000000000000..e9b0811f7102 --- /dev/null +++ b/tests/run/t6111-maybe.scala @@ -0,0 +1,31 @@ +//> using options -Yexplicit-nulls +// SI-6675 DEPRECATED AUTO-TUPLING BECAUSE BAD IDEA -- MEAMAXIMACULPA +// TODO: remove this test case in 2.12, when the deprecation will go into effect and this will no longer compile +// slightly overkill, but a good test case for implicit resolution in extractor calls, +// along with the real fix: an extractor pattern with 1 sub-pattern should type check for all extractors +// that return Option[T], whatever T (even if it's a tuple) +import language.experimental.magic +import scala.magic.* +object Foo { + def unapply[S, T](scrutinee: S)(using evidence: FooHasType[S, T]): T? = scrutinee match { + case i: Int => Ok((i, i).asInstanceOf[T]) + } +} + +class FooHasType[S, T] +object FooHasType { + given int: FooHasType[Int, (Int, Int)] = new FooHasType[Int, (Int, Int)] +} + +// resurrected from neg/t997 +object Foo997 { def unapply(x : String): (String, String)? = (x, x) } + +object Test extends App { + val x = 8 + println(x match { + case Foo(p) => p // p should be a pair of Int + }) + + // Prints '{x, x}' + "x" match { case Foo997(a) => println(a) } +} diff --git a/tests/run/tryPatternMatch-maybe.check b/tests/run/tryPatternMatch-maybe.check new file mode 100644 index 000000000000..44f7b7d5ac10 --- /dev/null +++ b/tests/run/tryPatternMatch-maybe.check @@ -0,0 +1,20 @@ +success 1 +success 2 +success 3 +success 4 +success 5 +success 6 +success 7 +success 8 +success 9.1 +success 9.2 +IllegalArgumentException: abc +IllegalArgumentException +NullPointerException | IOException +NoSuchElementException +EX +InnerException +NullPointerException +ExceptionTrait +ClassCastException +TimeoutException escaped diff --git a/tests/run/tryPatternMatch-maybe.scala b/tests/run/tryPatternMatch-maybe.scala new file mode 100644 index 000000000000..692faa9f25fb --- /dev/null +++ b/tests/run/tryPatternMatch-maybe.scala @@ -0,0 +1,143 @@ +//> using options -Yexplicit-nulls +// scalajs: --compliant-semantics + +import language.experimental.magic +import java.io.IOException +import java.util.concurrent.TimeoutException + +object IAE { + def unapply(e: Exception): String? = + if e.isInstanceOf[IllegalArgumentException] && e.getMessage != null then e.getMessage + else null +} + +object EX extends Exception { + val msg = "a" + class InnerException extends Exception(msg) +} + +trait ExceptionTrait extends Exception + +trait TestTrait { + type ExceptionType <: Exception + + def traitTest(): Unit = { + try { + throw new IOException + } catch { + case _: ExceptionType => println("success 9.2") + case _ => println("failed 9.2") + } + } +} + +object Test extends TestTrait { + type ExceptionType = IOException + + def main(args: Array[String]): Unit = { + var a: Int = 1 + + try { + throw new Exception("abc") + } catch { + case _: Exception => println("success 1") + case _ => println("failed 1") + } + + try { + throw new Exception("abc") + } catch { + case e: Exception => println("success 2") + case _ => println("failed 2") + } + + try { + throw new Exception("abc") + } catch { + case e: Exception if e.getMessage == "abc" => println("success 3") + case _ => println("failed 3") + } + + try { + throw new Exception("abc") + } catch { + case e: Exception if e.getMessage == "" => println("failed 4") + case _ => println("success 4") + } + + try { + throw EX + } catch { + case EX => println("success 5") + case _ => println("failed 5") + } + + try { + throw new EX.InnerException + } catch { + case _: EX.InnerException => println("success 6") + case _ => println("failed 6") + } + + try { + throw new NullPointerException + } catch { + case _: NullPointerException | _:IOException => println("success 7") + case _ => println("failed 7") + } + + try { + throw new ExceptionTrait {} + } catch { + case _: ExceptionTrait => println("success 8") + case _ => println("failed 8") + } + + try { + throw new IOException + } catch { + case _: ExceptionType => println("success 9.1") + case _ => println("failed 9.1") + } + + traitTest() // test 9.2 + + def testThrow(throwIt: => Unit): Unit = { + try { + throwIt + } catch { + // These cases will be compiled as catch cases + case e: NullPointerException => println("NullPointerException") + case e: IndexOutOfBoundsException => println("IndexOutOfBoundsException") + case _: NoSuchElementException => println("NoSuchElementException") + case _: EX.InnerException => println("InnerException") + // All the following will be compiled as a match + case IAE(msg) => println("IllegalArgumentException: " + msg) + case _: ExceptionTrait => println("ExceptionTrait") + case e: IOException if e.getMessage == null => println("IOException") + case _: NullPointerException | _:IOException => println("NullPointerException | IOException") +// case `a` => println("`a`") + case EX => println("EX") + case e: IllegalArgumentException => println("IllegalArgumentException") + case _: ClassCastException => println("ClassCastException") + } + } + + testThrow(throw new IllegalArgumentException("abc")) + testThrow(throw new IllegalArgumentException()) + testThrow(throw new IOException("abc")) + testThrow(throw new NoSuchElementException()) + testThrow(throw EX) + testThrow(throw new EX.InnerException) + testThrow(throw new NullPointerException()) + testThrow(throw new ExceptionTrait {}) + testThrow(throw a.asInstanceOf[Throwable]) + try { + testThrow(throw new TimeoutException) + println("TimeoutException did not escape") + } catch { + case _: TimeoutException => println("TimeoutException escaped") + } + } + +} diff --git a/tests/run/type-test-nat-maybe.check b/tests/run/type-test-nat-maybe.check new file mode 100644 index 000000000000..5dc5e167eeae --- /dev/null +++ b/tests/run/type-test-nat-maybe.check @@ -0,0 +1,6 @@ +Some((SuccClass(SuccClass(ZeroObject)),SuccClass(ZeroObject))) +Some((ZeroObject,SuccClass(SuccClass(ZeroObject)))) +None +Some((2,1)) +Some((0,2)) +None diff --git a/tests/run/type-test-nat-maybe.scala b/tests/run/type-test-nat-maybe.scala new file mode 100644 index 000000000000..9ee8faa29b23 --- /dev/null +++ b/tests/run/type-test-nat-maybe.scala @@ -0,0 +1,132 @@ +//> using options -Yexplicit-nulls +import language.experimental.magic +import scala.reflect.TypeTest + +object Test { + def main(args: Array[String]): Unit = { + app(ClassNums) + app(IntNums) + } + + def app(peano: Peano): Unit = { + import peano.* + def divOpt(m: Nat, n: Nat): Option[(Nat, Nat)] = { + n match { + case Zero => None + case s @ Succ(_) => Some(safeDiv(m, s)) + } + } + val two = Succ(Succ(Zero)) + val five = Succ(Succ(Succ(two))) + println(divOpt(five, two)) + println(divOpt(two, five)) + println(divOpt(two, Zero)) + } +} + +trait Peano { + type Nat + + type Zero <: Nat + given TypeTest[Nat, Zero] = typeTestOfZero + + type Succ <: Nat + given TypeTest[Nat, Succ] = typeTestOfSucc + + def safeDiv(m: Nat, n: Succ): (Nat, Nat) + + protected def typeTestOfZero: TypeTest[Nat, Zero] + protected def typeTestOfSucc: TypeTest[Nat, Succ] + + def succDeco(succ: Succ): SuccAPI + trait SuccAPI { + def pred: Nat + } + + val Zero: Zero + + val Succ: SuccExtractor + trait SuccExtractor { + def apply(nat: Nat): Succ + def unapply(nat: Succ): Nat? + } +} + +object IntNums extends Peano { + type Nat = Int + type Zero = Int + type Succ = Int + + protected def typeTestOfZero: TypeTest[Nat, Zero] = new { + def unapply(x: Nat): Option[x.type & Zero] = + if x == 0 then Some(x) + else None + } + + protected def typeTestOfSucc: TypeTest[Nat, Succ] = new { + def unapply(x: Nat): Option[x.type & Succ] = + if x > 0 then Some(x) + else None + } + + def safeDiv(m: Nat, n: Succ): (Nat, Nat) = (m / n, m % n) + + val Zero: Zero = 0 + + object Succ extends SuccExtractor { + def apply(nat: Nat): Succ = nat + 1 + def unapply(nat: Succ): Nat? = nat - 1 + } + def succDeco(succ: Succ): SuccAPI = new SuccAPI { + def pred: Nat = succ - 1 + } +} + +object ClassNums extends Peano { + trait NatTrait + object ZeroObject extends NatTrait { + override def toString: String = "ZeroObject" + } + case class SuccClass(predecessor: NatTrait) extends NatTrait + + type Nat = NatTrait + type Zero = ZeroObject.type + type Succ = SuccClass + + protected def typeTestOfZero: TypeTest[Nat, Zero] = new { + def unapply(x: Nat): Option[x.type & Zero] = x match + case x: (ZeroObject.type & x.type) => Some(x) + case _ => None + } + + protected def typeTestOfSucc: TypeTest[Nat, Succ] = new { + def unapply(x: Nat): Option[x.type & Succ] = x match + case x: (SuccClass & x.type) => Some(x) + case _ => None + } + + def safeDiv(m: Nat, n: Succ): (Nat, Nat) = { + def intValue(x: Nat, acc: Int): Int = x match { + case nat: SuccClass => intValue(nat.predecessor, acc + 1) + case _ => acc + } + def natValue(x: Int): Nat = + if x == 0 then ZeroObject + else new SuccClass(natValue(x - 1)) + val i = intValue(m, 0) + val j = intValue(n, 0) + (natValue(i / j), natValue(i % j)) + } + + val Zero: Zero = ZeroObject + + object Succ extends SuccExtractor { + def apply(nat: Nat): Succ = new SuccClass(nat) + def unapply(nat: Succ): Nat? = nat.predecessor + } + + def succDeco(succ: Succ): SuccAPI = new SuccAPI { + def pred: Nat = succ.predecessor + } + +} diff --git a/tests/run/unapply-maybe.scala b/tests/run/unapply-maybe.scala new file mode 100644 index 000000000000..345ffbc514d1 --- /dev/null +++ b/tests/run/unapply-maybe.scala @@ -0,0 +1,124 @@ +//> using options -Yexplicit-nulls +import language.experimental.magic +object Test { + def main(args: Array[String]): Unit = { + Foo.run() + Mas.run() + LisSeqArr.run() + StreamFoo.run() + Test1256.run() + } +} + +// this class is used for representation +class Bar { + var size: Int = 50 + var name: String = "medium" +} + +// test basic unapply for 0, 1 and 2 args and with precise type test +object Fii { + def unapply(x: Any): Boolean = x.isInstanceOf[Bar] +} +object Faa { + def unapply(x: Any): String? = if x.isInstanceOf[Bar] then x.asInstanceOf[Bar].name else null +} +object FaaPrecise { + def unapply(x: Bar): String? = x.name +} +object FaaPreciseSome { + def unapply(x: Bar) = Some(x.name) // return type Some[String] +} +object VarFoo { + def unapply(a : Int)(using b : Int) : Int? = a + b +} + +object Foo { + def unapply(x: Any): Product2[Int, String]? = x match { + case y: Bar => (y.size, y.name) + case _ => null + } + def doMatch1(b:Bar) = b match { + case Foo(s:Int, n:String) => (s,n) + } + def doMatch2(b:Bar) = b match { + case Fii() => null + } + def doMatch3(b:Bar) = b match { + case Faa(n:String) => n + } + def doMatch4(b:Bar) = (b:Any) match { + case FaaPrecise(n:String) => n + } + def doMatch5(b:Bar) = (b:Any) match { + case FaaPreciseSome(n:String) => n + } + def run(): Unit = { + val b = new Bar + assert(doMatch1(b) == (50,"medium")) + assert(doMatch2(b) == null) + assert(doMatch3(b) == "medium") + assert(doMatch4(b) == "medium") + assert(doMatch5(b) == "medium") + given bc: Int = 3 + assert(7 == (4 match { + case VarFoo(x) => x + })) + } +} + +// same, but now object is not top-level +object Mas { + object Gaz { + def unapply(x: Any): Product2[Int, String]? = x match { + case y: Baz => (y.size, y.name) + case _ => null + } + } + class Baz { + var size: Int = 60 + var name: String = "too large" + } + def run(): Unit = { + val b = new Baz + assert((60,"too large") == (b match { + case Gaz(s:Int, n:String) => (s,n) + })) + } +} + +object LisSeqArr { + def run(): Unit = { + assert((1,2) == ((List(1,2,3): Any) match { case List(x,y,_*) => (x,y)})) + assert((1,2) == ((List(1,2,3): Any) match { case Seq(x,y,_*) => (x,y)})) + } +} + +object StreamFoo { + def sum(lazyList: LazyList[Int]): Int = + lazyList match { + case ll if ll.isEmpty => 0 + case LazyList.cons(hd, tl) => hd + sum(tl) + } + def run(): Unit = { + val str: LazyList[Int] = List(1,2,3).to(LazyList) + assert(6 == sum(str)) + } +} + +object Test1256 { + class Sync { + def unapply(scrut: Any): Boolean = false + } + + class Buffer { + val Get = new Sync + val jp: PartialFunction[Any, Any] = { + case Get() => + } + } + + def run(): Unit = { + assert(!(new Buffer).jp.isDefinedAt(42)) + } +} diff --git a/tests/run/unapply-tparam-maybe.scala b/tests/run/unapply-tparam-maybe.scala new file mode 100644 index 000000000000..cea24b7e6b82 --- /dev/null +++ b/tests/run/unapply-tparam-maybe.scala @@ -0,0 +1,38 @@ +//> using options -Yexplicit-nulls +import language.experimental.magic +class Foo[T] { + def unapply(x: Int): Int? = 4 +} + +object Foo { + def unapply[T](x: T): Int? = 5 +} + +object Bar { + def unapply[T](x: T): Int? = 5 +} + +class Baz[T] { + def unapply(x: Int): Int? = 4 +} + +object Baz + + +object Test extends App { + 1 match { + case Foo(x) => assert(x == 5) + } + + 1 match { + case Foo[Int](x) => assert(x == 5) // Type params on object takes precedence + } + + 1 match { + case Bar[Int](x) => assert(x == 5) + } + + 1 match { + case Baz[Int](x) => assert(x == 4) // Otherwise type params are for the class + } +} \ No newline at end of file diff --git a/tests/run/virtpatmat_unapply-maybe.check b/tests/run/virtpatmat_unapply-maybe.check new file mode 100644 index 000000000000..2b89b77d1e92 --- /dev/null +++ b/tests/run/virtpatmat_unapply-maybe.check @@ -0,0 +1,2 @@ +1 +6 diff --git a/tests/run/virtpatmat_unapply-maybe.scala b/tests/run/virtpatmat_unapply-maybe.scala new file mode 100644 index 000000000000..0350506ce971 --- /dev/null +++ b/tests/run/virtpatmat_unapply-maybe.scala @@ -0,0 +1,34 @@ +//> using options -Yexplicit-nulls +import language.experimental.magic +class IntList(val hd: Int, val tl: IntList) +object NilIL extends IntList(0, null.asInstanceOf[IntList]) +object IntList { + def unapply(il: IntList): (Int, IntList)? = if il eq NilIL then null else (il.hd, il.tl) + def apply(x: Int, xs: IntList) = new IntList(x, xs) +} + +object Test extends App { + IntList(1, IntList(2, NilIL)) match { + case IntList(a1, IntList(a2, IntList(a3, y))) => println(a1 + a2 + a3) + case IntList(x, y) => println(x) + } + + IntList(1, IntList(2, IntList(3, NilIL))) match { + case IntList(a1, IntList(a2, IntList(a3, y))) => println(a1 + a2 + a3) + case IntList(x, y) => println(x) + } +} + +// ((x1: IntList) => IntList.unapply(x1).flatMap(((x4: (Int, IntList)) => IntList.unapply(x4._2).flatMap(((x5: (Int, IntList)) => IntList.unapply(x5._2).flatMap(((x6: (Int, IntList)) => implicitly[Predef.MatchingStrategy[Option]].success(Predef.println(x4._1.+(x5._1).+(x6._1))))))))).orElse(IntList.unapply(x1).flatMap(((x7: (Int, IntList)) => implicitly[scala.Predef.MatchingStrategy[Option]].success(Predef.println(x7._1))))).orElse(implicitly[scala.Predef.MatchingStrategy[Option]].fail))(IntList.apply(1, IntList.apply(2, IntList.apply(3, null)))) + +/* + ((x1: IntList) => + IntList.this.unapply(x1).flatMap[Int](((x4: (Int, IntList)) => + IntList.this.unapply(x4._2).flatMap[Int](((x5: (Int, IntList)) => + IntList.this.unapply(x5._2).flatMap[Int](((x6: (Int, IntList)) => + Predef.this.implicitly[scala.Predef.MatchingStrategy[Option]](scala.this.Predef.OptionMatching).success[Int](x6._1))))))).orElse[Int]( + IntList.this.unapply(x1).flatMap[Int](((x7: (Int, IntList)) => + Predef.this.implicitly[scala.Predef.MatchingStrategy[Option]](scala.this.Predef.OptionMatching).success[Int](x7._1)))).orElse[Int]( + Predef.this.implicitly[scala.Predef.MatchingStrategy[Option]](scala.this.Predef.OptionMatching).fail) + ).apply(IntList.apply(1, null)) +*/ diff --git a/tests/warn/i12253-maybe.check b/tests/warn/i12253-maybe.check new file mode 100644 index 000000000000..8d9f140292e1 --- /dev/null +++ b/tests/warn/i12253-maybe.check @@ -0,0 +1,13 @@ +-- [E092] Pattern Match Unchecked Warning: tests/warn/i12253-maybe.scala:15:10 ----------------------------------------- +15 | case extractors.InlinedLambda(_, Select(_, name)) => Expr(name) // warn // warn + | ^ + |the type test for extractors.q2.reflect.Term cannot be checked at runtime because it refers to an abstract type member or type parameter + | + | longer explanation available when compiling with `-explain` +-- [E092] Pattern Match Unchecked Warning: tests/warn/i12253-maybe.scala:15:38 ----------------------------------------- +15 | case extractors.InlinedLambda(_, Select(_, name)) => Expr(name) // warn // warn + | ^ + |the type test for q1.reflect.Select cannot be checked at runtime because it refers to an abstract type member or type parameter + | + | longer explanation available when compiling with `-explain` +there was 1 deprecation warning; re-run with -deprecation for details diff --git a/tests/warn/i12253-maybe.scala b/tests/warn/i12253-maybe.scala new file mode 100644 index 000000000000..6ad4954a70ec --- /dev/null +++ b/tests/warn/i12253-maybe.scala @@ -0,0 +1,33 @@ +//> using options -Yexplicit-nulls + + +import language.experimental.magic +import scala.quoted.{given, *} +import deriving.*, compiletime.* + +object MacroUtils: + transparent inline def extractNameFromSelector[To, T](inline code: To => T) = ${extractNameFromSelectorImpl('code)} + + def extractNameFromSelectorImpl[To: Type, T: Type](code: Expr[To => T])(using q1: Quotes): Expr[String] = + import quotes.reflect.* + val extractors = new Extractors + code.asTerm match + case extractors.InlinedLambda(_, Select(_, name)) => Expr(name) // warn // warn + case t => report.throwError(s"Illegal argument to extractor: ${code.show}, in tasty: $t") + + class Extractors(using val q2: Quotes): + //attempt to strip away consecutive inlines in AST and extract only final lambda + import quotes.reflect.* + + object InlinedLambda: + def unapply(arg: Term): (List[ValDef], Term)? = + arg match + case Inlined(_, _, Lambda(vals, term)) => (vals, term) + case Inlined(_, _, nested) => InlinedLambda.unapply(nested) + case t => null + end InlinedLambda + + end Extractors +end MacroUtils + +// nopos-warn deprecation \ No newline at end of file diff --git a/tests/warn/maybe-conversions.check b/tests/warn/maybe-conversions.check new file mode 100644 index 000000000000..f80aabe96302 --- /dev/null +++ b/tests/warn/maybe-conversions.check @@ -0,0 +1,32 @@ +-- [E029] Pattern Match Exhaustivity Warning: tests/warn/maybe-conversions.scala:6:47 ---------------------------------- +6 |def toOptionMissingNull[T](x: T?): Option[T] = x match // warn + | ^ + | match may not be exhaustive. + | + | It would fail on pattern case: null + | + | longer explanation available when compiling with `-explain` +-- [E029] Pattern Match Exhaustivity Warning: tests/warn/maybe-conversions.scala:9:45 ---------------------------------- +9 |def toOptionMissingOk[T](x: T?): Option[T] = x match // warn + | ^ + | match may not be exhaustive. + | + | It would fail on pattern case: Ok(_) + | + | longer explanation available when compiling with `-explain` +-- [E029] Pattern Match Exhaustivity Warning: tests/warn/maybe-conversions.scala:12:63 --------------------------------- +12 |def toEitherMissingErr(x: Int ? String): Either[String, Int] = x match // warn + | ^ + | match may not be exhaustive. + | + | It would fail on pattern case: Err(_) + | + | longer explanation available when compiling with `-explain` +-- [E029] Pattern Match Exhaustivity Warning: tests/warn/maybe-conversions.scala:15:62 --------------------------------- +15 |def toEitherMissingOk(x: Int ? String): Either[String, Int] = x match // warn + | ^ + | match may not be exhaustive. + | + | It would fail on pattern case: Ok(_) + | + | longer explanation available when compiling with `-explain` diff --git a/tests/warn/maybe-conversions.scala b/tests/warn/maybe-conversions.scala new file mode 100644 index 000000000000..85f35452f467 --- /dev/null +++ b/tests/warn/maybe-conversions.scala @@ -0,0 +1,16 @@ +//> using options -Yexplicit-nulls +import language.experimental.magic +import scala.magic.* +import scala.util.Either + +def toOptionMissingNull[T](x: T?): Option[T] = x match // warn + case Ok(y) => Some(y) + +def toOptionMissingOk[T](x: T?): Option[T] = x match // warn + case null => None + +def toEitherMissingErr(x: Int ? String): Either[String, Int] = x match // warn + case Ok(y) => Right(y) + +def toEitherMissingOk(x: Int ? String): Either[String, Int] = x match // warn + case Err(e) => Left(e) diff --git a/tests/warn/maybe-typetest.check b/tests/warn/maybe-typetest.check new file mode 100644 index 000000000000..9a94900e586d --- /dev/null +++ b/tests/warn/maybe-typetest.check @@ -0,0 +1,46 @@ +-- [E165] Type Warning: tests/warn/maybe-typetest.scala:7:12 ----------------------------------------------------------- +7 | case y: String? => println(y) // warn typetest // warn unmatchable + | ^^^^^^^ + | pattern selector should be an instance of Matchable, + | but it has unmatchable type T instead + | + | longer explanation available when compiling with `-explain` +-- [E165] Type Warning: tests/warn/maybe-typetest.scala:13:12 ---------------------------------------------------------- +13 | case y: Option[String] => println(y)// warn typetest // warn unmatchable + | ^^^^^^^^^^^^^^ + | pattern selector should be an instance of Matchable, + | but it has unmatchable type T instead + | + | longer explanation available when compiling with `-explain` +-- [E030] Match case Unreachable Warning: tests/warn/maybe-typetest.scala:11:9 ----------------------------------------- +11 | case _ => // warn unreachable + | ^ + | Unreachable case +-- [E030] Match case Unreachable Warning: tests/warn/maybe-typetest.scala:17:9 ----------------------------------------- +17 | case _ => // warn unreachable + | ^ + | Unreachable case +-- [E092] Pattern Match Unchecked Warning: tests/warn/maybe-typetest.scala:7:9 ----------------------------------------- +7 | case y: String? => println(y) // warn typetest // warn unmatchable + | ^ + | the type test for String? cannot be checked at runtime because its type arguments can't be determined from T + | + | longer explanation available when compiling with `-explain` +-- [E092] Pattern Match Unchecked Warning: tests/warn/maybe-typetest.scala:10:9 ---------------------------------------- +10 | case y: String? => println(y) // warn typetest? + | ^ + |the type test for String? cannot be checked at runtime because its type arguments can't be determined from T ? Nothing + | + | longer explanation available when compiling with `-explain` +-- [E092] Pattern Match Unchecked Warning: tests/warn/maybe-typetest.scala:13:9 ---------------------------------------- +13 | case y: Option[String] => println(y)// warn typetest // warn unmatchable + | ^ + |the type test for Option[String] cannot be checked at runtime because its type arguments can't be determined from T + | + | longer explanation available when compiling with `-explain` +-- [E092] Pattern Match Unchecked Warning: tests/warn/maybe-typetest.scala:16:9 ---------------------------------------- +16 | case y: Option[String] => println(y)// warn typetest + | ^ + |the type test for Option[String] cannot be checked at runtime because its type arguments can't be determined from Some[T] + | + | longer explanation available when compiling with `-explain` diff --git a/tests/warn/maybe-typetest.scala b/tests/warn/maybe-typetest.scala new file mode 100644 index 000000000000..b0cca5901cd5 --- /dev/null +++ b/tests/warn/maybe-typetest.scala @@ -0,0 +1,18 @@ +//> using options -Yexplicit-nulls +import language.experimental.magic +import scala.magic.* + +def Test[T](x: T) = + x match + case y: String? => println(y) // warn typetest // warn unmatchable + case _ => + Ok(x) match + case y: String? => println(y) // warn typetest? + case _ => // warn unreachable + x match + case y: Option[String] => println(y)// warn typetest // warn unmatchable + case _ => + Some(x) match + case y: Option[String] => println(y)// warn typetest + case _ => // warn unreachable +