From 3b2eae9f735fd7c0c3549a8f928ee912a398a1fb Mon Sep 17 00:00:00 2001 From: odersky Date: Mon, 31 Aug 2026 11:39:23 +0200 Subject: [PATCH 1/6] New error handling New language feature: experimental.errorHandling. It needs -Yexplicit-nulls to be also set This enables the following: - Add maybe types T? and T ? E. - Add amybe prompt and postfix e? operator - Implement subtype rules - Implement erasure rules - Special pattern-matching rules for Ok and Err extractors --- .../dotty/tools/dotc/CompilationUnit.scala | 5 +- .../src/dotty/tools/dotc/ast/Desugar.scala | 5 +- compiler/src/dotty/tools/dotc/ast/tpd.scala | 20 + .../src/dotty/tools/dotc/config/Feature.scala | 30 +- .../dotty/tools/dotc/core/Definitions.scala | 22 +- .../tools/dotc/core/SymDenotations.scala | 18 +- .../dotty/tools/dotc/core/TypeComparer.scala | 23 +- .../dotty/tools/dotc/core/TypeErasure.scala | 31 +- .../src/dotty/tools/dotc/core/Types.scala | 35 +- .../dotty/tools/dotc/parsing/Parsers.scala | 15 +- .../tools/dotc/printing/RefinedPrinter.scala | 14 +- .../dotc/transform/InterceptedMethods.scala | 2 +- .../tools/dotc/transform/PatternMatcher.scala | 191 ++++--- .../tools/dotc/transform/patmat/Space.scala | 56 +- .../src/dotty/tools/dotc/typer/Checking.scala | 4 +- .../src/dotty/tools/dotc/typer/Typer.scala | 9 +- .../dotty/tools/dotc/typer/TyperPhase.scala | 3 + docs/_docs/internals/syntax.md | 3 +- library/src/scala/compiletime/Maybe.scala | 42 ++ library/src/scala/language.scala | 8 +- library/src/scala/maybe.scala | 22 + library/src/scala/runtime/Fail.scala | 6 + library/src/scala/runtime/MaybeCase.scala | 7 + library/src/scala/runtime/Valid.scala | 6 + library/src/scala/util/Err.scala | 19 + library/src/scala/util/Ok.scala | 14 + project/MiMaFilters.scala | 1 + tests/explicit-nulls/pos/opt-maybe.scala | 12 + .../pos/match-complete-maybe.scala | 114 ++++ .../warn/unapply-implicit-arg2-maybe.check | 12 + .../warn/unapply-implicit-arg2-maybe.scala | 16 + tests/neg/bad-unapplies-maybe.check | 39 ++ tests/neg/bad-unapplies-maybe.scala | 30 ++ tests/neg/i14896-maybe.scala | 4 + tests/neg/i1793-maybe.scala | 9 + tests/neg/i21841-maybe.check | 6 + tests/neg/i21841-maybe.scala | 24 + tests/neg/i2378-maybe.scala | 32 ++ tests/neg/i24168-maybe.scala | 14 + tests/neg/i8530-b-maybe.check | 7 + tests/neg/i8530-b-maybe.scala | 11 + tests/neg/i8894-maybe.scala | 15 + tests/neg/orelse-subtyping.check | 9 + tests/neg/orelse-subtyping.scala | 13 + tests/neg/patmat2-maybe.scala | 36 ++ ...table-pattern-binding-messages-maybe.check | 48 ++ ...table-pattern-binding-messages-maybe.scala | 19 + tests/neg/t7868-maybe.scala | 15 + tests/neg/t8128-maybe.check | 4 + tests/neg/t8128-maybe.scala | 8 + tests/neg/unchecked-patterns-maybe.scala | 30 ++ tests/patmat/i2363-maybe.check | 16 + tests/patmat/i2363-maybe.scala | 27 + tests/patmat/optionless-maybe.check | 8 + tests/patmat/optionless-maybe.scala | 34 ++ .../pos-custom-args/captures/i26586.scala | 18 + .../captures/i24729-maybe.scala | 23 + tests/pos/StringContext-maybe.scala | 490 ++++++++++++++++++ tests/pos/first-class-patterns-maybe.scala | 25 + tests/pos/i16342-maybe.scala | 20 + tests/pos/i17525-maybe.scala | 7 + tests/pos/i1793-maybe.scala | 9 + tests/pos/i18175-maybe.scala | 108 ++++ tests/pos/i18601-maybe.scala | 20 + tests/pos/i20107-maybe.scala | 10 + tests/pos/i25663-maybe.scala | 7 + tests/pos/i8083-maybe.scala | 28 + tests/pos/i8530-maybe.scala | 30 ++ tests/pos/i8577-maybe.scala | 23 + tests/pos/i8972-maybe.scala | 11 + tests/pos/inline-unapply-maybe.scala | 17 + tests/pos/maybe-conversions.scala | 24 + tests/pos/maybe-subtyping.scala | 21 + tests/pos/misc-unapply_pos-maybe.scala | 29 ++ tests/pos/simpleExtractors-1-maybe.scala | 32 ++ tests/pos/t1260-maybe.scala | 20 + tests/pos/t6675-maybe.scala | 24 + tests/pos/t6994-maybe.scala | 10 + tests/pos/t8128-maybe.scala | 20 + .../parse-dates-monadic.check | 12 + .../parse-dates-monadic.scala | 84 +++ .../stdlibExperimentalDefinitions.scala | 17 +- tests/run/Typeable-maybe.check | 8 + tests/run/Typeable-maybe.scala | 63 +++ tests/run/eq-unit-pattern.scala | 23 + tests/run/errorhandling/Result.scala | 3 +- tests/run/errorhandling/Test.scala | 3 +- tests/run/errorhandling/Validator.scala | 4 +- tests/run/errorhandling/optional.scala | 4 +- tests/run/fully-abstract-nat-3-maybe.check | 13 + tests/run/fully-abstract-nat-3-maybe.scala | 159 ++++++ tests/run/i13968-maybe.scala | 27 + tests/run/i1773-maybe.check | 2 + tests/run/i1773-maybe.scala | 18 + tests/run/i4177-maybe.scala | 20 + tests/run/i8530-b-maybe.scala | 23 + tests/run/i8530-maybe.check | 7 + tests/run/i8530-maybe.scala | 49 ++ tests/run/i8577a-maybe.scala | 15 + tests/run/i8577i-maybe.scala | 16 + tests/run/maybe-numeric-widening.check | 1 + tests/run/maybe-numeric-widening.scala | 12 + tests/run/maybe-widening.check | 1 + tests/run/maybe-widening.scala | 19 + tests/run/maybe.check | 19 + tests/run/maybe.scala | 102 ++++ tests/run/named-patterns-maybe.check | 20 + tests/run/named-patterns-maybe.scala | 75 +++ tests/run/ok-tostring.check | 3 + tests/run/ok-tostring.scala | 10 + tests/run/orelse.check | 33 ++ tests/run/orelse.scala | 134 +++++ tests/run/postfix-qmark.scala | 22 + tests/run/string-extractor-maybe.check | 9 + tests/run/string-extractor-maybe.scala | 65 +++ tests/run/t6111-maybe.check | 2 + tests/run/t6111-maybe.scala | 32 ++ tests/run/tryPatternMatch-maybe.check | 20 + tests/run/tryPatternMatch-maybe.scala | 143 +++++ tests/run/type-test-nat-maybe.check | 6 + tests/run/type-test-nat-maybe.scala | 132 +++++ tests/run/unapply-maybe.scala | 124 +++++ tests/run/unapply-tparam-maybe.scala | 38 ++ tests/run/virtpatmat_unapply-maybe.check | 2 + tests/run/virtpatmat_unapply-maybe.scala | 34 ++ tests/warn/i12253-maybe.check | 13 + tests/warn/i12253-maybe.scala | 33 ++ tests/warn/maybe-conversions.check | 32 ++ tests/warn/maybe-conversions.scala | 16 + tests/warn/maybe-typetest.check | 46 ++ tests/warn/maybe-typetest.scala | 19 + 131 files changed, 3917 insertions(+), 119 deletions(-) create mode 100644 library/src/scala/compiletime/Maybe.scala create mode 100644 library/src/scala/maybe.scala create mode 100644 library/src/scala/runtime/Fail.scala create mode 100644 library/src/scala/runtime/MaybeCase.scala create mode 100644 library/src/scala/runtime/Valid.scala create mode 100644 library/src/scala/util/Err.scala create mode 100644 library/src/scala/util/Ok.scala create mode 100644 tests/explicit-nulls/pos/opt-maybe.scala create mode 100644 tests/init-global/pos/match-complete-maybe.scala create mode 100644 tests/init-global/warn/unapply-implicit-arg2-maybe.check create mode 100644 tests/init-global/warn/unapply-implicit-arg2-maybe.scala create mode 100644 tests/neg/bad-unapplies-maybe.check create mode 100644 tests/neg/bad-unapplies-maybe.scala create mode 100644 tests/neg/i14896-maybe.scala create mode 100644 tests/neg/i1793-maybe.scala create mode 100644 tests/neg/i21841-maybe.check create mode 100644 tests/neg/i21841-maybe.scala create mode 100644 tests/neg/i2378-maybe.scala create mode 100644 tests/neg/i24168-maybe.scala create mode 100644 tests/neg/i8530-b-maybe.check create mode 100644 tests/neg/i8530-b-maybe.scala create mode 100644 tests/neg/i8894-maybe.scala create mode 100644 tests/neg/orelse-subtyping.check create mode 100644 tests/neg/orelse-subtyping.scala create mode 100644 tests/neg/patmat2-maybe.scala create mode 100644 tests/neg/refutable-pattern-binding-messages-maybe.check create mode 100644 tests/neg/refutable-pattern-binding-messages-maybe.scala create mode 100644 tests/neg/t7868-maybe.scala create mode 100644 tests/neg/t8128-maybe.check create mode 100644 tests/neg/t8128-maybe.scala create mode 100644 tests/neg/unchecked-patterns-maybe.scala create mode 100644 tests/patmat/i2363-maybe.check create mode 100644 tests/patmat/i2363-maybe.scala create mode 100644 tests/patmat/optionless-maybe.check create mode 100644 tests/patmat/optionless-maybe.scala create mode 100644 tests/pending/pos-custom-args/captures/i26586.scala create mode 100644 tests/pos-custom-args/captures/i24729-maybe.scala create mode 100644 tests/pos/StringContext-maybe.scala create mode 100644 tests/pos/first-class-patterns-maybe.scala create mode 100644 tests/pos/i16342-maybe.scala create mode 100644 tests/pos/i17525-maybe.scala create mode 100644 tests/pos/i1793-maybe.scala create mode 100644 tests/pos/i18175-maybe.scala create mode 100644 tests/pos/i18601-maybe.scala create mode 100644 tests/pos/i20107-maybe.scala create mode 100644 tests/pos/i25663-maybe.scala create mode 100644 tests/pos/i8083-maybe.scala create mode 100644 tests/pos/i8530-maybe.scala create mode 100644 tests/pos/i8577-maybe.scala create mode 100644 tests/pos/i8972-maybe.scala create mode 100644 tests/pos/inline-unapply-maybe.scala create mode 100644 tests/pos/maybe-conversions.scala create mode 100644 tests/pos/maybe-subtyping.scala create mode 100644 tests/pos/misc-unapply_pos-maybe.scala create mode 100644 tests/pos/simpleExtractors-1-maybe.scala create mode 100644 tests/pos/t1260-maybe.scala create mode 100644 tests/pos/t6675-maybe.scala create mode 100644 tests/pos/t6994-maybe.scala create mode 100644 tests/pos/t8128-maybe.scala create mode 100644 tests/run-bootstrapped/parse-dates-monadic.check create mode 100644 tests/run-bootstrapped/parse-dates-monadic.scala create mode 100644 tests/run/Typeable-maybe.check create mode 100644 tests/run/Typeable-maybe.scala create mode 100644 tests/run/eq-unit-pattern.scala create mode 100644 tests/run/fully-abstract-nat-3-maybe.check create mode 100644 tests/run/fully-abstract-nat-3-maybe.scala create mode 100644 tests/run/i13968-maybe.scala create mode 100644 tests/run/i1773-maybe.check create mode 100644 tests/run/i1773-maybe.scala create mode 100644 tests/run/i4177-maybe.scala create mode 100644 tests/run/i8530-b-maybe.scala create mode 100644 tests/run/i8530-maybe.check create mode 100644 tests/run/i8530-maybe.scala create mode 100644 tests/run/i8577a-maybe.scala create mode 100644 tests/run/i8577i-maybe.scala create mode 100644 tests/run/maybe-numeric-widening.check create mode 100644 tests/run/maybe-numeric-widening.scala create mode 100644 tests/run/maybe-widening.check create mode 100644 tests/run/maybe-widening.scala create mode 100644 tests/run/maybe.check create mode 100644 tests/run/maybe.scala create mode 100644 tests/run/named-patterns-maybe.check create mode 100644 tests/run/named-patterns-maybe.scala create mode 100644 tests/run/ok-tostring.check create mode 100644 tests/run/ok-tostring.scala create mode 100644 tests/run/orelse.check create mode 100644 tests/run/orelse.scala create mode 100644 tests/run/postfix-qmark.scala create mode 100644 tests/run/string-extractor-maybe.check create mode 100644 tests/run/string-extractor-maybe.scala create mode 100644 tests/run/t6111-maybe.check create mode 100644 tests/run/t6111-maybe.scala create mode 100644 tests/run/tryPatternMatch-maybe.check create mode 100644 tests/run/tryPatternMatch-maybe.scala create mode 100644 tests/run/type-test-nat-maybe.check create mode 100644 tests/run/type-test-nat-maybe.scala create mode 100644 tests/run/unapply-maybe.scala create mode 100644 tests/run/unapply-tparam-maybe.scala create mode 100644 tests/run/virtpatmat_unapply-maybe.check create mode 100644 tests/run/virtpatmat_unapply-maybe.scala create mode 100644 tests/warn/i12253-maybe.check create mode 100644 tests/warn/i12253-maybe.scala create mode 100644 tests/warn/maybe-conversions.check create mode 100644 tests/warn/maybe-conversions.scala create mode 100644 tests/warn/maybe-typetest.check create mode 100644 tests/warn/maybe-typetest.scala diff --git a/compiler/src/dotty/tools/dotc/CompilationUnit.scala b/compiler/src/dotty/tools/dotc/CompilationUnit.scala index 7d9a4e47e5b9..59733a5fc645 100644 --- a/compiler/src/dotty/tools/dotc/CompilationUnit.scala +++ b/compiler/src/dotty/tools/dotc/CompilationUnit.scala @@ -82,9 +82,12 @@ class CompilationUnit protected (val source: SourceFile, val info: CompilationUn /** Will be set to true if unit was compiled with `magic` language import. */ var magic: Boolean = false + /** Will be set to true if unit was compiled with `errorHandling` language import. */ + var newErrorHandling: Boolean = false + /** Will be set to true if the unit contains a pureFunctions language import */ var knowsPureFuns: Boolean = false - + /** Will be set to true if the unit contains an inlineTrait language import */ var knowsInlineTraits: Boolean = false diff --git a/compiler/src/dotty/tools/dotc/ast/Desugar.scala b/compiler/src/dotty/tools/dotc/ast/Desugar.scala index 0145630276fc..09d1bc6c7773 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.MaybeClass.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 2659abc5e3fc..0b8886bcce45 100644 --- a/compiler/src/dotty/tools/dotc/ast/tpd.scala +++ b/compiler/src/dotty/tools/dotc/ast/tpd.scala @@ -98,6 +98,15 @@ 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) + /** A smart version of this that returns one of the branches if the condition + * is a boolean literal. + */ + 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 +1103,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 +1120,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..f660a0c4d469 100644 --- a/compiler/src/dotty/tools/dotc/config/Feature.scala +++ b/compiler/src/dotty/tools/dotc/config/Feature.scala @@ -45,6 +45,7 @@ object Feature: val magic = experimental("magic") val inlineTraits = experimental("inlineTraits") val specializedTraits = experimental("specializedTraits") + val errorHandling = experimental("errorHandling") val nonViralExperimentalFeatures: Set[TermName] = Set(captureChecking, separationChecking, safe) @@ -86,6 +87,7 @@ object Feature: (magic, "Enable extensions for working with coding agents"), (inlineTraits, "Allow inline traits"), (specializedTraits, "Allow specialized traits"), + (errorHandling, "Allow error handling using T? and T ? E types"), ) /** Features that are now standard; the language import / -language choice is @@ -187,16 +189,21 @@ object Feature: def quotedPatternsWithPolymorphicFunctionsEnabled(using Context) = enabled(quotedPatternsWithPolymorphicFunctions) - - def inlineTraitsEnabled(using Context) = + + def inlineTraitsEnabled(using Context) = enabledBySetting(inlineTraits) || enabledBySetting(specializedTraits) || ctx.compilationUnit.knowsInlineTraits - + + def errorHandlingEnabled(using Context) = + ctx.originalCompilationUnit.newErrorHandling + || enabledBySetting(errorHandling) + || enabledBySetting(magic) + /** Is pureFunctions enabled for this compilation unit? */ def pureFunsEnabled(using Context) = enabledBySetting(pureFunctions) - || ctx.compilationUnit.knowsPureFuns + || ctx.originalCompilationUnit.knowsPureFuns || ccEnabled /** Is capture checking enabled by a command-line setting? */ @@ -324,24 +331,24 @@ object Feature: * @return true iff the import was handled */ def handleGlobalLanguageImport(prefix: TermName, imported: Name)(using Context): Boolean = + def enableCC() = + ctx.compilationUnit.needsCaptureChecking = true + if ctx.run != null then ctx.run.nn.ccEnabledSomewhere = true QualifiedName(prefix, imported.asTermName) match case `pureFunctions` => ctx.compilationUnit.knowsPureFuns = true if ctx.run != null then ctx.run.nn.pureFunsImportEncountered = true true case `captureChecking` => - ctx.compilationUnit.needsCaptureChecking = true - if ctx.run != null then ctx.run.nn.ccEnabledSomewhere = true + enableCC() true case `separationChecking` => - ctx.compilationUnit.needsCaptureChecking = true + enableCC() ctx.compilationUnit.needsSeparationChecking = true - if ctx.run != null then ctx.run.nn.ccEnabledSomewhere = true true case `safe` => - ctx.compilationUnit.needsCaptureChecking = true + enableCC() ctx.compilationUnit.safeMode = true - if ctx.run != null then ctx.run.nn.ccEnabledSomewhere = true true case `magic` => ctx.compilationUnit.magic = true @@ -350,6 +357,9 @@ object Feature: ctx.compilationUnit.knowsInlineTraits = true if ctx.run != null then ctx.run.nn.inlineTraitsImportEncountered = true true + case `errorHandling` => + ctx.compilationUnit.newErrorHandling = true + true case _ => false diff --git a/compiler/src/dotty/tools/dotc/core/Definitions.scala b/compiler/src/dotty/tools/dotc/core/Definitions.scala index 74c95a9368e4..f7335b2b6732 100644 --- a/compiler/src/dotty/tools/dotc/core/Definitions.scala +++ b/compiler/src/dotty/tools/dotc/core/Definitions.scala @@ -482,6 +482,23 @@ class Definitions { } def AnyKindType: TypeRef = AnyKindClass.typeRef + // Maybe-related + @tu lazy val MaybeClass: ClassSymbol = requiredClass("scala.compiletime.Maybe") + @tu lazy val ValidClass: ClassSymbol = requiredClass("scala.runtime.Valid") + @tu lazy val FailClass: ClassSymbol = requiredClass("scala.runtime.Fail") + @tu lazy val maybeModule: Symbol = requiredModule("scala.maybe") + @tu lazy val maybe_provided1: Symbol = maybeModule.info.member(termName("provided")).suchThat(_.info.isInstanceOf[MethodType]).symbol + @tu lazy val maybe_provided2: Symbol = maybeModule.info.member(termName("provided")).suchThat(_.info.isInstanceOf[PolyType]).symbol + @tu lazy val maybe_CanErr: Symbol = maybeModule.requiredType("CanErr") + + @tu lazy val OkModule: Symbol = requiredModule("scala.util.Ok") + @tu lazy val Ok_apply: Symbol = OkModule.requiredMethod(nme.apply) + @tu lazy val Ok_unapply: Symbol = OkModule.requiredMethod(nme.unapply) + + @tu lazy val ErrModule: Symbol = requiredModule("scala.util.Err") + @tu lazy val Err_unapply: Symbol = ErrModule.requiredMethod(nme.unapply) + + // More synthetic symbols @tu lazy val andType: TypeSymbol = enterBinaryAlias(tpnme.AND, AndType(_, _)) @tu lazy val orType: TypeSymbol = enterBinaryAlias(tpnme.OR, OrType(_, _, soft = false)) @@ -1594,6 +1611,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) @@ -1770,7 +1789,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 = @@ -2217,6 +2236,7 @@ class Definitions { m(TupleClass) = ProductClass m(NonEmptyTupleClass) = ProductClass m(PairClass) = ObjectClass + m(MaybeClass) = 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 6d4a771fa174..b55394bdb15d 100644 --- a/compiler/src/dotty/tools/dotc/core/TypeComparer.scala +++ b/compiler/src/dotty/tools/dotc/core/TypeComparer.scala @@ -853,6 +853,15 @@ class TypeComparer(@constructorOnly initctx: Context) extends ConstraintHandling return recur(tp1, OrType(tp21, tp221, tp2.isSoft)) && recur(tp1, OrType(tp21, tp222, tp2.isSoft)) case _ => } + // T? <: T | Null if T disjoint from null or Err + tp2 match + case OrNull(tp2a) => + tp1w match + case MaybeType(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 @@ -1041,6 +1050,9 @@ 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.MaybeClass) => + // null <: T ? Unit + 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) @@ -1505,6 +1517,12 @@ class TypeComparer(@constructorOnly initctx: Context) extends ConstraintHandling case _ => false } && recordGadtUsageIf(true) + /** T <: T? if T is not null or Err */ + def byMaybeWidening: Boolean = tp2 match + case MaybeType(res2, err2) if tp1.isNotNullNorMaybe => + recur(tp1, res2) + case _ => false + tycon2 match { case param2: TypeParamRef => isMatchingApply(tp1) || @@ -1513,6 +1531,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 @@ -1975,7 +1994,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 @@ -1987,7 +2006,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..068c230b6f9c 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.MaybeClass + || 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.MaybeClass) 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 e6a93876c168..40dd0608b77b 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 MaybeType(_, errArg) => !norMaybe && !errArg.admitsUnit + 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 = @@ -404,6 +409,10 @@ object Types extends TypeUtils { case _ => false ) + /** Can this type have () as a value? */ + def admitsUnit(using Context): Boolean = + defn.unitSuperClasses.contains(classSymbol) + /** Is this type produced as a repair for an error? */ final def isError(using Context): Boolean = stripTypeVar.isInstanceOf[ErrorType] @@ -5790,6 +5799,18 @@ object Types extends TypeUtils { def unapply(tp: MatchAlias): Option[Type] = Some(tp.alias) } + /** A constructor/extractor for `R ? E` types */ + object MaybeType { + def apply(resTp: Type, errTp: Type)(using Context) = + defn.MaybeClass.typeRef.appliedTo(resTp, errTp) + + def unapply(tp: Type)(using Context): Option[(Type, Type)] = tp.dealias match + case AppliedType(tycon, resArg :: errArg :: Nil) if tycon.isRef(defn.MaybeClass) => + Some((resArg, errArg)) + case _ => + None + } + // ----- Annotated and Import types ----------------------------------------------- /** An annotated type tpe @ annot */ diff --git a/compiler/src/dotty/tools/dotc/parsing/Parsers.scala b/compiler/src/dotty/tools/dotc/parsing/Parsers.scala index 161275146cca..84be6cdd835b 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.errorHandlingEnabled && isPostfixQmark(isType = true) then + atSpan(t.span.start): + PostfixOp(t, typeIdent()) else t } @@ -3102,7 +3110,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.errorHandlingEnabled && 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 diff --git a/compiler/src/dotty/tools/dotc/printing/RefinedPrinter.scala b/compiler/src/dotty/tools/dotc/printing/RefinedPrinter.scala index 31f2b2b8811e..f33afa2b3d4e 100644 --- a/compiler/src/dotty/tools/dotc/printing/RefinedPrinter.scala +++ b/compiler/src/dotty/tools/dotc/printing/RefinedPrinter.scala @@ -189,7 +189,7 @@ class RefinedPrinter(_ctx: Context) extends PlainPrinter(_ctx) { changePrec(GlobalPrec): val argStr: Text = args match case arg :: Nil if !defn.isDirectTupleNType(arg) && !isContextual => - atPrec(InfixPrec): + changePrec(InfixPrec): argText(arg) case _=> "(" @@ -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.MaybeClass) => + if e.isRef(defn.UnitClass) then + toTextLocal(r) ~ "?" + else + changePrec(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.MaybeClass && 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/InterceptedMethods.scala b/compiler/src/dotty/tools/dotc/transform/InterceptedMethods.scala index 5d1f1f129ba8..c8854c94ce2a 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..642e3884424f 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,6 +376,39 @@ 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) + gm.tpe.widen match + case MaybeType(res, _) => + if isErrMatch then + val MaybeType(_, errArg) = gm.tpe.widen.runtimeChecked + if errArg.isRef(defn.UnitClass) then + unitLiteral + else + val select = gm.asInstance(defn.FailClass.typeRef.appliedTo(defn.AnyType)) + .select(nme.elem) + if errArg.admitsUnit then + If(gm.nullTest(cond = true), + unitLiteral.ensureConforms(errArg), + select) + else select + else + val tested = + if res.isNotNullNorMaybe then gm + else + val validTpe = defn.ValidClass.typeRef + If(gm.isInstance(validTpe), gm.asInstance(validTpe).select(nme.elem), gm) + tested.asInstance(getSelection.tpe.widen) + case _ => + 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 @@ -396,7 +431,12 @@ object PatternMatcher { 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 +450,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.Ok_unapply => + unappResultPlan(unapp, args, arg.symbol, unappType, wasUnaryNamedTupleSelectArgForNamedTuple, IsOkTest) + case Apply(fn, arg :: Nil) if fn.symbol == defn.Err_unapply => + 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 +539,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(MaybeType(_, errTp)) => errTp.admitsUnit + 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 +557,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 +584,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 @@ -812,11 +868,24 @@ object PatternMatcher { 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 MaybeType(_, errArg) => + val test = scrutinee.nullTest(cond = false) + if errArg.isRef(defn.UnitClass) + then test + else test.and(scrutinee.isInstance(defn.FailClass.typeRef).not) + case _ => + constToLiteral( + scrutinee + .select(nme.isEmpty, _.info.isParameterless) + .select(nme.UNARY_!, _.info.isParameterless)) + case IsErrTest => + val MaybeType(_, errTp) = scrutinee.tpe.widen.runtimeChecked + val typeTest = scrutinee.isInstance(defn.FailClass.typeRef) + if errTp <:< defn.UnitType then scrutinee.nullTest(cond = true) + else if errTp.admitsUnit then scrutinee.nullTest(cond = true).or(typeTest) + else typeTest case NonNullTest => scrutinee.testNotNull case GuardTest => diff --git a/compiler/src/dotty/tools/dotc/transform/patmat/Space.scala b/compiler/src/dotty/tools/dotc/transform/patmat/Space.scala index 33e191c4bf66..1a26850f2bc9 100644 --- a/compiler/src/dotty/tools/dotc/transform/patmat/Space.scala +++ b/compiler/src/dotty/tools/dotc/transform/patmat/Space.scala @@ -418,7 +418,17 @@ 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 MaybeType(_, errTp) + if fun.symbol == defn.Err_unapply && errTp.admitsUnit => + Or(prod :: nullSpace :: Nil) + case _ => + prod case Typed(pat @ UnApply(_, _, _), _) => project(pat) @@ -445,6 +455,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 MaybeType(defn.NothingType, errTp) + MaybeType(resTp, defn.NothingType) :: errPart :: Nil + private def unapplySeqInfo(resTp: Type, pos: SrcPos)(using Context): (Int, Type, Type) = { var resultTp = resTp var elemTp = unapplySeqTypeElemTp(resultTp) @@ -574,12 +595,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 MaybeType(resTp, _) if unapp.symbol == defn.Ok_unapply => resTp :: Nil + case MaybeType(_, errTp) if unapp.symbol == defn.Err_unapply => 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 @@ -672,6 +704,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 MaybeType(resTp, errTp) => + unapp.symbol == defn.Ok_unapply && errTp.isNothingType + || unapp.symbol == defn.Err_unapply && resTp.isNothingType + case _ => false } /** Decompose a type into subspaces -- assume the type can be decomposed */ @@ -690,6 +730,8 @@ object SpaceEngine { case tp if !TypeComparer.provablyDisjoint(tp, tpB) => AndType(tp, tpB) case OrType(tp1, tp2) => List(tp1, tp2) + case MaybeType(resTp, errTp) + if !resTp.isNothingType && !errTp.isNothingType => maybeParts(resTp, errTp, nullable = errTp.admitsUnit) 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) @@ -884,6 +926,9 @@ object SpaceEngine { case Typ(tp: TermRef, _) => if (flattenList && tp <:< defn.NilType) "" else tp.symbol.showName + case Typ(MaybeType(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 @@ -1115,7 +1160,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 MaybeType(_, errTp) => errTp.admitsUnit + 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..9c93d975d850 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.MaybeClass 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/Typer.scala b/compiler/src/dotty/tools/dotc/typer/Typer.scala index 8514e78d9de3..7b9f94b56068 100644 --- a/compiler/src/dotty/tools/dotc/typer/Typer.scala +++ b/compiler/src/dotty/tools/dotc/typer/Typer.scala @@ -3793,9 +3793,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.errorHandlingEnabled then + cpy.AppliedTypeTree(tree)(untpd.ref(defn.MaybeClass.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..418fcddbbf8b 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 Feature.errorHandlingEnabled && !ctx.explicitNulls then + report.error(em"errorHandling 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..b677f7700425 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 diff --git a/library/src/scala/compiletime/Maybe.scala b/library/src/scala/compiletime/Maybe.scala new file mode 100644 index 000000000000..d00cfcc3806c --- /dev/null +++ b/library/src/scala/compiletime/Maybe.scala @@ -0,0 +1,42 @@ +package scala.compiletime + +import scala.runtime.Valid +import annotation.experimental +import scala.util.boundary.break +import scala.util.{Ok, Err} + +/** Under experimental.maybe, a trait backing maybe types `T?` */ +@experimental +sealed trait Maybe[+T, +E] extends Any, Matchable: + def isEmpty: Boolean + def get: T + +object Maybe { + extension [T, E](x: Maybe[T, E]) + transparent inline def ? (using maybe.CanErr[E]): T = + x.runtimeChecked match // runtime checked needed for non-bootstrapped compilation + case Ok(y) => y + case Err(e) => break(Err(e)) + + def withErr[E1](e: E1): Maybe[T, E1] = x.runtimeChecked match + case Ok(y) => Ok(y) + case Err(_) => Err(e) + + def mapErr[E1](f: E => E1): Maybe[T, E1] = x.runtimeChecked match + case Ok(y) => Ok(y) + case Err(e) => Err(f(e)) + + def map[U](f: T => U): Maybe[U, E] = x.runtimeChecked match + case Ok(y) => Ok(f(y)) + case Err(e) => Err(e) + + def flatMap[U](f: T => Maybe[U, E]): Maybe[U, E] = x.runtimeChecked match + case Ok(y) => f(y) + case Err(e) => Err(e) + + // TODO Add withFilter + + +} + + diff --git a/library/src/scala/language.scala b/library/src/scala/language.scala index 39ab7fa232ed..b0ed4b3a94b2 100644 --- a/library/src/scala/language.scala +++ b/library/src/scala/language.scala @@ -270,7 +270,7 @@ object language { object erasedDefinitions /** Experimental support for specialized traits - * + * * @see [[docs/_docs/internals/specialized-traits.md]] */ @compileTimeOnly("`specializedTraits` can only be used at compile time in import statements") @@ -425,7 +425,11 @@ object language { @compileTimeOnly("`magic` can only be used at compile time in import statements") object magic - + /** Experimental support for maybe types `T?` and result types `T ? E`. + */ + @compileTimeOnly("`errorHandling` can only be used at compile time in import statements") + object errorHandling + /** Experimental support for inline traits */ @compileTimeOnly("`inlineTraits` can only be used at compile time in import statements") diff --git a/library/src/scala/maybe.scala b/library/src/scala/maybe.scala new file mode 100644 index 000000000000..46d7eaf056b7 --- /dev/null +++ b/library/src/scala/maybe.scala @@ -0,0 +1,22 @@ +//> using options -Yexplicit-nulls +package scala +import scala.util.boundary, boundary.Label +import annotation.experimental +import compiletime.Maybe +import scala.util.{Ok, Err} + +@experimental +object maybe { + + type CanErr[E] = Label[Maybe[Nothing, E]] + + inline def apply[T, E](inline body: CanErr[E] ?=> T): Maybe[T, E] = + boundary(Ok(body)) + + 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/runtime/Fail.scala b/library/src/scala/runtime/Fail.scala new file mode 100644 index 000000000000..bc007349ae27 --- /dev/null +++ b/library/src/scala/runtime/Fail.scala @@ -0,0 +1,6 @@ +package scala.runtime + +import annotation.experimental + +@experimental +case class Fail[+E](elem: E) extends MaybeCase diff --git a/library/src/scala/runtime/MaybeCase.scala b/library/src/scala/runtime/MaybeCase.scala new file mode 100644 index 000000000000..7bf620f4bd91 --- /dev/null +++ b/library/src/scala/runtime/MaybeCase.scala @@ -0,0 +1,7 @@ +package scala.runtime + +import annotation.experimental + +@experimental +abstract class MaybeCase + diff --git a/library/src/scala/runtime/Valid.scala b/library/src/scala/runtime/Valid.scala new file mode 100644 index 000000000000..186fd63974f1 --- /dev/null +++ b/library/src/scala/runtime/Valid.scala @@ -0,0 +1,6 @@ +package scala.runtime + +import annotation.experimental + +@experimental +case class Valid(elem: Any) extends MaybeCase diff --git a/library/src/scala/util/Err.scala b/library/src/scala/util/Err.scala new file mode 100644 index 000000000000..53c0e9a6ba67 --- /dev/null +++ b/library/src/scala/util/Err.scala @@ -0,0 +1,19 @@ +package scala.util + +import 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/util/Ok.scala b/library/src/scala/util/Ok.scala new file mode 100644 index 000000000000..dbcb50c218db --- /dev/null +++ b/library/src/scala/util/Ok.scala @@ -0,0 +1,14 @@ +package scala.util + +import scala.runtime.{MaybeCase, Valid} +import scala.compiletime.Maybe +import annotation.experimental + +@experimental +object Ok: + inline def apply[T](x: T): Maybe[T, Nothing] = { + if x == null || x.isInstanceOf[MaybeCase] then new Valid(x) + else x + }.asInstanceOf[Maybe[T, Nothing]] + + def unapply(x: Maybe[Any, Any]): x.type = x diff --git a/project/MiMaFilters.scala b/project/MiMaFilters.scala index 3ab5ff1a1ff8..e543bb8ab44c 100644 --- a/project/MiMaFilters.scala +++ b/project/MiMaFilters.scala @@ -15,6 +15,7 @@ object MiMaFilters { Versions.mimaPreviousVersion -> Seq( // new annotation carrying the component names of Java records ProblemFilters.exclude[MissingClassProblem]("scala.annotation.internal.JavaRecordFields"), + ProblemFilters.exclude[MissingClassProblem]("scala.language$experimental$errorHandling$"), ), // Additions since last LTS diff --git a/tests/explicit-nulls/pos/opt-maybe.scala b/tests/explicit-nulls/pos/opt-maybe.scala new file mode 100644 index 000000000000..1fa093965c6c --- /dev/null +++ b/tests/explicit-nulls/pos/opt-maybe.scala @@ -0,0 +1,12 @@ + +import language.experimental.errorHandling +import scala.util.{Ok, Err} + +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..fa99f497d84f --- /dev/null +++ b/tests/init-global/pos/match-complete-maybe.scala @@ -0,0 +1,114 @@ +//> using options -Yexplicit-nulls +import language.experimental.errorHandling +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..feec44b4da3a --- /dev/null +++ b/tests/init-global/warn/unapply-implicit-arg2-maybe.scala @@ -0,0 +1,16 @@ +//> using options -Yexplicit-nulls +import language.experimental.errorHandling +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..1410ab943a92 --- /dev/null +++ b/tests/neg/bad-unapplies-maybe.scala @@ -0,0 +1,30 @@ +//> using options -Yexplicit-nulls +import language.experimental.errorHandling +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/i14896-maybe.scala b/tests/neg/i14896-maybe.scala new file mode 100644 index 000000000000..51692374a2e4 --- /dev/null +++ b/tests/neg/i14896-maybe.scala @@ -0,0 +1,4 @@ +//> using options -Yexplicit-nulls +import language.experimental.errorHandling +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..2eb1ed935257 --- /dev/null +++ b/tests/neg/i1793-maybe.scala @@ -0,0 +1,9 @@ +//> using options -Yexplicit-nulls +import language.experimental.errorHandling +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..6a1b380deb96 --- /dev/null +++ b/tests/neg/i21841-maybe.scala @@ -0,0 +1,24 @@ +//> using options -Yexplicit-nulls +import language.experimental.errorHandling +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..53b546988aa9 --- /dev/null +++ b/tests/neg/i2378-maybe.scala @@ -0,0 +1,32 @@ +//> using options -Yexplicit-nulls +import language.experimental.errorHandling +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..2729cbbdb9bb --- /dev/null +++ b/tests/neg/i24168-maybe.scala @@ -0,0 +1,14 @@ +//> using options -Yexplicit-nulls +import language.experimental.errorHandling +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..dbfe99ac87cf --- /dev/null +++ b/tests/neg/i8530-b-maybe.scala @@ -0,0 +1,11 @@ +//> using options -Yexplicit-nulls +import language.experimental.errorHandling +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..aa8970fc39c7 --- /dev/null +++ b/tests/neg/i8894-maybe.scala @@ -0,0 +1,15 @@ +//> using options -Yexplicit-nulls +import language.experimental.errorHandling +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/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..2a424eaabd98 --- /dev/null +++ b/tests/neg/orelse-subtyping.scala @@ -0,0 +1,13 @@ +//> using options -Yexplicit-nulls + +import language.experimental.errorHandling + + +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..7215ee23af26 --- /dev/null +++ b/tests/neg/patmat2-maybe.scala @@ -0,0 +1,36 @@ +//> using options -Yexplicit-nulls +import language.experimental.errorHandling +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..4a6bf6ac7f40 --- /dev/null +++ b/tests/neg/refutable-pattern-binding-messages-maybe.check @@ -0,0 +1,48 @@ +-- Error: tests/neg/refutable-pattern-binding-messages-maybe.scala:7:14 ------------------------------------------------ +7 | 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:8:14 ------------------------------------------------ +8 | 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:12:20 ----------------------------------------------- +12 | 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:13:11 ----------------------------------------------- +13 | 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:17:13 ----------------------------------------------- +17 | 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:18:10 ----------------------------------------------- +18 | 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..8e6686eebf11 --- /dev/null +++ b/tests/neg/refutable-pattern-binding-messages-maybe.scala @@ -0,0 +1,19 @@ +//> using options -Yexplicit-nulls +import language.experimental.errorHandling +import language.future +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..90b18ae2440f --- /dev/null +++ b/tests/neg/t7868-maybe.scala @@ -0,0 +1,15 @@ +//> using options -Yexplicit-nulls +import language.experimental.errorHandling +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..5a7f0706d947 --- /dev/null +++ b/tests/neg/t8128-maybe.scala @@ -0,0 +1,8 @@ +//> using options -Yexplicit-nulls +import language.experimental.errorHandling +import scala.util.{Ok, Err} +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..7092d4040eb5 --- /dev/null +++ b/tests/neg/unchecked-patterns-maybe.scala @@ -0,0 +1,30 @@ +//> using options -Werror -Yexplicit-nulls + +import language.experimental.errorHandling +import language.future + +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/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..294ad09df507 --- /dev/null +++ b/tests/patmat/i2363-maybe.scala @@ -0,0 +1,27 @@ +//> using options -Yexplicit-nulls +import language.experimental.errorHandling +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..f77fcb687a85 --- /dev/null +++ b/tests/patmat/optionless-maybe.scala @@ -0,0 +1,34 @@ +//> using options -Yexplicit-nulls +import language.experimental.errorHandling +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..05aab6082f54 --- /dev/null +++ b/tests/pos-custom-args/captures/i24729-maybe.scala @@ -0,0 +1,23 @@ +//> using options -Yexplicit-nulls +import language.experimental.errorHandling +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..0ca939bdca95 --- /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.errorHandling +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..18e2066d02fc --- /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.errorHandling + 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/i16342-maybe.scala b/tests/pos/i16342-maybe.scala new file mode 100644 index 000000000000..add33b306e00 --- /dev/null +++ b/tests/pos/i16342-maybe.scala @@ -0,0 +1,20 @@ +//> using options -Yexplicit-nulls +import language.experimental.errorHandling +type Opaque = Base & Tag + +type Base = Any { + type Hack +} + +trait Tag + +object Opaque { + def apply(value: String): Opaque = value.asInstanceOf[Opaque] + + def unapply(userId: Opaque): String? = userId.value + def unappy2(userId: Base & Tag): Option[String] = Option(userId).map(_.value) +} + +final implicit class Ops(private val userId: Opaque) extends AnyVal { + def value: String = userId.asInstanceOf[String] +} \ No newline at end of file diff --git a/tests/pos/i17525-maybe.scala b/tests/pos/i17525-maybe.scala new file mode 100644 index 000000000000..3ab634678323 --- /dev/null +++ b/tests/pos/i17525-maybe.scala @@ -0,0 +1,7 @@ +//> using options -Yexplicit-nulls +import language.experimental.errorHandling +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..b25cbc4c3688 --- /dev/null +++ b/tests/pos/i1793-maybe.scala @@ -0,0 +1,9 @@ +//> using options -Yexplicit-nulls +import language.experimental.errorHandling +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..997279f95253 --- /dev/null +++ b/tests/pos/i18175-maybe.scala @@ -0,0 +1,108 @@ +//> using options -Yexplicit-nulls +import language.experimental.errorHandling +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..86572d6ef059 --- /dev/null +++ b/tests/pos/i18601-maybe.scala @@ -0,0 +1,20 @@ +//> using options -Werror -Yexplicit-nulls +import language.experimental.errorHandling +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..efa143726b90 --- /dev/null +++ b/tests/pos/i20107-maybe.scala @@ -0,0 +1,10 @@ +//> using options -Yexplicit-nulls +import language.experimental.errorHandling +import scala.util.{Ok, Err} + +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..c5f3b7b47f29 --- /dev/null +++ b/tests/pos/i25663-maybe.scala @@ -0,0 +1,7 @@ +//> using options -Yexplicit-nulls +import language.experimental.errorHandling +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..4e3ee9acd4e2 --- /dev/null +++ b/tests/pos/i8083-maybe.scala @@ -0,0 +1,28 @@ +//> using options -Yexplicit-nulls +import language.experimental.errorHandling +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..586841033d77 --- /dev/null +++ b/tests/pos/i8530-maybe.scala @@ -0,0 +1,30 @@ +//> using options -Yexplicit-nulls +import language.experimental.errorHandling +import util.Ok + +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..dee679e0038a --- /dev/null +++ b/tests/pos/i8577-maybe.scala @@ -0,0 +1,23 @@ +//> using options -Yexplicit-nulls +package i8577 + +import language.experimental.errorHandling +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..b06f11f2111e --- /dev/null +++ b/tests/pos/i8972-maybe.scala @@ -0,0 +1,11 @@ +//> using options -Yexplicit-nulls +import language.experimental.errorHandling +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..a41a6ec5e3fe --- /dev/null +++ b/tests/pos/inline-unapply-maybe.scala @@ -0,0 +1,17 @@ +//> using options -Yexplicit-nulls +import language.experimental.errorHandling +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..18b6d8f9449d --- /dev/null +++ b/tests/pos/maybe-conversions.scala @@ -0,0 +1,24 @@ +//> using options -Yexplicit-nulls +import language.experimental.errorHandling + +import scala.util.{Either, Ok, Err} + +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..2b85cb6bc5fe --- /dev/null +++ b/tests/pos/maybe-subtyping.scala @@ -0,0 +1,21 @@ +//> using options -Yexplicit-nulls +import language.experimental.errorHandling + +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 + + val _: String | Nothing = "abc" diff --git a/tests/pos/misc-unapply_pos-maybe.scala b/tests/pos/misc-unapply_pos-maybe.scala new file mode 100644 index 000000000000..0fe3d7f225d7 --- /dev/null +++ b/tests/pos/misc-unapply_pos-maybe.scala @@ -0,0 +1,29 @@ +//> using options -Yexplicit-nulls +import language.experimental.errorHandling +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..a839ac6e214a --- /dev/null +++ b/tests/pos/simpleExtractors-1-maybe.scala @@ -0,0 +1,32 @@ +//> using options -Yexplicit-nulls +import language.experimental.errorHandling +import scala.util.{Ok, Err} + +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/t1260-maybe.scala b/tests/pos/t1260-maybe.scala new file mode 100644 index 000000000000..69a7ffc693db --- /dev/null +++ b/tests/pos/t1260-maybe.scala @@ -0,0 +1,20 @@ +//> using options -Yexplicit-nulls +import language.experimental.errorHandling +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..b74bd6dc7fd2 --- /dev/null +++ b/tests/pos/t6675-maybe.scala @@ -0,0 +1,24 @@ +//> using options -Yexplicit-nulls +import language.experimental.errorHandling +import scala.util.{Ok, Err} + +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..6e49a3124ef5 --- /dev/null +++ b/tests/pos/t6994-maybe.scala @@ -0,0 +1,10 @@ +//> using options -Yexplicit-nulls +import language.experimental.errorHandling +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..fee93830deca --- /dev/null +++ b/tests/pos/t8128-maybe.scala @@ -0,0 +1,20 @@ +//> using options -Yexplicit-nulls +import language.experimental.errorHandling +import scala.util.{Ok, Err} + +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/parse-dates-monadic.check b/tests/run-bootstrapped/parse-dates-monadic.check new file mode 100644 index 000000000000..d8af81f9b792 --- /dev/null +++ b/tests/run-bootstrapped/parse-dates-monadic.check @@ -0,0 +1,12 @@ +1/1/2000 +Date(1,1,2000) +Date(1,1,2000) +1/1-2000 +null +Fail(Date not in format day/month/year) +1/jan/2000 +null +Fail(malformed month: jan) +1/13/2000 +Date(1,13,2000) +Date(1,13,2000) diff --git a/tests/run-bootstrapped/parse-dates-monadic.scala b/tests/run-bootstrapped/parse-dates-monadic.scala new file mode 100644 index 000000000000..069e1ad65637 --- /dev/null +++ b/tests/run-bootstrapped/parse-dates-monadic.scala @@ -0,0 +1,84 @@ +//> using options -Yexplicit-nulls +import language.experimental.errorHandling +import scala.util.{Ok, Err} + +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): Date? = + str.split("/") match + case Array(d, m, y) => + for + day <- d.parseInt + month <- m.parseInt + year <- y.parseInt + yield + Date(day, month, year) + case _ => + null + +def parseDate2(str: String): Date ? String = + str.split("/") match + case Array(d, m, y) => + for + day <- d.parseInt.withErr(s"malformed day: $d") + month <- m.parseInt.withErr(s"malformed month: $m") + year <- y.parseInt.withErr(s"malformed year: $y") + yield + 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..f52b576ab7e4 100644 --- a/tests/run-tasty-inspector/stdlibExperimentalDefinitions.scala +++ b/tests/run-tasty-inspector/stdlibExperimentalDefinitions.scala @@ -99,7 +99,22 @@ val experimentalDefinitionInLibrary = Set( // New feature: Specialized traits "scala.specialize.Specialized", - "scala.specialize.Specialized$" + "scala.specialize.Specialized$", + + // New feature: maybe + "scala.compiletime.Maybe", + "scala.compiletime.Maybe$", + "scala.util.Ok", + "scala.util.Ok$", + "scala.util.Err", + "scala.util.Err$", + "scala.runtime.Fail", + "scala.runtime.Fail$", + "scala.runtime.Valid", + "scala.runtime.Valid$", + "scala.runtime.MaybeCase", + "scala.maybe", + "scala.maybe$", ) 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..a375287408aa --- /dev/null +++ b/tests/run/Typeable-maybe.scala @@ -0,0 +1,63 @@ +//> 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.errorHandling +import scala.util.{Ok, Err} + +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/eq-unit-pattern.scala b/tests/run/eq-unit-pattern.scala new file mode 100644 index 000000000000..a768ec61b474 --- /dev/null +++ b/tests/run/eq-unit-pattern.scala @@ -0,0 +1,23 @@ +// Matching `()` against a value whose static type is abstract must not be +// reduced to a constant test: the value can still be the unit value. +sealed abstract class Void +case object UnitRef extends Void +case class Caught[A](ref: A) extends Void + +def toVoid[A](ref: A): Void = ref match + case () => UnitRef + case _ => Caught(ref) + +def isUnit[A](x: A): Boolean = x == () + +@main def Test = + assert(toVoid(()) == UnitRef, toVoid(())) + assert(toVoid(1) == Caught(1)) + assert(toVoid("a") == Caught("a")) + val any: Any = () + assert(toVoid(any) == UnitRef, toVoid(any)) + val ref: AnyRef = ().asInstanceOf[AnyRef] + assert(toVoid(ref) == UnitRef, toVoid(ref)) + assert(isUnit(())) + assert(isUnit(any)) + assert(!isUnit(1)) diff --git a/tests/run/errorhandling/Result.scala b/tests/run/errorhandling/Result.scala index 8a200c790489..5fc9d281d181 100644 --- a/tests/run/errorhandling/Result.scala +++ b/tests/run/errorhandling/Result.scala @@ -1,4 +1,5 @@ -package scala.util +package test.util +import scala.util.* import boundary.{Label, break} sealed abstract class Result[+T, +E] diff --git a/tests/run/errorhandling/Test.scala b/tests/run/errorhandling/Test.scala index 9f379828aa9a..f772b9f1c8ad 100644 --- a/tests/run/errorhandling/Test.scala +++ b/tests/run/errorhandling/Test.scala @@ -1,4 +1,5 @@ -import scala.util.*, boundary.break +import scala.util.boundary, boundary.break +import test.util.* import caps.any diff --git a/tests/run/errorhandling/Validator.scala b/tests/run/errorhandling/Validator.scala index b9fc2f661b04..d37a66db3676 100644 --- a/tests/run/errorhandling/Validator.scala +++ b/tests/run/errorhandling/Validator.scala @@ -1,6 +1,6 @@ //> using options -language:experimental.captureChecking,experimental.separationChecking -package scala.util -import boundary.{break, Label} +package test.util +import scala.util.boundary, boundary.{break, Label} import collection.mutable import caps.Control import caps.fresh diff --git a/tests/run/errorhandling/optional.scala b/tests/run/errorhandling/optional.scala index e041834b825c..a0cf9ed63e88 100644 --- a/tests/run/errorhandling/optional.scala +++ b/tests/run/errorhandling/optional.scala @@ -1,5 +1,5 @@ -package scala.util -import boundary.{break, Label} +package test.util +import scala.util.boundary, boundary.{break, Label} /** A mockup of scala.Option */ abstract class Option[+T] 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..05e5939ea905 --- /dev/null +++ b/tests/run/fully-abstract-nat-3-maybe.scala @@ -0,0 +1,159 @@ +//> using options -Yexplicit-nulls + +import language.experimental.errorHandling +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/i13968-maybe.scala b/tests/run/i13968-maybe.scala new file mode 100644 index 000000000000..21d090de8725 --- /dev/null +++ b/tests/run/i13968-maybe.scala @@ -0,0 +1,27 @@ +//> using options -Yexplicit-nulls +import language.experimental.errorHandling +object Bar { + def unapply(x: Any): (Int *: Int *: EmptyTuple)? = 1 *: 2 *: Tuple() +} + +object Bar23 { + def unapply(x: Any): ( + Int *: Int *: Int *: Int *: Int *: Int *: Int *: Int *: Int *: Int *: + Int *: Int *: Int *: Int *: Int *: Int *: Int *: Int *: Int *: Int *: + Int *: Int *: Int *: EmptyTuple + )? = ( + 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..64f64347bd64 --- /dev/null +++ b/tests/run/i1773-maybe.scala @@ -0,0 +1,18 @@ +//> using options -Yexplicit-nulls +import language.experimental.errorHandling +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..5fd6f37eb07a --- /dev/null +++ b/tests/run/i4177-maybe.scala @@ -0,0 +1,20 @@ +//> using options -Yexplicit-nulls +import language.experimental.errorHandling +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..127b13711da4 --- /dev/null +++ b/tests/run/i8530-b-maybe.scala @@ -0,0 +1,23 @@ +//> using options -Yexplicit-nulls +import language.experimental.errorHandling +import scala.util.{Ok, Err} +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..11645d828f2b --- /dev/null +++ b/tests/run/i8530-maybe.scala @@ -0,0 +1,49 @@ +//> using options -Yexplicit-nulls +import language.experimental.errorHandling +import scala.util.{Ok, Err} + +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..6cd0b2c0546e --- /dev/null +++ b/tests/run/i8577a-maybe.scala @@ -0,0 +1,15 @@ +//> using options -Yexplicit-nulls +import language.experimental.errorHandling +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..f4d26a5e9a88 --- /dev/null +++ b/tests/run/i8577i-maybe.scala @@ -0,0 +1,16 @@ +//> using options -Yexplicit-nulls +import language.experimental.errorHandling +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..af73b56c2f7c --- /dev/null +++ b/tests/run/maybe-numeric-widening.scala @@ -0,0 +1,12 @@ +//> using options -Yexplicit-nulls +import language.experimental.errorHandling +import util.Ok + +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..35487bb70312 --- /dev/null +++ b/tests/run/maybe-widening.scala @@ -0,0 +1,19 @@ +//> using options -Yexplicit-nulls +import language.experimental.errorHandling + +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..23c79c02c6fc --- /dev/null +++ b/tests/run/maybe.scala @@ -0,0 +1,102 @@ +//> using options -Yexplicit-nulls +import language.experimental.errorHandling +import util.Ok + +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..57bb16db53e1 --- /dev/null +++ b/tests/run/named-patterns-maybe.scala @@ -0,0 +1,75 @@ +//> using options -Yexplicit-nulls + +import language.experimental.errorHandling +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/ok-tostring.check b/tests/run/ok-tostring.check new file mode 100644 index 000000000000..8f56770cbfbf --- /dev/null +++ b/tests/run/ok-tostring.check @@ -0,0 +1,3 @@ +Valid(null) +Valid(Fail(bad)) +good diff --git a/tests/run/ok-tostring.scala b/tests/run/ok-tostring.scala new file mode 100644 index 000000000000..ca236f713c88 --- /dev/null +++ b/tests/run/ok-tostring.scala @@ -0,0 +1,10 @@ +//> using options -Yexplicit-nulls +import language.experimental.errorHandling +import language.future +import scala.util.{Ok, Err} + +@main def Test = + println(Ok(null)) + println(Ok(Err("bad"))) + println(Ok("good")) + 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..847d9a5f71f9 --- /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.errorHandling + +import scala.util.{Either, Ok, Err} + +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..7ac0b2afceb4 --- /dev/null +++ b/tests/run/postfix-qmark.scala @@ -0,0 +1,22 @@ +//> using options -Yexplicit-nulls +import language.experimental.errorHandling + +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..97405249cf87 --- /dev/null +++ b/tests/run/string-extractor-maybe.scala @@ -0,0 +1,65 @@ +//> using options -Yexplicit-nulls +import language.experimental.errorHandling +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..faf45cd13c3c --- /dev/null +++ b/tests/run/t6111-maybe.scala @@ -0,0 +1,32 @@ +//> 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.errorHandling +import util.Ok + +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..2139d3d6628e --- /dev/null +++ b/tests/run/tryPatternMatch-maybe.scala @@ -0,0 +1,143 @@ +//> using options -Yexplicit-nulls +// scalajs: --compliant-semantics + +import language.experimental.errorHandling +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..c3196acfd9e3 --- /dev/null +++ b/tests/run/type-test-nat-maybe.scala @@ -0,0 +1,132 @@ +//> using options -Yexplicit-nulls +import language.experimental.errorHandling +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..218a3cf293a9 --- /dev/null +++ b/tests/run/unapply-maybe.scala @@ -0,0 +1,124 @@ +//> using options -Yexplicit-nulls +import language.experimental.errorHandling +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..458d663ac249 --- /dev/null +++ b/tests/run/unapply-tparam-maybe.scala @@ -0,0 +1,38 @@ +//> using options -Yexplicit-nulls +import language.experimental.errorHandling +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..2c52ba21cdd3 --- /dev/null +++ b/tests/run/virtpatmat_unapply-maybe.scala @@ -0,0 +1,34 @@ +//> using options -Yexplicit-nulls +import language.experimental.errorHandling +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..69ce7621f449 --- /dev/null +++ b/tests/warn/i12253-maybe.scala @@ -0,0 +1,33 @@ +//> using options -Yexplicit-nulls + + +import language.experimental.errorHandling +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..df0b5314c108 --- /dev/null +++ b/tests/warn/maybe-conversions.scala @@ -0,0 +1,16 @@ +//> using options -Yexplicit-nulls +import language.experimental.errorHandling + +import scala.util.{Either, Ok, Err} + +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..f910c5bbfb8d --- /dev/null +++ b/tests/warn/maybe-typetest.check @@ -0,0 +1,46 @@ +-- [E165] Type Warning: tests/warn/maybe-typetest.scala:8:12 ----------------------------------------------------------- +8 | 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:14:12 ---------------------------------------------------------- +14 | 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:12:9 ----------------------------------------- +12 | case _ => // warn unreachable + | ^ + | Unreachable case +-- [E030] Match case Unreachable Warning: tests/warn/maybe-typetest.scala:18:9 ----------------------------------------- +18 | case _ => // warn unreachable + | ^ + | Unreachable case +-- [E092] Pattern Match Unchecked Warning: tests/warn/maybe-typetest.scala:8:9 ----------------------------------------- +8 | 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:11:9 ---------------------------------------- +11 | 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:14:9 ---------------------------------------- +14 | 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:17:9 ---------------------------------------- +17 | 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..3386dbedc26d --- /dev/null +++ b/tests/warn/maybe-typetest.scala @@ -0,0 +1,19 @@ +//> using options -Yexplicit-nulls +import language.experimental.errorHandling +import language.future +import scala.util.{Ok, Err} + +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 + From 42c1e44dcf8534c346fd25bd1a3f8ebe595c552f Mon Sep 17 00:00:00 2001 From: odersky Date: Mon, 31 Aug 2026 13:11:46 +0200 Subject: [PATCH 2/6] Optimize creation and pattern matching with Ok and Err --- .../dotty/tools/dotc/inlines/Inliner.scala | 38 +++++++- .../dotty/tools/dotc/inlines/Inlines.scala | 88 ++++++++++--------- .../tools/dotc/transform/BetaReduce.scala | 11 ++- .../tools/dotc/transform/PatternMatcher.scala | 87 +++++++++++++++--- tests/neg/yimports-stable.check | 6 +- tests/neg/yimports-stable/C_2.scala | 2 +- tests/neg/yimports-stable/minidef_1.scala | 2 +- 7 files changed, 171 insertions(+), 63 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/inlines/Inliner.scala b/compiler/src/dotty/tools/dotc/inlines/Inliner.scala index 118c1bf9073b..508b58d91688 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 @@ -211,6 +212,41 @@ object Inliner: else constToLiteral(rootTree) + /** Can a value of type `tp` be the unit value `()` at runtime? That's the case if + * `tp` is not a class type, since then it could still be instantiated to `Unit`, + * or if it is one of the classes that `()` is an instance of. Note that `()` is + * represented as a `BoxedUnit` where a reference type is expected, so `Object` + * and `java.io.Serializable` have to be counted in as well. + */ + private def canBeUnit(tp: Type)(using Context): Boolean = + val cls = tp.widenDealias.typeSymbol + !cls.isClass + || defn.UnitClass.derivesFrom(cls) + || cls == defn.ObjectClass + || cls == defn.JavaSerializableClass + + /** 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: one argument is of Unit type and the other one's + * type tells us whether it can be the unit value or not. + */ + def reduceUnitEQ(tree: Tree)(using Context): Tree = tree match + case Apply(sel @ Select(arg1, nme.EQ), arg2 :: Nil) if isPureExpr(arg1) && isPureExpr(arg2) => + 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 !canBeUnit(tp2) then const(false) + else EmptyTree + else EmptyTree + val tp1 = arg1.tpe.widen + val tp2 = arg2.tpe.widen + 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 @@ -810,7 +846,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 1146f0e5d986..d8a637ecc38e 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) @@ -286,7 +286,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)) @@ -297,8 +297,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) @@ -320,13 +320,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, @@ -336,11 +336,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 " @@ -355,7 +355,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 { @@ -367,31 +367,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) => @@ -400,10 +400,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) } @@ -411,11 +411,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) @@ -721,7 +721,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 `Ok` callValueArgss match case (arg :: Nil) :: Nil => if inlinedMethod == defn.Compiletime_requireConst then @@ -731,6 +731,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.Ok_unapply && arg.tpe.isNotNullNorMaybe then + return arg case _ => // Special handling of `constValue[T]`, `constValueOpt[T]`, `constValueTuple[T]`, `summonInline[T]` and `summonAll[T]` @@ -919,11 +921,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)) @@ -979,11 +981,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 = @@ -1026,7 +1028,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) @@ -1051,7 +1053,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) @@ -1062,14 +1064,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) @@ -1155,7 +1157,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/transform/BetaReduce.scala b/compiler/src/dotty/tools/dotc/transform/BetaReduce.scala index 7a03cd2a15f5..0e2b7918a1f6 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 reduceUnitEQ */ + 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.reduceUnitEQ(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/PatternMatcher.scala b/compiler/src/dotty/tools/dotc/transform/PatternMatcher.scala index 642e3884424f..2baa54e82629 100644 --- a/compiler/src/dotty/tools/dotc/transform/PatternMatcher.scala +++ b/compiler/src/dotty/tools/dotc/transform/PatternMatcher.scala @@ -418,16 +418,6 @@ object PatternMatcher { 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 @@ -862,6 +852,76 @@ 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.errorHandlingEnabled 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 */ @@ -891,7 +951,7 @@ object PatternMatcher { case GuardTest => scrutinee case EqualTest(tree) => - tree.equal(scrutinee) + inlines.Inliner.reduceUnitEQ(tree.equal(scrutinee)) case LengthTest(len, exact) => val lengthCompareSym = defn.Seq_lengthCompare.matchingMember(scrutinee.tpe) if (lengthCompareSym.exists) @@ -1106,7 +1166,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) @@ -1213,7 +1273,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/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 { From 0bead1c25d300a62509e66b57eb8805ecf9bfa0b Mon Sep 17 00:00:00 2001 From: odersky Date: Tue, 1 Sep 2026 09:17:27 +0200 Subject: [PATCH 3/6] Allow if guards without then in error handling contexts --- .../dotty/tools/dotc/parsing/Parsers.scala | 49 +++++-- .../src/dotty/tools/dotc/typer/Typer.scala | 96 +++++++------ docs/_docs/internals/syntax.md | 1 + tests/neg-macros/if-without-then.check | 12 ++ tests/neg-macros/if-without-then.scala | 12 ++ tests/neg/source-import.scala | 2 +- .../run-bootstrapped/maybe-translation.scala | 128 ++++++++++++++++++ tests/run-bootstrapped/parse-dates.check | 20 +++ tests/run-bootstrapped/parse-dates.scala | 80 +++++++++++ 9 files changed, 344 insertions(+), 56 deletions(-) create mode 100644 tests/neg-macros/if-without-then.check create mode 100644 tests/neg-macros/if-without-then.scala create mode 100644 tests/run-bootstrapped/maybe-translation.scala create mode 100644 tests/run-bootstrapped/parse-dates.check create mode 100644 tests/run-bootstrapped/parse-dates.scala diff --git a/compiler/src/dotty/tools/dotc/parsing/Parsers.scala b/compiler/src/dotty/tools/dotc/parsing/Parsers.scala index 84be6cdd835b..27910cd13818 100644 --- a/compiler/src/dotty/tools/dotc/parsing/Parsers.scala +++ b/compiler/src/dotty/tools/dotc/parsing/Parsers.scala @@ -2678,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.errorHandlingEnabled && location == Location.InBlock) case WHILE => atSpan(in.skipToken()) { val cond = condExpr(DO) @@ -2765,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) => @@ -2828,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; @@ -4174,8 +4191,6 @@ object Parsers { if allSourceVersionNames.contains(imported) && prefix.isEmpty then if !outermost then syntaxError(em"source version import is only allowed at the toplevel", id.span) - else if ctx.compilationUnit.sourceVersion.isDefined then - syntaxError(em"duplicate source version import", id.span) else if illegalSourceVersionNames.contains(imported) then val candidate = val nonMigration = imported.toString.replace("-migration", "") @@ -4186,7 +4201,12 @@ object Parsers { case _ => baseMsg syntaxError(msg, id.span) else - ctx.compilationUnit.sourceVersion = Some(SourceVersion.valueOf(imported.toString)) + val sourceVersion = SourceVersion.valueOf(imported.toString) + ctx.compilationUnit.sourceVersion match + case Some(existing) if existing != sourceVersion => + syntaxError(em"conflicting source version import: $existing and $sourceVersion", id.span) + case _ => + ctx.compilationUnit.sourceVersion = Some(sourceVersion) case None => imp @@ -5250,6 +5270,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/typer/Typer.scala b/compiler/src/dotty/tools/dotc/typer/Typer.scala index 7b9f94b56068..8c06e68c0c67 100644 --- a/compiler/src/dotty/tools/dotc/typer/Typer.scala +++ b/compiler/src/dotty/tools/dotc/typer/Typer.scala @@ -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.errorHandlingEnabled) + val elsep1 = + if tree.elsep.isEmpty then tpd.unitLiteral else typed(tree.elsep) + val labelType = defn.maybe_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.maybe_provided1), tree.cond :: Nil) + else cpy.Apply(tree)(untpd.ref(defn.maybe_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. diff --git a/docs/_docs/internals/syntax.md b/docs/_docs/internals/syntax.md index b677f7700425..75adf81f4de4 100644 --- a/docs/_docs/internals/syntax.md +++ b/docs/_docs/internals/syntax.md @@ -330,6 +330,7 @@ BlockStat ::= Import | Extension | Expr1 | EndMarker + | ‘if’ Expr [‘else’ Expr] TypeBlock ::= {TypeBlockStat semi} Type TypeBlockStat ::= ‘type’ {nl} TypeDef diff --git a/tests/neg-macros/if-without-then.check b/tests/neg-macros/if-without-then.check new file mode 100644 index 000000000000..62f2f5b83100 --- /dev/null +++ b/tests/neg-macros/if-without-then.check @@ -0,0 +1,12 @@ +-- Error: tests/neg-macros/if-without-then.scala:5:2 ------------------------------------------------------------------- +5 | if (true) println("yes") // error + | ^^^^^^^^^^^^^^^^^^^^^^^^ + | `if` without `then` is illegal here since no given of type maybe.CanErr[Unit] is available. +-- Error: tests/neg-macros/if-without-then.scala:7:2 ------------------------------------------------------------------- +7 | if (true) // error + | ^^^^^^^^^ + | `if` without `then` is illegal here since no given of type maybe.CanErr[Unit] is available. +-- Error: tests/neg-macros/if-without-then.scala:10:2 ------------------------------------------------------------------ +10 | if true else "error" // error + | ^^^^^^^^^^^^^^^^^^^^ + | `if` without `then` is illegal here since no given of type maybe.CanErr[String] is available. diff --git a/tests/neg-macros/if-without-then.scala b/tests/neg-macros/if-without-then.scala new file mode 100644 index 000000000000..c32615846efc --- /dev/null +++ b/tests/neg-macros/if-without-then.scala @@ -0,0 +1,12 @@ +//> using options -Yexplicit-nulls +import language.experimental.errorHandling + +def foo = + if (true) println("yes") // error + + if (true) // error + println("yes") + + if true else "error" // error + + diff --git a/tests/neg/source-import.scala b/tests/neg/source-import.scala index 5cb7f532ea5c..9e886b8dceed 100644 --- a/tests/neg/source-import.scala +++ b/tests/neg/source-import.scala @@ -1,5 +1,5 @@ import language.`3.0` -import language.`3.0` // error +import language.`3.1` // error class C: import language.`3.0-migration` // error diff --git a/tests/run-bootstrapped/maybe-translation.scala b/tests/run-bootstrapped/maybe-translation.scala new file mode 100644 index 000000000000..9a1f9d4d5b12 --- /dev/null +++ b/tests/run-bootstrapped/maybe-translation.scala @@ -0,0 +1,128 @@ +//> using options -Yexplicit-nulls +import language.experimental.errorHandling +import scala.util.{Ok, Err} + +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(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: Object): Object = + (boundary2[Object]: + { + val y: Int = + +*/ + +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.withErr(x, "not an int") + matchResult2[Int]: + { + case val x6: Object = x$proxy1 + if + (null == x6).unary_!() && + x6.isInstanceOf[scala.runtime.Fail].unary_!() + then + { + case val x9: Int = Int.unbox(x6:Object) + case val y: Int = x9 + return[matchResult2] y:Int + } + else () + { + case val x7: Object = + x6.asInstanceOf[scala.runtime.Fail].elem() + case val e: String = x7.asInstanceOf[String] + return[matchResult2] + return[boundary2] + new scala.runtime.Fail(e):Object:Object + } + throw new MatchError(x6) + } + } + if (y > 0).unary_!() then + return[boundary2] + new scala.runtime.Fail("not positive"):Object:Object + else () + Int.box(y:Int) + } + ):Object:Object + +*/ + +def maybeReverse(s: String?): String? = s match + case Ok(s) => s.reverse + case null => null + +/** With -Vprint:erasure should produce something like: + + matchResult3[String]: + { + case val x11: String = s + if (null == x11).unary_!() then + { + case val x12: String = x11.asInstanceOf[String] + case val s: String = x12 + return[matchResult3] + { + scala.collection.StringOps.reverse$extension(augmentString(s)) + :String + } + } + else () + return[matchResult3] + { + null + } + throw new MatchError(x11) + } +*/ +@main def Test = + assert(foo(6) == 6) + assert(foo(11) == null) + assert(bar(6) == 6) + assert(bar(-1) == null) + assert(baz(6) == 6) + assert(baz(-1) == Err("not positive"), baz(-1)) 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..7a1d58469b31 --- /dev/null +++ b/tests/run-bootstrapped/parse-dates.scala @@ -0,0 +1,80 @@ +//> using options -Yexplicit-nulls +import language.experimental.errorHandling +import scala.util.{Ok, Err} + + +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")) From c843274a85fffe6eb075058d6528901eefd23e9c Mon Sep 17 00:00:00 2001 From: odersky Date: Tue, 1 Sep 2026 12:07:54 +0200 Subject: [PATCH 4/6] Let magic imply errorHandling, future and captureChecking Comes with the following bug fixes: 1. Maybe arguments should not be boxed. 2. Capture sets of module vals need to be interpolated 3. When reporting overriding errors under -explain, use isSubTypeWhenFrozen 4. Interpolate self types of modules downwards 5. Make Tuple a Pure trait (fails i13968-maybe.scala otherwise) 6. Make newSyntax a def so that it can see language imports Also: Move spec string support into new package magic.compiletime --- .../dotty/tools/dotc/cc/CheckCaptures.scala | 6 +- compiler/src/dotty/tools/dotc/cc/Setup.scala | 8 +- .../src/dotty/tools/dotc/config/Feature.scala | 5 +- .../dotty/tools/dotc/core/Definitions.scala | 13 +- .../dotty/tools/dotc/parsing/Scanners.scala | 4 +- .../dotty/tools/dotc/reporting/messages.scala | 20 +- .../tools/dotc/typer/ErrorReporting.scala | 6 +- .../dotty/tools/dotc/typer/SpecStrings.scala | 2 +- .../src/dotty/tools/dotc/typer/Typer.scala | 2 +- docs/_docs/reference/error-codes/E164.md | 4 +- library/src/scala/Tuple.scala | 2 +- library/src/scala/compiletime/package.scala | 7 - .../src/scala/magic/compiletime/package.scala | 16 + project/MiMaFilters.scala | 2 + .../captures/effect-swaps-explicit.check | 2 +- .../captures/effect-swaps.check | 2 +- .../captures/local-mutables-2.check | 4 +- tests/pos/spec-strings.scala | 1 + .../stdlibExperimentalDefinitions.scala | 6 +- tests/run/LazyLists-magic.scala | 112 +++++++ tests/run/fully-abstract-nat-magic.check | 35 +++ tests/run/fully-abstract-nat-magic.scala | 296 ++++++++++++++++++ 22 files changed, 517 insertions(+), 38 deletions(-) create mode 100644 library/src/scala/magic/compiletime/package.scala create mode 100644 tests/run/LazyLists-magic.scala create mode 100644 tests/run/fully-abstract-nat-magic.check create mode 100644 tests/run/fully-abstract-nat-magic.scala diff --git a/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala b/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala index f0bad6e1a4e5..8f6c7e34c767 100644 --- a/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala +++ b/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala @@ -1535,9 +1535,10 @@ class CheckCaptures extends Recheck, SymTransformer: val saved = curEnv curEnv = Env(cls, EnvKind.Regular, localSet, curEnv) + val selfType = cls.classInfo.selfType try // (2) Capture set of self type includes capture set of class - val thisSet = cls.classInfo.selfType.captureSet.withDescription(i"of the self type of $cls") + val thisSet = selfType.captureSet.withDescription(i"of the self type of $cls") withGlobalCapAsRoot: // OK? We need this here since self types use GlobalAny instead of a LocalCap checkSubset(localSet, thisSet, tree.srcPos) @@ -1576,8 +1577,9 @@ class CheckCaptures extends Recheck, SymTransformer: finally if cls.is(ModuleClass) then interpolate(cls.sourceModule.info, cls.sourceModule) + interpolate(selfType, cls) else - capt.println(i"Use set of $cls = ${cls.useSet}, self type: ${cls.classInfo.selfType}") + capt.println(i"Use set of $cls = ${cls.useSet}, self type: $selfType") completed += cls curEnv = saved end recheckClassDef diff --git a/compiler/src/dotty/tools/dotc/cc/Setup.scala b/compiler/src/dotty/tools/dotc/cc/Setup.scala index 421557181012..33f8ebcdf725 100644 --- a/compiler/src/dotty/tools/dotc/cc/Setup.scala +++ b/compiler/src/dotty/tools/dotc/cc/Setup.scala @@ -311,9 +311,11 @@ class Setup extends PreRecheck, SymTransformer, SetupAPI: CapturingType(OrType(parent1, tp2, tp.isSoft), refs1, tp1.isBoxed) case tp @ OrType(tp1, tp2 @ CapturingType(parent2, refs2)) => CapturingType(OrType(tp1, parent2, tp.isSoft), refs2, tp2.isBoxed) - case tp @ AppliedType(tycon, args) - if !defn.isFunctionClass(tp.dealias.typeSymbol) && (tp.dealias eq tp) => - tp.derivedAppliedType(tycon, args.mapConserve(_.boxDeeply)) + case tp @ AppliedType(tycon, args) if tp.dealias eq tp => + val sym = tycon.typeSymbol + if !defn.isFunctionClass(sym) && sym != defn.MaybeClass + then tp.derivedAppliedType(tycon, args.mapConserve(_.boxDeeply)) + else tp case tp: RealTypeBounds => tp.derivedTypeBounds(tp.lo, tp.hi.boxDeeply) case tp: LazyRef => diff --git a/compiler/src/dotty/tools/dotc/config/Feature.scala b/compiler/src/dotty/tools/dotc/config/Feature.scala index f660a0c4d469..f17c51ac5f39 100644 --- a/compiler/src/dotty/tools/dotc/config/Feature.scala +++ b/compiler/src/dotty/tools/dotc/config/Feature.scala @@ -351,10 +351,13 @@ object Feature: ctx.compilationUnit.safeMode = true true case `magic` => + enableCC() ctx.compilationUnit.magic = true + ctx.compilationUnit.newErrorHandling = 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 `errorHandling` => diff --git a/compiler/src/dotty/tools/dotc/core/Definitions.scala b/compiler/src/dotty/tools/dotc/core/Definitions.scala index f7335b2b6732..3306ea4da468 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,16 @@ 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 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 + // Maybe-related @tu lazy val MaybeClass: ClassSymbol = requiredClass("scala.compiletime.Maybe") @tu lazy val ValidClass: ClassSymbol = requiredClass("scala.runtime.Valid") 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/reporting/messages.scala b/compiler/src/dotty/tools/dotc/reporting/messages.scala index e2a7cd77227b..c9a9599f968e 100644 --- a/compiler/src/dotty/tools/dotc/reporting/messages.scala +++ b/compiler/src/dotty/tools/dotc/reporting/messages.scala @@ -1223,7 +1223,7 @@ extends DeclarationMsg(OverrideErrorID), NoDisambiguation: override def canExplain = memberTp.exists && otherTp.exists def explain(using Context) = - if canExplain then err.whyNoMatchStr(memberTp, otherTp) else "" + if canExplain then err.whyNoMatchStr(memberTp, otherTp, frozen = true) else "" class ForwardReferenceExtendsOverDefinition(value: Symbol, definition: Symbol)(using Context) extends ReferenceMsg(ForwardReferenceExtendsOverDefinitionID) { @@ -3960,7 +3960,7 @@ final class IllegalUseOfSpecialized(using Context) inline def foo[T: Specialized](v: Vec[T]) = v.x In this instance it was used in a way which is unsupported, such as - trying to create a type synonym or a value with explicit type Specialized[X]. + trying to create a type synonym or a value with explicit type Specialized[X]. """ /** Shows up as a TypeError (in the notes field) if variance is attempted @@ -3974,17 +3974,17 @@ final class IllegalVarianceInSpecializedTraitsNote(using Context) extends Note: - Primitives are specialized: Foo[Int] erases to Foo$$sp$$Int - Reference types are specialized to the highest non-top class: Foo[Lion] erases to Foo$$sp$$Animal - Top classes are erased normally: Foo[Any] / Foo[AnyVal] / Foo[Object] / Foo[AnyRef] erase to Foo. - This means that variance patterns that cross these erasure categories will fail at + This means that variance patterns that cross these erasure categories will fail at runtime due to a ClassCastException, so they are not permitted. Please see the docs for more information on how specialized traits are erased. Suggested fixes: - Make the type of the target site more general e.g. Foo[Object] instead of Foo[Animal]. - - Reconsider if you really need to use Nothing / Object / Any / AnyRef / AnyVal in your code. + - Reconsider if you really need to use Nothing / Object / Any / AnyRef / AnyVal in your code. - Remove Specialized from the definition of the corresponding parameter. """ - override def covers(other: Note)(using Context): Boolean = + override def covers(other: Note)(using Context): Boolean = other.isInstanceOf[IllegalVarianceInSpecializedTraitsNote] final class VarianceInSpecializedTraitsLimitation(using Context) @@ -3997,17 +3997,17 @@ final class VarianceInSpecializedTraitsLimitation(using Context) - Primitives are specialized: Foo[Int] erases to Foo$$sp$$Int - Reference types are specialized to the highest non-top class: Foo[Lion] erases to Foo$$sp$$Animal - Top classes are erased normally: Foo[Any] / Foo[AnyVal] / Foo[Object] / Foo[AnyRef] erase to Foo. - This means that certain variance patterns that cross these erasure categories will fail at + This means that certain variance patterns that cross these erasure categories will fail at runtime due to a ClassCastException, so they are not permitted. - - For example, treating Foo[Any] as Foo[Animal] via contravariance is not allowed with Specialized. + + For example, treating Foo[Any] as Foo[Animal] via contravariance is not allowed with Specialized. Please see the docs for more information on how specialized traits are erased. If you accept this limitation you can silence this warning with @nowarn. For example: - + @nowarn("id=E${VarianceInSpecializedTraitsLimitationID.errorNumber}") inline trait Foo[-T: Specialized]: - + Otherwise, remove Specialized, or remove the variance. """ diff --git a/compiler/src/dotty/tools/dotc/typer/ErrorReporting.scala b/compiler/src/dotty/tools/dotc/typer/ErrorReporting.scala index 8b873666b9d4..9db68a4f6367 100644 --- a/compiler/src/dotty/tools/dotc/typer/ErrorReporting.scala +++ b/compiler/src/dotty/tools/dotc/typer/ErrorReporting.scala @@ -226,7 +226,7 @@ object ErrorReporting { else "" /** A subtype log explaining why `found` does not conform to `expected` */ - def whyNoMatchStr(found: Type, expected: Type): String = + def whyNoMatchStr(found: Type, expected: Type, frozen: Boolean = false): String = val header = i"""I tried to show that | $found @@ -241,7 +241,9 @@ object ErrorReporting { else i"""a constraint with: |$c""" - i"""${TypeComparer.explained(_.isSubType(found, expected), header, short = !ctx.settings.Ydebug.value)} + i"""${TypeComparer.explained( + if frozen then _.isSubTypeWhenFrozen(found, expected) + else _.isSubType(found, expected), header, short = !ctx.settings.Ydebug.value)} | |The tests were made under $constraintText""" 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 8c06e68c0c67..32915b936d12 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 diff --git a/docs/_docs/reference/error-codes/E164.md b/docs/_docs/reference/error-codes/E164.md index 8d263cec8bff..b11c7874c507 100644 --- a/docs/_docs/reference/error-codes/E164.md +++ b/docs/_docs/reference/error-codes/E164.md @@ -39,8 +39,8 @@ class Derived extends Base { | => String | but none of the attempts shown below succeeded: | - | ==> => Int <: => String - | ==> Int <: String = false + | ==> => Int <: => String in frozen constraint + | ==> Int <: String in frozen constraint = false | | The tests were made under the empty constraint ----------------------------------------------------------------------------- diff --git a/library/src/scala/Tuple.scala b/library/src/scala/Tuple.scala index e1df38d7ade0..49f089cb3509 100644 --- a/library/src/scala/Tuple.scala +++ b/library/src/scala/Tuple.scala @@ -6,7 +6,7 @@ import compiletime.* import compiletime.ops.int.* /** Tuple of arbitrary arity. */ -sealed trait Tuple extends Product { +sealed trait Tuple extends Product, caps.Pure { import Tuple.* /** Creates a copy of this tuple as an Array. */ 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/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/project/MiMaFilters.scala b/project/MiMaFilters.scala index e543bb8ab44c..8bd13f639de6 100644 --- a/project/MiMaFilters.scala +++ b/project/MiMaFilters.scala @@ -189,6 +189,8 @@ object MiMaFilters { ProblemFilters.exclude[MissingClassProblem]("scala.language$experimental$specializedTraits$"), ProblemFilters.exclude[MissingClassProblem]("scala.runtime.stdLibPatches.language$experimental$specializedTraits$"), + ProblemFilters.exclude[MissingClassProblem]("scala.language$experimental$errorHandling$"), + // scala/scala3#26100: lock-free initialization extracted to a shared base class ProblemFilters.exclude[MissingTypesProblem]("scala.collection.immutable.LazyList"), ProblemFilters.exclude[MissingTypesProblem]("scala.collection.immutable.LazyListIterable"), diff --git a/tests/neg-custom-args/captures/effect-swaps-explicit.check b/tests/neg-custom-args/captures/effect-swaps-explicit.check index d33414e7ac41..401877130f3f 100644 --- a/tests/neg-custom-args/captures/effect-swaps-explicit.check +++ b/tests/neg-custom-args/captures/effect-swaps-explicit.check @@ -5,7 +5,7 @@ |Capability `contextual$1` outlives its scope: it leaks into outer capture set 's1 which is owned by method fail3. |The leakage occurred when trying to match the following types: | - |Found: (contextual$1: boundary.Label[Result[Future[T^'s2]^'s3, E^'s4]^'s5]^) ?->{Result, fr, async} + |Found: (contextual$1: boundary.Label[Result[Future[T^'s2]^'s3, E^'s4]^'s5]^) ?->{fr, async} | Result[Future[T^'s6]^{fr, contextual$1}, E^'s7]^'s8 |Required: (boundary.Label[Result[Future[T^'s9]^'s1, E^'s10]^'s11]^) ?=> Result[Future[T^'s9]^'s1, E^'s10]^'s11 | diff --git a/tests/neg-custom-args/captures/effect-swaps.check b/tests/neg-custom-args/captures/effect-swaps.check index 79f5c5007fbb..a4758257cd75 100644 --- a/tests/neg-custom-args/captures/effect-swaps.check +++ b/tests/neg-custom-args/captures/effect-swaps.check @@ -5,7 +5,7 @@ |Capability `contextual$1` outlives its scope: it leaks into outer capture set 's1 which is owned by method fail3. |The leakage occurred when trying to match the following types: | - |Found: (contextual$1: boundary.Label[Result[Future[T^'s2]^'s3, E^'s4]^'s5]^) ?->{Result, fr, async} + |Found: (contextual$1: boundary.Label[Result[Future[T^'s2]^'s3, E^'s4]^'s5]^) ?->{fr, async} | Result[Future[T^'s6]^{fr, contextual$1}, E^'s7]^'s8 |Required: (boundary.Label[Result[Future[T^'s9]^'s1, E^'s10]^'s11]^) ?=> Result[Future[T^'s9]^'s1, E^'s10]^'s11 | diff --git a/tests/neg-custom-args/captures/local-mutables-2.check b/tests/neg-custom-args/captures/local-mutables-2.check index 2266bbc7d7a0..e995affc9d29 100644 --- a/tests/neg-custom-args/captures/local-mutables-2.check +++ b/tests/neg-custom-args/captures/local-mutables-2.check @@ -1,7 +1,7 @@ -- Error: tests/neg-custom-args/captures/local-mutables-2.scala:7:9 ---------------------------------------------------- 7 |def foo() = // error: separation failure | ^ - |Separation failure in method foo's inferred result type (() ->{any.rd} Int, (x: Int) => Unit)^{}. + |Separation failure in method foo's inferred result type (() ->{any.rd} Int, (x: Int) => Unit). |One part, () ->{any.rd} Int, hides capabilities {any².rd}. |Another part, (x: Int) => Unit, captures capabilities {any³, any, any²}. |The two sets overlap at {`any` of value r}. @@ -12,7 +12,7 @@ -- Error: tests/neg-custom-args/captures/local-mutables-2.scala:11:10 -------------------------------------------------- 11 |def foo2() = // error: separation failure | ^ - | Separation failure in method foo2's inferred result type (() ->{any.rd} Int, (x: Int) => Unit)^{}. + | Separation failure in method foo2's inferred result type (() ->{any.rd} Int, (x: Int) => Unit). | One part, (x: Int) => Unit, hides capabilities {any}. | Another part, () ->{any.rd} Int, captures capabilities {any.rd}. | The two sets overlap at {`any` of variable r}. 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/run-tasty-inspector/stdlibExperimentalDefinitions.scala b/tests/run-tasty-inspector/stdlibExperimentalDefinitions.scala index f52b576ab7e4..78571e33634a 100644 --- a/tests/run-tasty-inspector/stdlibExperimentalDefinitions.scala +++ b/tests/run-tasty-inspector/stdlibExperimentalDefinitions.scala @@ -96,7 +96,7 @@ val experimentalDefinitionInLibrary = Set( // New feature: Erased trait "scala.compiletime.Erased", - + // New feature: Specialized traits "scala.specialize.Specialized", "scala.specialize.Specialized$", @@ -115,6 +115,10 @@ val experimentalDefinitionInLibrary = Set( "scala.runtime.MaybeCase", "scala.maybe", "scala.maybe$", + + // New feature: magic + "scala.magic.compiletime.package$.$spec", + "scala.magic.compiletime.package$.$wrappedType", ) diff --git a/tests/run/LazyLists-magic.scala b/tests/run/LazyLists-magic.scala new file mode 100644 index 000000000000..c62890f808ed --- /dev/null +++ b/tests/run/LazyLists-magic.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] extends caps.Pure: + + 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/fully-abstract-nat-magic.check b/tests/run/fully-abstract-nat-magic.check new file mode 100644 index 000000000000..bb36ec67992e --- /dev/null +++ b/tests/run/fully-abstract-nat-magic.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-magic.scala b/tests/run/fully-abstract-nat-magic.scala new file mode 100644 index 000000000000..f2ecd64434a0 --- /dev/null +++ b/tests/run/fully-abstract-nat-magic.scala @@ -0,0 +1,296 @@ +//> using options -Yexplicit-nulls +import language.experimental.magic + +import scala.reflect.ClassTag +import language.implicitConversions +import util.Ok + +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 extends caps.Pure { + + // === 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 + } + } + +} From f7458fe04966b6aaf785230dea7b3293dfcd3535 Mon Sep 17 00:00:00 2001 From: odersky Date: Tue, 1 Sep 2026 20:05:06 +0200 Subject: [PATCH 5/6] Don't warn for null.isInstanceOf[T] This could be the result of inlining, in which case the warning would be annoying. Also, the warning message is about divergence not about redundant tests. --- compiler/src/dotty/tools/dotc/transform/TypeTestsCasts.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/compiler/src/dotty/tools/dotc/transform/TypeTestsCasts.scala b/compiler/src/dotty/tools/dotc/transform/TypeTestsCasts.scala index dbab1e35d808..450dbbdfcef3 100644 --- a/compiler/src/dotty/tools/dotc/transform/TypeTestsCasts.scala +++ b/compiler/src/dotty/tools/dotc/transform/TypeTestsCasts.scala @@ -257,7 +257,7 @@ object TypeTestsCasts { if expr.tpe.isNotNull then constant(expr, Literal(Constant(true))) else expr.testNotNull else { - if expr.tpe.isBottomType then + if expr.tpe.hasClassSymbol(defn.NothingClass) then report.warning(TypeTestAlwaysDiverges(expr.tpe, testType), tree.srcPos) val nestedCtx = ctx.fresh.setNewTyperState() val foundClsSyms = effectiveClassSymbols(expr.tpe.widen) From b3ed0c388053eea0353a53d17f7bbb8bb53b6c48 Mon Sep 17 00:00:00 2001 From: odersky Date: Wed, 2 Sep 2026 14:58:28 +0200 Subject: [PATCH 6/6] Update MiMaFilters --- project/MiMaFilters.scala | 2 ++ 1 file changed, 2 insertions(+) diff --git a/project/MiMaFilters.scala b/project/MiMaFilters.scala index 8bd13f639de6..c8c6b12c2325 100644 --- a/project/MiMaFilters.scala +++ b/project/MiMaFilters.scala @@ -16,6 +16,8 @@ object MiMaFilters { // new annotation carrying the component names of Java records ProblemFilters.exclude[MissingClassProblem]("scala.annotation.internal.JavaRecordFields"), ProblemFilters.exclude[MissingClassProblem]("scala.language$experimental$errorHandling$"), + ProblemFilters.exclude[MissingClassProblem]("scala.magic.compiletime.package"), + ProblemFilters.exclude[MissingClassProblem]("scala.magic.compiletime.package$"), ), // Additions since last LTS