From 1563a7818ea1c0d5fbe8c5bd0245bbca20a9afc9 Mon Sep 17 00:00:00 2001 From: odersky Date: Thu, 13 Aug 2026 15:07:00 +0200 Subject: [PATCH 01/28] Allow `?` postfix operator - Enabled by experimental.magic - Independent of language.postfixOps - Highest precedence --- .../dotty/tools/dotc/parsing/Parsers.scala | 15 +- docs/_docs/internals/syntax.md | 3 +- tests/run/errorhandling/magicTest.scala | 170 ++++++++++++++++++ tests/run/errorhandling/maybe.scala | 74 ++++++++ tests/run/postfix-qmark.scala | 21 +++ 5 files changed, 280 insertions(+), 3 deletions(-) create mode 100644 tests/run/errorhandling/magicTest.scala create mode 100644 tests/run/errorhandling/maybe.scala create mode 100644 tests/run/postfix-qmark.scala diff --git a/compiler/src/dotty/tools/dotc/parsing/Parsers.scala b/compiler/src/dotty/tools/dotc/parsing/Parsers.scala index 161275146cca..4ecb57753cab 100644 --- a/compiler/src/dotty/tools/dotc/parsing/Parsers.scala +++ b/compiler/src/dotty/tools/dotc/parsing/Parsers.scala @@ -2093,6 +2093,11 @@ object Parsers { || !canStartInfixTypeTokens.contains(ahead.token) || ahead.lineOffset > 0 + private def isPostfixQmark(isType: Boolean) = + val ahead = in.lookahead + !(if isType then canStartInfixTypeTokens else canStartInfixExprTokens).contains(ahead.token) + || ahead.lineOffset > 0 + inline def gobbleHat(): Boolean = if Feature.ccEnabled && isIdent(nme.UPARROW) then in.nextToken() @@ -2105,12 +2110,15 @@ object Parsers { refinedTypeRest(atSpan(startOffset(t)) { RefinedTypeTree(rejectWildcardType(t), refinement(indentOK = true)) }) - else if Feature.ccEnabled && in.isIdent(nme.UPARROW) && isCaptureUpArrow then + else if in.isIdent(nme.UPARROW) && Feature.ccEnabled && isCaptureUpArrow then atSpan(t.span.start): in.nextToken() if in.token == LBRACE then makeRetaining(t, captureSet(), tpnme.retains) else makeRetaining(t, Nil, tpnme.retainsCap) + else if in.isIdent(nme.?) && Feature.magicEnabled && isPostfixQmark(isType = true) then + atSpan(t.span.start): + PostfixOp(t, typeIdent()) else t } @@ -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.magicEnabled && isPostfixQmark(isType = false) then + atSpan(t.span.start): + PostfixOp(t, termIdent()) + else if in.isColon && location == Location.InParens && followingIsLambdaParams() then t match case id @ Ident(name) => if name.is(WildcardParamName) then 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/tests/run/errorhandling/magicTest.scala b/tests/run/errorhandling/magicTest.scala new file mode 100644 index 000000000000..b3441dfd29cb --- /dev/null +++ b/tests/run/errorhandling/magicTest.scala @@ -0,0 +1,170 @@ +package magicTest + +import scala.util.*, boundary.break +import caps.any +import language.experimental.magic + +/** boundary/break as a replacement for non-local returns */ +def indexOf[T](xs: List[T], elem: T): Int = + boundary: + for (x, i) <- xs.zipWithIndex do + if x == elem then break(i) + -1 + +def breakTest() = + println("breakTest") + assert(indexOf(List(1, 2, 3), 2) == 1) + assert(indexOf(List(1, 2, 3), 0) == -1) + +/** traverse becomes trivial to write */ +def traverse[T](xs: List[Option[T]]): Option[List[T]] = + optional(xs.map(_?)) + +def optTest() = + println("optTest") + assert(traverse(List(Some(1), Some(2), Some(3))) == Some(List(1, 2, 3))) + assert(traverse(List(Some(1), None, Some(3))) == None) + +/** A check function returning a Result[Unit, _] */ +inline def check[E](p: Boolean, err: E): Result[Unit, E] = + if p then Ok(()) else Err(err) + +/** Another variant of a check function that returns directly to the given + * label in case of error. + */ +inline def check_![E](p: Boolean, err: E)(using l: boundary.Label[Err[E]]): Unit = + if p then () else break(Err(err)) + +/** Use `Result` to convert exceptions to `Err` values */ +def parseDouble(s: String): Result[Double, Exception] = + Result(s.toDouble) + +def parseDoubles(ss: List[String]): Result[List[Double], Exception] = + respond: + ss.map(parseDouble(_)?) + +/** Demonstrate combination of `check` and `?`. */ +def trySqrt(x: Double) = // inferred: Result[Double, String] + respond: + check(x >= 0, s"cannot take sqrt of negative $x")? // direct jump + math.sqrt(x) + +/** Instead of `check(...)?` one can also use `check_!(...)`. + * Note use of `mapErr` to convert Exception errors to String errors. + */ +def sumRoots(xs: List[String]) = // inferred: Result[Double, String] + respond: + check_!(xs.nonEmpty, "list is empty") // direct jump + val ys = parseDoubles(xs).mapErr(_.toString)? // direct jump + ys.reduce((x, y) => x + trySqrt(y)?) // need exception to propagate `Err` + +def resultTest() = { + println("resultTest") + def assertFail(value: Any, s: String) = value match + case Err(msg: String) => assert(msg.contains(s)) + assert(sumRoots(List("1", "4", "9")) == Ok(6)) + assertFail(sumRoots(List("1", "-2", "4")), "cannot take sqrt of negative") + assertFail(sumRoots(List()), "list is empty") + assertFail(sumRoots(List("1", "3ab")), "NumberFormatException") + + val xs = sumRoots(List("1", "-2", "4")) *: sumRoots(List()) *: sumRoots(List("1", "3ab")) *: Result.empty + xs match + case Err(msgs) => assert(msgs.length == 3) + case _ => assert(false) + + val ys = sumRoots(List("1", "2", "4")) *: sumRoots(List("1")) *: sumRoots(List("2")) *: Result.empty + ys match + case Ok((a, b, c)) => // ok + case _ => assert(false) +} + +def validateTest() = { + println("validateTest") + import Validation.validate + + case class Email private (value: String) + object Email: + def from(raw: String): Result[Email, String] = + if raw.contains("@") then Ok(Email(raw)) // demo!! + else Err(s"Invalid email ${raw}") + case class Form(name: String, email: Email, age: Int) + + def validatedForm(name: String, rawEmail: String, age: Int, confirmed: Boolean): Result[Form, List[String]] = + validate: v => + v.require(!name.isEmpty, "Missing name") + v.test(name.head.isUpper, s"${name} does not start with uppercase letter") + val email = v.test(Email.from(rawEmail)) + v.test(age >= 18, s"Age ${age} is below minimum age 18") + v.test(confirmed, "Missing confirmation") + Form(name, email.valid, age) + + // Good stuff: can no longer edit the scope if you leak it + // def leak(): Unit = + // val leaked: Result[Validation[String]^, List[String]] = Validation.validate: v => + // v + // leaked match + // case Ok(scope) => + // scope.appendOne("leaked error") // error + // ??? + // case Err(errs) => () + + type JsonDict = Map[String, String] + def jsonDict(elems: (String, String)*): JsonDict = Map(elems*) + + enum InvoiceOrRefund: + case Invoice(customerId: String, amount: BigInt) + case Refund(invoiceId: String, reason: String) + + def validateJson(json: JsonDict): Result[InvoiceOrRefund, List[String]] = + validate: v => + val kind = v.require(Result.fromOption(json.get("kind"), "missing 'kind'")) + v.require(kind == "invoice" || kind == "refund", s"invalid 'kind' ${kind}") + if kind == "invoice" then + val customerId = v.test(Result.fromOption(json.get("customerId"), s"Missing customerId")) + val amount = v.test { + respond: + val amount = Result.fromOption(json.get("amount"), s"Missing amount")? + Result(BigInt(amount)).mapErr(err => s"Invalid amount: ${err.getMessage}")? + } + InvoiceOrRefund.Invoice(customerId.valid, amount.valid) + else + val invoiceId = v.test(Result.fromOption(json.get("invoiceId"), s"Missing invoiceId")) + val reason = v.test(Result.fromOption(json.get("reason"), s"Missing reason")) + InvoiceOrRefund.Refund(invoiceId.valid, reason.valid) + + def printResult[T, E](result: Result[T, List[E]]): Unit = result match + case Ok(value) => println(s"ok: $value") + case Err(errs) => println(s"err: ${errs.zipWithIndex.map { case (e, i) => s"[$i]: $e" }.mkString(", ")}") + + val p1 = validatedForm("Bob", "Bob@example.com", 21, true) // TTTT + val p2 = validatedForm("", "Bob@example.com", 21, true) // F (abort early) + val p3 = validatedForm("bob", "Bob@example.com", 21, true) // FTTT + val p4 = validatedForm("Bob", "bad-email", 21, true) // TFTT + val p5 = validatedForm("Bob", "Bob@example.com", 16, true) // TTFT + val p6 = validatedForm("Bob", "Bob@example.com", 21, false) // TTTF + val p7 = validatedForm("bob", "bad-email", 16, false) // FFFF + printResult(p1) + printResult(p2) + printResult(p3) + printResult(p4) + printResult(p5) + printResult(p6) + printResult(p7) + + println("----") + + val j1 = validateJson(jsonDict("kind" -> "invoice", "customerId" -> "c1", "amount" -> "100")) // ok + val j2 = validateJson(jsonDict("kind" -> "refund", "invoiceId" -> "i1", "reason" -> "bad product")) // ok + val j3 = validateJson(jsonDict("kind" -> "invoice")) // [0]: missing customerId, [1]: missing amount + val j4 = validateJson(jsonDict("kind" -> "invoice", "customerId" -> "c1", "amount" -> "bad amount")) // bad amount + val j5 = validateJson(jsonDict("kind" -> "refund")) // [0]: missing invoiceId, [1]: missing reason + val j6 = validateJson(jsonDict("kind" -> "other")) // invalid kind + val j7= validateJson(jsonDict()) // missing kind + printResult(j1) + printResult(j2) + printResult(j3) + printResult(j4) + printResult(j5) + printResult(j6) + printResult(j7) +} diff --git a/tests/run/errorhandling/maybe.scala b/tests/run/errorhandling/maybe.scala new file mode 100644 index 000000000000..f5bfb6a3324a --- /dev/null +++ b/tests/run/errorhandling/maybe.scala @@ -0,0 +1,74 @@ +package scala.util +import boundary.{Label, break} +import language.experimental.magic + +infix type `??`[+R, +E] = Result[R, E] + +inline def maybe[R, E](inline body: Label[Err[E]] ?=> R): R ?? E = + boundary(Ok(body)) + +implicit def toResult[R, E](x: R): R ?? E = Ok(x) + +def NULL: Err[Unit] = Err(()) + +extension (str: String) def parseInt: Int ?? Unit = + 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 + +extension [R, D](x: R ?? D) + def withErr[E](msg: E): R ?? E = x match + case Ok(y) => Ok(y) + case Err(_) => Err(msg) + +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 provided(cond: Boolean)(using Label[Err[Unit]]): Unit = + if !cond then boundary.break(NULL) + +inline def provided[E](cond: Boolean, inline err: E)(using Label[Err[E]]): Unit = + if !cond then boundary.break(Err(err)) + +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")? + provided(1 <= day && day <= 31, s"day $day outside allowed range 1..31") + provided(1 <= month && month <= 12, 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 ?? Unit = + str.split("/") match + case Array(d, m, y) => + maybe: + val day = d.parseInt? + val month = m.parseInt? + val year = y.parseInt? + provided(1 <= day && day <= 31) + provided(1 <= month && month <= 12) + Date(day, month, year) + case _ => + NULL diff --git a/tests/run/postfix-qmark.scala b/tests/run/postfix-qmark.scala new file mode 100644 index 000000000000..7e0d6514a3cd --- /dev/null +++ b/tests/run/postfix-qmark.scala @@ -0,0 +1,21 @@ +import language.experimental.magic + +case class C(elem: Int): + def ? (y: Int) = C(elem + y) + def ? = C(elem) + +def foo(x: C, y: C) = () + +@main def Test = + val c = C(1) + val x = c ? 1 + assert(x.elem == 2) + val y = c ? + identity(1) + assert(y.elem == 1) + val z = c + ? 1 + assert(z.elem == 2) + foo(c?, c?) + val xx = c? + println("done") From 1c45a6c5d163ed64b80487fcaa6ede0c3121c763 Mon Sep 17 00:00:00 2001 From: odersky Date: Fri, 14 Aug 2026 13:45:40 +0200 Subject: [PATCH 02/28] Desugar to Magic types --- .../src/dotty/tools/dotc/ast/Desugar.scala | 5 ++++- .../src/dotty/tools/dotc/config/Feature.scala | 5 ++++- .../dotty/tools/dotc/core/Definitions.scala | 12 +++++++++-- .../dotty/tools/dotc/typer/SpecStrings.scala | 2 +- .../src/dotty/tools/dotc/typer/Typer.scala | 11 ++++++---- library/src/scala/compiletime/package.scala | 7 ------- library/src/scala/magic/$Maybe.scala | 15 ++++++++++++++ library/src/scala/magic/Ok.scala | 14 +++++++++++++ library/src/scala/magic/compiletime.scala | 15 ++++++++++++++ library/src/scala/magic/package.scala | 4 ++++ library/src/scala/magic/runtime/Valid.scala | 6 ++++++ tests/neg/yimports-stable.check | 6 +++--- tests/neg/yimports-stable/C_2.scala | 2 +- tests/neg/yimports-stable/minidef_1.scala | 2 +- tests/pos/maybe.scala | 20 +++++++++++++++++++ .../stdlibExperimentalDefinitions.scala | 8 ++++++++ 16 files changed, 113 insertions(+), 21 deletions(-) create mode 100644 library/src/scala/magic/$Maybe.scala create mode 100644 library/src/scala/magic/Ok.scala create mode 100644 library/src/scala/magic/compiletime.scala create mode 100644 library/src/scala/magic/package.scala create mode 100644 library/src/scala/magic/runtime/Valid.scala create mode 100644 tests/pos/maybe.scala diff --git a/compiler/src/dotty/tools/dotc/ast/Desugar.scala b/compiler/src/dotty/tools/dotc/ast/Desugar.scala index 0145630276fc..641606007756 100644 --- a/compiler/src/dotty/tools/dotc/ast/Desugar.scala +++ b/compiler/src/dotty/tools/dotc/ast/Desugar.scala @@ -2416,13 +2416,16 @@ object desugar { // This is a deliberate departure from scalac, where StringContext is not rooted (See #4732) Apply(Select(Apply(scalaDot(nme.StringContext), strs), id).withSpan(tree.span), elems) case PostfixOp(t, op) => - if (ctx.mode is Mode.Type) && !isBackquoted(op) && op.name == tpnme.raw.STAR then + def isOp(name: TypeName) = op.name == name && !isBackquoted(op) + if isOp(tpnme.raw.STAR) then if ctx.isJava then AppliedTypeTree(ref(defn.RepeatedParamType), t) else Annotated( AppliedTypeTree(ref(defn.SeqType), t), New(ref(defn.RepeatedAnnot.typeRef), Nil :: Nil)) + else if isOp(tpnme.?) then + AppliedTypeTree(ref(defn.MagicMaybeClass.typeRef), t /*:: ref(defn.UnitClass.typeRef)*/ :: Nil) else assert(ctx.mode.isExpr || ctx.reporter.errorsReported || ctx.mode.is(Mode.Interactive), ctx.mode) Select(t, op.name) diff --git a/compiler/src/dotty/tools/dotc/config/Feature.scala b/compiler/src/dotty/tools/dotc/config/Feature.scala index f5e0502e1689..cb0d29fab728 100644 --- a/compiler/src/dotty/tools/dotc/config/Feature.scala +++ b/compiler/src/dotty/tools/dotc/config/Feature.scala @@ -277,12 +277,15 @@ object Feature: ccEnabledSomewhere && (defn.ccExperimental.contains(sym) || sym.exists && defn.ccExperimental.contains(sym.owner)) + private def magicException(sym: Symbol)(using Context): Boolean = + Feature.magicEnabled && sym.enclosingPackageClass == defn.MagicPackageClass + def checkExperimentalDef(sym: Symbol, srcPos: SrcPos)(using Context) = val experimentalSym = if sym.hasAnnotation(defn.ExperimentalAnnot) then sym else if sym.owner.hasAnnotation(defn.ExperimentalAnnot) then sym.owner else NoSymbol - if !isExperimentalEnabled && !ccException(experimentalSym) then + if !isExperimentalEnabled && !ccException(experimentalSym) && !magicException(experimentalSym) then val msg = experimentalSym.getAnnotation(defn.ExperimentalAnnot).map { case ExperimentalAnnotation(msg) if msg.nonEmpty => s": $msg" diff --git a/compiler/src/dotty/tools/dotc/core/Definitions.scala b/compiler/src/dotty/tools/dotc/core/Definitions.scala index d0b461681a95..63b5bb395e86 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,15 @@ class Definitions { } def AnyKindType: TypeRef = AnyKindClass.typeRef + // Magic stuff + @tu lazy val MagicPackageClass: ClassSymbol = requiredPackage("scala.magic").moduleClass.asClass + @tu lazy val MagicMaybeClass: ClassSymbol = requiredClass("scala.magic.$Maybe") + + @tu lazy val MagicCompiletimeModule: Symbol = requiredModule("scala.magic.compiletime") + @tu lazy val Magic_spec: Symbol = MagicCompiletimeModule.requiredMethod("$spec") + @tu lazy val Magic_wrappedType: Symbol = MagicCompiletimeModule.requiredMethod("$wrappedType") + + // More synthetic symbols @tu lazy val andType: TypeSymbol = enterBinaryAlias(tpnme.AND, AndType(_, _)) @tu lazy val orType: TypeSymbol = enterBinaryAlias(tpnme.OR, OrType(_, _, soft = false)) diff --git a/compiler/src/dotty/tools/dotc/typer/SpecStrings.scala b/compiler/src/dotty/tools/dotc/typer/SpecStrings.scala index 63b6f0df84ad..bd3c5e1b4c39 100644 --- a/compiler/src/dotty/tools/dotc/typer/SpecStrings.scala +++ b/compiler/src/dotty/tools/dotc/typer/SpecStrings.scala @@ -152,7 +152,7 @@ trait SpecStrings { this: Typer => extractBackquoted(strLit, closing + 1) case untpd.TypedSplice(splice) => extract( - untpd.TypedSplice(ref(defn.Compiletime_wrappedType) + untpd.TypedSplice(ref(defn.Magic_wrappedType) .appliedToTypeTree(splice))) case tree: untpd.TypedSplice => extract(tree) diff --git a/compiler/src/dotty/tools/dotc/typer/Typer.scala b/compiler/src/dotty/tools/dotc/typer/Typer.scala index 0b99ca4f2cb7..65b700babab0 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 @@ -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.magicEnabled 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/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/$Maybe.scala b/library/src/scala/magic/$Maybe.scala new file mode 100644 index 000000000000..f3edd415529a --- /dev/null +++ b/library/src/scala/magic/$Maybe.scala @@ -0,0 +1,15 @@ +package scala.magic + +import scala.magic.runtime.Valid +import annotation.experimental + +/** Under experimental.magic, a trait backing maybe types `T?` */ +@experimental +trait `$Maybe`[+T] extends Any, Matchable: + def isEmpty: Boolean = (this: Any) == null + def get: T = (this: Any) match + case x: Valid => x.elem.asInstanceOf[T] + case x => x.asInstanceOf[T] + + + diff --git a/library/src/scala/magic/Ok.scala b/library/src/scala/magic/Ok.scala new file mode 100644 index 000000000000..da85d9148fe6 --- /dev/null +++ b/library/src/scala/magic/Ok.scala @@ -0,0 +1,14 @@ +package scala.magic + +import scala.magic.runtime.Valid +import annotation.experimental + +@experimental +object Ok: + inline def apply[T](x: T): `$Maybe`[T] = { + if x == null then new Valid(null) + else if x.isInstanceOf[Valid] then new Valid(x) + else x + }.asInstanceOf[`$Maybe`[T]] + + def unapply[T](x: `$Maybe`[T]): x.type = x diff --git a/library/src/scala/magic/compiletime.scala b/library/src/scala/magic/compiletime.scala new file mode 100644 index 000000000000..8b065772732d --- /dev/null +++ b/library/src/scala/magic/compiletime.scala @@ -0,0 +1,15 @@ +package scala.magic + +import annotation.experimental + +@experimental +object compiletime { + + /** 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 = () +} diff --git a/library/src/scala/magic/package.scala b/library/src/scala/magic/package.scala new file mode 100644 index 000000000000..78e4369b04e2 --- /dev/null +++ b/library/src/scala/magic/package.scala @@ -0,0 +1,4 @@ +package scala + +package object magic + diff --git a/library/src/scala/magic/runtime/Valid.scala b/library/src/scala/magic/runtime/Valid.scala new file mode 100644 index 000000000000..551f2f00fac1 --- /dev/null +++ b/library/src/scala/magic/runtime/Valid.scala @@ -0,0 +1,6 @@ +package scala.magic.runtime + +import annotation.experimental + +@experimental +class Valid(val elem: Any) diff --git a/tests/neg/yimports-stable.check b/tests/neg/yimports-stable.check index ba87116955c2..8ffb6b04bb06 100644 --- a/tests/neg/yimports-stable.check +++ b/tests/neg/yimports-stable.check @@ -1,13 +1,13 @@ error: bad preamble import hello.world.potions -- [E006] Not Found Error: tests/neg/yimports-stable/C_2.scala:4:9 ----------------------------------------------------- -4 | val v: Numb = magic // error // error +4 | val v: Numb = magix // error // error | ^^^^ | Not found: type Numb - did you mean Null? | | longer explanation available when compiling with `-explain` -- [E006] Not Found Error: tests/neg/yimports-stable/C_2.scala:4:16 ---------------------------------------------------- -4 | val v: Numb = magic // error // error +4 | val v: Numb = magix // error // error | ^^^^^ - | Not found: magic - did you mean main? + | Not found: magix - did you mean main? | | longer explanation available when compiling with `-explain` diff --git a/tests/neg/yimports-stable/C_2.scala b/tests/neg/yimports-stable/C_2.scala index 6cd8c17f6620..3343400e446b 100644 --- a/tests/neg/yimports-stable/C_2.scala +++ b/tests/neg/yimports-stable/C_2.scala @@ -1,7 +1,7 @@ //> using options -Yimports:scala,scala.Predef,hello.world.potions // class C { - val v: Numb = magic // error // error + val v: Numb = magix // error // error def greet() = println("hello, world!") } // nopos-error diff --git a/tests/neg/yimports-stable/minidef_1.scala b/tests/neg/yimports-stable/minidef_1.scala index b3ea7445df24..97e3944e34c8 100644 --- a/tests/neg/yimports-stable/minidef_1.scala +++ b/tests/neg/yimports-stable/minidef_1.scala @@ -3,7 +3,7 @@ package hello trait stuff { type Numb = Int - val magic = 42 + val magix = 42 } object world { diff --git a/tests/pos/maybe.scala b/tests/pos/maybe.scala new file mode 100644 index 000000000000..d668d9d6845d --- /dev/null +++ b/tests/pos/maybe.scala @@ -0,0 +1,20 @@ + +import language.experimental.magic +import scala.magic.* + +def toOptionStr(x: String?): Option[String] = 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 Test[T](x: T) = + val x1 = toOption(Ok("s")) + val x2 = toOption(null) + + val y1 = toOptionStr(Ok("s")) + val y2 = toOptionStr(null) + + val z1 = toOption(Ok(x)) diff --git a/tests/run-tasty-inspector/stdlibExperimentalDefinitions.scala b/tests/run-tasty-inspector/stdlibExperimentalDefinitions.scala index a22d88f7848b..27181a4397d2 100644 --- a/tests/run-tasty-inspector/stdlibExperimentalDefinitions.scala +++ b/tests/run-tasty-inspector/stdlibExperimentalDefinitions.scala @@ -100,6 +100,14 @@ val experimentalDefinitionInLibrary = Set( // New feature: Specialized traits "scala.specialize.Specialized", "scala.specialize.Specialized$" + + // New feature: magic + "scala.magic.$Maybe", + "scala.magic.Ok", + "scala.magic.Ok$", + "scala.magic.compiletime", + "scala.magic.compiletime$", + "scala.magic.runtime.Valid", ) From e4487538f0e35d86e5979db99efb820c1a344627 Mon Sep 17 00:00:00 2001 From: odersky Date: Sat, 15 Aug 2026 13:07:02 +0200 Subject: [PATCH 03/28] Fixing a crash when recompiling ExtractDependencies fell into a "NoDenotation cannot be cast to ClassDenotation" assertioin violation. When recompiling after some changes. This fix avoids that. --- compiler/src/dotty/tools/dotc/core/SymDenotations.scala | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/compiler/src/dotty/tools/dotc/core/SymDenotations.scala b/compiler/src/dotty/tools/dotc/core/SymDenotations.scala index 177fbe87ef43..6e9491144490 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() From e7018f71c4a6ea43a812e4cea1f4081f05421c4b Mon Sep 17 00:00:00 2001 From: odersky Date: Sat, 15 Aug 2026 13:28:27 +0200 Subject: [PATCH 04/28] Allow magic only if -Yexplicit-nulls is also set Otherwise the maybe logic does not work correctly. It would be nice if we could link explicit-nulls with magic, but that does not work since explicit-nulls is a global flag that has to be set when the compiler starts up. We can't even change it from run to run. --- compiler/src/dotty/tools/dotc/core/SymDenotations.scala | 6 +++++- compiler/src/dotty/tools/dotc/typer/TyperPhase.scala | 3 +++ tests/pos/maybe.scala | 2 +- tests/pos/spec-strings.scala | 1 + tests/run/errorhandling/magicTest.scala | 1 + tests/run/errorhandling/maybe.scala | 3 ++- tests/run/postfix-qmark.scala | 1 + 7 files changed, 14 insertions(+), 3 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/core/SymDenotations.scala b/compiler/src/dotty/tools/dotc/core/SymDenotations.scala index 6e9491144490..647a45817326 100644 --- a/compiler/src/dotty/tools/dotc/core/SymDenotations.scala +++ b/compiler/src/dotty/tools/dotc/core/SymDenotations.scala @@ -934,7 +934,11 @@ 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 + || symbol == defn.MagicMaybeClass else isNullableClassAfterErasure /** Is this symbol a class of which `null` is a value after erasure? diff --git a/compiler/src/dotty/tools/dotc/typer/TyperPhase.scala b/compiler/src/dotty/tools/dotc/typer/TyperPhase.scala index 7fb67db7d939..b3f57a6fdf22 100644 --- a/compiler/src/dotty/tools/dotc/typer/TyperPhase.scala +++ b/compiler/src/dotty/tools/dotc/typer/TyperPhase.scala @@ -8,6 +8,7 @@ import Phases.* import Contexts.* import Symbols.* import ImportInfo.withRootImports +import Decorators.em import parsing.{Parser => ParserPhase} import config.Printers.typr import inlines.PrepareInlineable @@ -45,6 +46,8 @@ class TyperPhase(addRootImports: Boolean = true) extends Phase { try if !unit.suspended then ctx.profiler.onUnit(ctx.phase, unit): unit.tpdTree = ctx.typer.typedExpr(unit.untpdTree) + if unit.magic && !ctx.explicitNulls then + report.error(em"magic can be enabled only if -Yexplicit-nulls is also set", unit.tpdTree.srcPos.startPos) typr.println("typed: " + unit.source) record("retained untyped trees", unit.untpdTree.treeSize) record("retained typed trees after typer", unit.tpdTree.treeSize) diff --git a/tests/pos/maybe.scala b/tests/pos/maybe.scala index d668d9d6845d..84ea6276fc0f 100644 --- a/tests/pos/maybe.scala +++ b/tests/pos/maybe.scala @@ -1,4 +1,4 @@ - +//> using options -Yexplicit-nulls import language.experimental.magic import scala.magic.* 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/errorhandling/magicTest.scala b/tests/run/errorhandling/magicTest.scala index b3441dfd29cb..5fd23906f0e9 100644 --- a/tests/run/errorhandling/magicTest.scala +++ b/tests/run/errorhandling/magicTest.scala @@ -1,3 +1,4 @@ +//> using options -Yexplicit-nulls package magicTest import scala.util.*, boundary.break diff --git a/tests/run/errorhandling/maybe.scala b/tests/run/errorhandling/maybe.scala index f5bfb6a3324a..94dc19e4b8e2 100644 --- a/tests/run/errorhandling/maybe.scala +++ b/tests/run/errorhandling/maybe.scala @@ -1,3 +1,4 @@ +//> using options -Yexplicit-nulls package scala.util import boundary.{Label, break} import language.experimental.magic @@ -13,7 +14,7 @@ def NULL: Err[Unit] = Err(()) extension (str: String) def parseInt: Int ?? Unit = try str.toInt - catch case ex: NumberFormatException => null + catch case ex: NumberFormatException => Err(null) case class Date(day: Int, month: Int, year: Int) diff --git a/tests/run/postfix-qmark.scala b/tests/run/postfix-qmark.scala index 7e0d6514a3cd..8ad843bc07f5 100644 --- a/tests/run/postfix-qmark.scala +++ b/tests/run/postfix-qmark.scala @@ -1,3 +1,4 @@ +//> using options -Yexplicit-nulls import language.experimental.magic case class C(elem: Int): From db3aca7e2e2e19d86da4e54b04c0164a8158fb5b Mon Sep 17 00:00:00 2001 From: odersky Date: Sat, 15 Aug 2026 16:14:28 +0200 Subject: [PATCH 05/28] Handling maybe types Special cases needed for subtyping, erasure, and pattern matching. --- .../dotty/tools/dotc/core/Definitions.scala | 2 + .../dotty/tools/dotc/core/TypeComparer.scala | 1 + .../dotty/tools/dotc/core/TypeErasure.scala | 13 ++- .../tools/dotc/transform/PatternMatcher.scala | 26 +++-- library/src/scala/magic/$Maybe.scala | 9 +- library/src/scala/magic/Ok.scala | 2 +- tests/pos/maybe.scala | 20 ---- tests/run/maybe.check | 19 ++++ tests/run/maybe.scala | 102 ++++++++++++++++++ tests/warn/maybe-typetest.check | 32 ++++++ tests/warn/maybe-typetest.scala | 18 ++++ 11 files changed, 210 insertions(+), 34 deletions(-) delete mode 100644 tests/pos/maybe.scala create mode 100644 tests/run/maybe.check create mode 100644 tests/run/maybe.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/core/Definitions.scala b/compiler/src/dotty/tools/dotc/core/Definitions.scala index 63b5bb395e86..da2755d3d15c 100644 --- a/compiler/src/dotty/tools/dotc/core/Definitions.scala +++ b/compiler/src/dotty/tools/dotc/core/Definitions.scala @@ -484,6 +484,7 @@ class Definitions { // Magic stuff @tu lazy val MagicPackageClass: ClassSymbol = requiredPackage("scala.magic").moduleClass.asClass @tu lazy val MagicMaybeClass: ClassSymbol = requiredClass("scala.magic.$Maybe") + @tu lazy val MagicValidClass: ClassSymbol = requiredClass("scala.magic.runtime.Valid") @tu lazy val MagicCompiletimeModule: Symbol = requiredModule("scala.magic.compiletime") @tu lazy val Magic_spec: Symbol = MagicCompiletimeModule.requiredMethod("$spec") @@ -2223,6 +2224,7 @@ class Definitions { m(TupleClass) = ProductClass m(NonEmptyTupleClass) = ProductClass m(PairClass) = ObjectClass + m(MagicMaybeClass) = ObjectClass m // ----- Initialization --------------------------------------------------- diff --git a/compiler/src/dotty/tools/dotc/core/TypeComparer.scala b/compiler/src/dotty/tools/dotc/core/TypeComparer.scala index 1e00179c4e5f..6ef358df3f5a 100644 --- a/compiler/src/dotty/tools/dotc/core/TypeComparer.scala +++ b/compiler/src/dotty/tools/dotc/core/TypeComparer.scala @@ -1516,6 +1516,7 @@ class TypeComparer(@constructorOnly initctx: Context) extends ConstraintHandling || byGadtBounds || defn.isCompiletimeAppliedType(tycon2.symbol) && compareCompiletimeAppliedType(tp2, tp1, fromBelow = true) + || tycon2.symbol == defn.MagicMaybeClass && tp1.isNotNull || tycon2.info.match case info2: TypeBounds => compareLower(info2, tyconIsTypeRef = true) diff --git a/compiler/src/dotty/tools/dotc/core/TypeErasure.scala b/compiler/src/dotty/tools/dotc/core/TypeErasure.scala index bbb29b5dc862..7c4905317b18 100644 --- a/compiler/src/dotty/tools/dotc/core/TypeErasure.scala +++ b/compiler/src/dotty/tools/dotc/core/TypeErasure.scala @@ -82,7 +82,11 @@ object TypeErasure: private val DisallowSpecialized = Property.Key[Unit] private def erasureDependsOnArgs(sym: Symbol)(using Context) = - sym == defn.ArrayClass || sym == defn.PairClass || sym.isDerivedValueClass || sym.isSpecializedTrait + sym == defn.ArrayClass + || sym == defn.PairClass + || sym == defn.MagicMaybeClass + || sym.isDerivedValueClass + || sym.isSpecializedTrait /** The arity of this tuple type, which can be made up of EmptyTuple, TupleX and `*:` pairs. * @@ -805,6 +809,7 @@ class TypeErasure(sourceLanguage: SourceLanguage, semiEraseVCs: Boolean, isConst else if (tycon.isRef(defn.PairClass)) erasePair(tp) else if (tp.isRepeatedParam) apply(tp.translateFromRepeated(toArray = sourceLanguage.isJava)) else if (semiEraseVCs && tycon.classSymbol.isDerivedValueClass) eraseDerivedValueClass(tp) + else if tycon.isRef(defn.MagicMaybeClass) then eraseMaybe(tp) else this(checkedSuperType(tp)) case tp: TermRef => this(underlyingOfTermRef(tp)) @@ -990,6 +995,12 @@ class TypeErasure(sourceLanguage: SourceLanguage, semiEraseVCs: Boolean, isConst else defn.TupleXXLClass.typeRef } + private def eraseMaybe(tp: AppliedType)(using Context): Type = + val arg = tp.args.head + if arg.isNotNull && arg.derivesFrom(defn.ObjectClass) + then apply(arg) + else defn.ObjectClass.typeRef + /** 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/transform/PatternMatcher.scala b/compiler/src/dotty/tools/dotc/transform/PatternMatcher.scala index 72c9f348f4ac..2f7c2e3826e9 100644 --- a/compiler/src/dotty/tools/dotc/transform/PatternMatcher.scala +++ b/compiler/src/dotty/tools/dotc/transform/PatternMatcher.scala @@ -390,7 +390,16 @@ object PatternMatcher { 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) + def getOfGetMatch(gm: Tree) = + val getSelection = gm.select(nme.get, _.info.isParameterless) + if gm.tpe.widen.isRef(defn.MagicMaybeClass) then + val validTpe = defn.MagicValidClass.typeRef + If(gm.isInstance(validTpe), + gm.asInstance(validTpe).select(nme.elem), + gm) + .asInstance(getSelection.tpe.widen) + else getSelection + // 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 = @@ -441,7 +450,7 @@ object PatternMatcher { if i == elemTypes.length - 1 then tree.cast(tp) else tree } unapplyProductSeqPlan(selectors, args) - else { + else { val selectors = productSelectors(getResult.info).map(ref(getResult).select(_)) unapplyProductSeqPlan(selectors, args) } @@ -528,7 +537,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 @@ -813,10 +822,13 @@ object PatternMatcher { val scrutinee = plan.scrutinee (plan.test: @unchecked) match case NonEmptyTest => - constToLiteral( - scrutinee - .select(nme.isEmpty, _.info.isParameterless) - .select(nme.UNARY_!, _.info.isParameterless)) + if scrutinee.tpe.widen.isRef(defn.MagicMaybeClass) then + scrutinee.testNotNull + else + constToLiteral( + scrutinee + .select(nme.isEmpty, _.info.isParameterless) + .select(nme.UNARY_!, _.info.isParameterless)) case NonNullTest => scrutinee.testNotNull case GuardTest => diff --git a/library/src/scala/magic/$Maybe.scala b/library/src/scala/magic/$Maybe.scala index f3edd415529a..800c4f169c4f 100644 --- a/library/src/scala/magic/$Maybe.scala +++ b/library/src/scala/magic/$Maybe.scala @@ -5,11 +5,10 @@ import annotation.experimental /** Under experimental.magic, a trait backing maybe types `T?` */ @experimental -trait `$Maybe`[+T] extends Any, Matchable: - def isEmpty: Boolean = (this: Any) == null - def get: T = (this: Any) match - case x: Valid => x.elem.asInstanceOf[T] - case x => x.asInstanceOf[T] +sealed trait `$Maybe`[+T] extends Any, Matchable: + def isEmpty: Boolean + def get: T + diff --git a/library/src/scala/magic/Ok.scala b/library/src/scala/magic/Ok.scala index da85d9148fe6..3bcf645470a2 100644 --- a/library/src/scala/magic/Ok.scala +++ b/library/src/scala/magic/Ok.scala @@ -11,4 +11,4 @@ object Ok: else x }.asInstanceOf[`$Maybe`[T]] - def unapply[T](x: `$Maybe`[T]): x.type = x + def unapply(x: `$Maybe`[Any]): x.type = x diff --git a/tests/pos/maybe.scala b/tests/pos/maybe.scala deleted file mode 100644 index 84ea6276fc0f..000000000000 --- a/tests/pos/maybe.scala +++ /dev/null @@ -1,20 +0,0 @@ -//> using options -Yexplicit-nulls -import language.experimental.magic -import scala.magic.* - -def toOptionStr(x: String?): Option[String] = 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 Test[T](x: T) = - val x1 = toOption(Ok("s")) - val x2 = toOption(null) - - val y1 = toOptionStr(Ok("s")) - val y2 = toOptionStr(null) - - val z1 = toOption(Ok(x)) 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..084f5ebad610 --- /dev/null +++ b/tests/run/maybe.scala @@ -0,0 +1,102 @@ +//> using options -Yexplicit-nulls +import language.experimental.magic +import scala.magic.* + +class C: + def toOptionAny[T](x: Any): Option[Any] = x match + case Ok(y) => Some(y) + case null => None + +def toOptionStr(x: String?): Option[String] = x match + case 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 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) => y + case WithTail(s) => 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/warn/maybe-typetest.check b/tests/warn/maybe-typetest.check new file mode 100644 index 000000000000..0c327e511970 --- /dev/null +++ b/tests/warn/maybe-typetest.check @@ -0,0 +1,32 @@ +-- [E030] Match case Unreachable Warning: tests/warn/maybe-typetest.scala:11:9 ----------------------------------------- +11 | case _ => // warn + | ^ + | Unreachable case +-- [E030] Match case Unreachable Warning: tests/warn/maybe-typetest.scala:17:9 ----------------------------------------- +17 | case _ => // warn + | ^ + | Unreachable case +-- [E092] Pattern Match Unchecked Warning: tests/warn/maybe-typetest.scala:7:9 ----------------------------------------- +7 | case y: String? => println(y) // warn + | ^ + |the type test for scala.magic.$Maybe[String] cannot be checked at runtime because its type arguments can't be determined from T + | + | longer explanation available when compiling with `-explain` +-- [E092] Pattern Match Unchecked Warning: tests/warn/maybe-typetest.scala:10:9 ---------------------------------------- +10 | case y: String? => println(y) // warn + | ^ + |the type test for scala.magic.$Maybe[String] cannot be checked at runtime because its type arguments can't be determined from scala.magic.$Maybe[T] + | + | longer explanation available when compiling with `-explain` +-- [E092] Pattern Match Unchecked Warning: tests/warn/maybe-typetest.scala:13:9 ---------------------------------------- +13 | case y: Option[String] => println(y)// warn + | ^ + |the type test for Option[String] cannot be checked at runtime because its type arguments can't be determined from T + | + | longer explanation available when compiling with `-explain` +-- [E092] Pattern Match Unchecked Warning: tests/warn/maybe-typetest.scala:16:9 ---------------------------------------- +16 | case y: Option[String] => println(y)// warn + | ^ + |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..34435526fca1 --- /dev/null +++ b/tests/warn/maybe-typetest.scala @@ -0,0 +1,18 @@ +//> using options -Yexplicit-nulls +import language.experimental.magic +import scala.magic.* + +def Test[T](x: T) = + x match + case y: String? => println(y) // warn + case _ => + Ok(x) match + case y: String? => println(y) // warn + case _ => // warn + x match + case y: Option[String] => println(y)// warn + case _ => + Some(x) match + case y: Option[String] => println(y)// warn + case _ => // warn + From b2933c6c82c66ba93be93827edc1b71bdfe2a2f4 Mon Sep 17 00:00:00 2001 From: odersky Date: Sat, 15 Aug 2026 19:40:39 +0200 Subject: [PATCH 06/28] Move some of library and tests into bootstrapped directory The erasure of Maybe changes after bootstrapped. So anything that touches it can compile only with the bootstrapped compiler. --- .../BootstrappedOnlyCompilationTests.scala | 7 +++++++ .../scala/magic/$Maybe.scala | 0 .../scala/magic/Ok.scala | 0 tests/{run => run-bootstrapped}/maybe.scala | 0 .../stdlibExperimentalDefinitions.scala | 4 ++-- .../maybe-typetest.check | 0 tests/warn-bootstrapped/maybe-typetest.scala | 18 ++++++++++++++++++ tests/warn/maybe-typetest.scala | 18 ------------------ 8 files changed, 27 insertions(+), 20 deletions(-) rename library/{src => src-bootstrapped}/scala/magic/$Maybe.scala (100%) rename library/{src => src-bootstrapped}/scala/magic/Ok.scala (100%) rename tests/{run => run-bootstrapped}/maybe.scala (100%) rename tests/{warn => warn-bootstrapped}/maybe-typetest.check (100%) create mode 100644 tests/warn-bootstrapped/maybe-typetest.scala delete mode 100644 tests/warn/maybe-typetest.scala diff --git a/compiler/test/dotty/tools/dotc/BootstrappedOnlyCompilationTests.scala b/compiler/test/dotty/tools/dotc/BootstrappedOnlyCompilationTests.scala index dfd3098a4d3b..6b8739c7b9ba 100644 --- a/compiler/test/dotty/tools/dotc/BootstrappedOnlyCompilationTests.scala +++ b/compiler/test/dotty/tools/dotc/BootstrappedOnlyCompilationTests.scala @@ -97,6 +97,13 @@ class BootstrappedOnlyCompilationTests { .checkExpectedErrors() } + @Test def warnBootstrappedOnly: Unit = { + given TestGroup = TestGroup("warnBootstrappedOnly") + aggregateTests( + compileFilesInDir("tests/warn-bootstrapped", defaultOptions), + ) + } + @Test def negWithCompiler: Unit = { implicit val testGroup: TestGroup = TestGroup("compileNegWithCompiler") aggregateTests( diff --git a/library/src/scala/magic/$Maybe.scala b/library/src-bootstrapped/scala/magic/$Maybe.scala similarity index 100% rename from library/src/scala/magic/$Maybe.scala rename to library/src-bootstrapped/scala/magic/$Maybe.scala diff --git a/library/src/scala/magic/Ok.scala b/library/src-bootstrapped/scala/magic/Ok.scala similarity index 100% rename from library/src/scala/magic/Ok.scala rename to library/src-bootstrapped/scala/magic/Ok.scala diff --git a/tests/run/maybe.scala b/tests/run-bootstrapped/maybe.scala similarity index 100% rename from tests/run/maybe.scala rename to tests/run-bootstrapped/maybe.scala diff --git a/tests/run-tasty-inspector/stdlibExperimentalDefinitions.scala b/tests/run-tasty-inspector/stdlibExperimentalDefinitions.scala index 27181a4397d2..6b740f309f8b 100644 --- a/tests/run-tasty-inspector/stdlibExperimentalDefinitions.scala +++ b/tests/run-tasty-inspector/stdlibExperimentalDefinitions.scala @@ -96,10 +96,10 @@ val experimentalDefinitionInLibrary = Set( // New feature: Erased trait "scala.compiletime.Erased", - + // New feature: Specialized traits "scala.specialize.Specialized", - "scala.specialize.Specialized$" + "scala.specialize.Specialized$", // New feature: magic "scala.magic.$Maybe", diff --git a/tests/warn/maybe-typetest.check b/tests/warn-bootstrapped/maybe-typetest.check similarity index 100% rename from tests/warn/maybe-typetest.check rename to tests/warn-bootstrapped/maybe-typetest.check diff --git a/tests/warn-bootstrapped/maybe-typetest.scala b/tests/warn-bootstrapped/maybe-typetest.scala new file mode 100644 index 000000000000..9687560a2d84 --- /dev/null +++ b/tests/warn-bootstrapped/maybe-typetest.scala @@ -0,0 +1,18 @@ +//> using options -Yexplicit-nulls -Werror +import language.experimental.magic +import scala.magic.* + +def Test[T](x: T) = + x match + case y: String? => println(y) // error + case _ => + Ok(x) match + case y: String? => println(y) // error + case _ => // error + x match + case y: Option[String] => println(y)// error + case _ => + Some(x) match + case y: Option[String] => println(y)// error + case _ => // error + diff --git a/tests/warn/maybe-typetest.scala b/tests/warn/maybe-typetest.scala deleted file mode 100644 index 34435526fca1..000000000000 --- a/tests/warn/maybe-typetest.scala +++ /dev/null @@ -1,18 +0,0 @@ -//> using options -Yexplicit-nulls -import language.experimental.magic -import scala.magic.* - -def Test[T](x: T) = - x match - case y: String? => println(y) // warn - case _ => - Ok(x) match - case y: String? => println(y) // warn - case _ => // warn - x match - case y: Option[String] => println(y)// warn - case _ => - Some(x) match - case y: Option[String] => println(y)// warn - case _ => // warn - From cb637200dd6c9309909cee285ef23c6c4916bf7a Mon Sep 17 00:00:00 2001 From: odersky Date: Sat, 15 Aug 2026 20:01:41 +0200 Subject: [PATCH 07/28] Print maybe types with `?` --- compiler/src/dotty/tools/dotc/printing/RefinedPrinter.scala | 2 ++ 1 file changed, 2 insertions(+) diff --git a/compiler/src/dotty/tools/dotc/printing/RefinedPrinter.scala b/compiler/src/dotty/tools/dotc/printing/RefinedPrinter.scala index 31f2b2b8811e..f7a3437130ac 100644 --- a/compiler/src/dotty/tools/dotc/printing/RefinedPrinter.scala +++ b/compiler/src/dotty/tools/dotc/printing/RefinedPrinter.scala @@ -274,6 +274,8 @@ class RefinedPrinter(_ctx: Context) extends PlainPrinter(_ctx) { } def appliedText(tp: Type): Text = tp match + case AppliedType(tycon, arg :: Nil) if tycon.isRef(defn.MagicMaybeClass) => + toText(arg) ~ "?" case tp @ AppliedType(tycon, args) => val namedElems = try tp.namedTupleElementTypesUpTo(200, false, normalize = false) From 2a4dd400afa845f475afc5ea5b593c3fd9758b8d Mon Sep 17 00:00:00 2001 From: odersky Date: Sat, 15 Aug 2026 20:01:56 +0200 Subject: [PATCH 08/28] Treat magic as non-viral --- compiler/src/dotty/tools/dotc/config/Feature.scala | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/config/Feature.scala b/compiler/src/dotty/tools/dotc/config/Feature.scala index cb0d29fab728..26c5b0e2fc00 100644 --- a/compiler/src/dotty/tools/dotc/config/Feature.scala +++ b/compiler/src/dotty/tools/dotc/config/Feature.scala @@ -47,7 +47,7 @@ object Feature: val specializedTraits = experimental("specializedTraits") val nonViralExperimentalFeatures: Set[TermName] = - Set(captureChecking, separationChecking, safe) + Set(captureChecking, separationChecking, safe, magic) /** Experimental language imports that imply that the importing unit * is experimental. @@ -187,12 +187,12 @@ object Feature: def quotedPatternsWithPolymorphicFunctionsEnabled(using Context) = enabled(quotedPatternsWithPolymorphicFunctions) - - def inlineTraitsEnabled(using Context) = + + def inlineTraitsEnabled(using Context) = enabledBySetting(inlineTraits) || enabledBySetting(specializedTraits) || ctx.compilationUnit.knowsInlineTraits - + /** Is pureFunctions enabled for this compilation unit? */ def pureFunsEnabled(using Context) = enabledBySetting(pureFunctions) @@ -350,7 +350,7 @@ object Feature: ctx.compilationUnit.magic = true true case `inlineTraits` => - ctx.compilationUnit.knowsInlineTraits = true + ctx.compilationUnit.knowsInlineTraits = true if ctx.run != null then ctx.run.nn.inlineTraitsImportEncountered = true true case _ => From 3b8e30034b5c7a249c38219e2b42cc570acc4abe Mon Sep 17 00:00:00 2001 From: odersky Date: Sun, 16 Aug 2026 10:41:20 +0200 Subject: [PATCH 09/28] Prepare for binary maybes Also: Drop $ in front of Maybe, no harm in using it directly, since it is a sealed trait. Also: Fix exhaustivity checking for Ok patterns --- compiler/src/dotty/tools/dotc/ast/Desugar.scala | 2 +- .../src/dotty/tools/dotc/config/Feature.scala | 2 +- .../src/dotty/tools/dotc/core/Definitions.scala | 7 +++++-- .../tools/dotc/printing/RefinedPrinter.scala | 8 ++++++-- .../dotty/tools/dotc/transform/patmat/Space.scala | 15 +++++++++++++++ compiler/src/dotty/tools/dotc/typer/Typer.scala | 4 ++-- .../scala/magic/{$Maybe.scala => Maybe.scala} | 2 +- library/src-bootstrapped/scala/magic/Ok.scala | 6 +++--- library/src/scala/magic/runtime/Fail.scala | 6 ++++++ .../stdlibExperimentalDefinitions.scala | 3 ++- 10 files changed, 42 insertions(+), 13 deletions(-) rename library/src-bootstrapped/scala/magic/{$Maybe.scala => Maybe.scala} (80%) create mode 100644 library/src/scala/magic/runtime/Fail.scala diff --git a/compiler/src/dotty/tools/dotc/ast/Desugar.scala b/compiler/src/dotty/tools/dotc/ast/Desugar.scala index 641606007756..6cb3e2c6136f 100644 --- a/compiler/src/dotty/tools/dotc/ast/Desugar.scala +++ b/compiler/src/dotty/tools/dotc/ast/Desugar.scala @@ -2425,7 +2425,7 @@ object desugar { AppliedTypeTree(ref(defn.SeqType), t), New(ref(defn.RepeatedAnnot.typeRef), Nil :: Nil)) else if isOp(tpnme.?) then - AppliedTypeTree(ref(defn.MagicMaybeClass.typeRef), t /*:: ref(defn.UnitClass.typeRef)*/ :: Nil) + AppliedTypeTree(ref(defn.MagicMaybeClass.typeRef), t :: ref(defn.UnitClass.typeRef) :: Nil) else assert(ctx.mode.isExpr || ctx.reporter.errorsReported || ctx.mode.is(Mode.Interactive), ctx.mode) Select(t, op.name) diff --git a/compiler/src/dotty/tools/dotc/config/Feature.scala b/compiler/src/dotty/tools/dotc/config/Feature.scala index 26c5b0e2fc00..c968650fe2a4 100644 --- a/compiler/src/dotty/tools/dotc/config/Feature.scala +++ b/compiler/src/dotty/tools/dotc/config/Feature.scala @@ -278,7 +278,7 @@ object Feature: || sym.exists && defn.ccExperimental.contains(sym.owner)) private def magicException(sym: Symbol)(using Context): Boolean = - Feature.magicEnabled && sym.enclosingPackageClass == defn.MagicPackageClass + Feature.magicEnabled && sym.isContainedIn(defn.MagicPackageClass) def checkExperimentalDef(sym: Symbol, srcPos: SrcPos)(using Context) = val experimentalSym = diff --git a/compiler/src/dotty/tools/dotc/core/Definitions.scala b/compiler/src/dotty/tools/dotc/core/Definitions.scala index da2755d3d15c..49b5b23a739b 100644 --- a/compiler/src/dotty/tools/dotc/core/Definitions.scala +++ b/compiler/src/dotty/tools/dotc/core/Definitions.scala @@ -483,9 +483,12 @@ class Definitions { // Magic stuff @tu lazy val MagicPackageClass: ClassSymbol = requiredPackage("scala.magic").moduleClass.asClass - @tu lazy val MagicMaybeClass: ClassSymbol = requiredClass("scala.magic.$Maybe") + @tu lazy val MagicMaybeClass: ClassSymbol = requiredClass("scala.magic.Maybe") @tu lazy val MagicValidClass: ClassSymbol = requiredClass("scala.magic.runtime.Valid") + @tu lazy val MagicOkModule: Symbol = requiredModule("scala.magic.Ok") + @tu lazy val Magic_OkUnapply: Symbol = MagicOkModule.requiredMethod(nme.unapply) + @tu lazy val MagicCompiletimeModule: Symbol = requiredModule("scala.magic.compiletime") @tu lazy val Magic_spec: Symbol = MagicCompiletimeModule.requiredMethod("$spec") @tu lazy val Magic_wrappedType: Symbol = MagicCompiletimeModule.requiredMethod("$wrappedType") @@ -1777,7 +1780,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 = diff --git a/compiler/src/dotty/tools/dotc/printing/RefinedPrinter.scala b/compiler/src/dotty/tools/dotc/printing/RefinedPrinter.scala index f7a3437130ac..e7fe09ba5911 100644 --- a/compiler/src/dotty/tools/dotc/printing/RefinedPrinter.scala +++ b/compiler/src/dotty/tools/dotc/printing/RefinedPrinter.scala @@ -274,8 +274,12 @@ class RefinedPrinter(_ctx: Context) extends PlainPrinter(_ctx) { } def appliedText(tp: Type): Text = tp match - case AppliedType(tycon, arg :: Nil) if tycon.isRef(defn.MagicMaybeClass) => - toText(arg) ~ "?" + case AppliedType(tycon, r :: e :: Nil) if tycon.isRef(defn.MagicMaybeClass) => + if e.isRef(defn.UnitClass) then + toText(r) ~ "?" + else + atPrec(InfixPrec): + toText(r) ~ " ? " ~ toText(e) case tp @ AppliedType(tycon, args) => val namedElems = try tp.namedTupleElementTypesUpTo(200, false, normalize = false) diff --git a/compiler/src/dotty/tools/dotc/transform/patmat/Space.scala b/compiler/src/dotty/tools/dotc/transform/patmat/Space.scala index 167fb9aad82b..7a0adc6a480a 100644 --- a/compiler/src/dotty/tools/dotc/transform/patmat/Space.scala +++ b/compiler/src/dotty/tools/dotc/transform/patmat/Space.scala @@ -435,6 +435,11 @@ object SpaceEngine { private def project(tp: Type)(using Context): Space = tp match { case OrType(tp1, tp2) => Or(project(tp1) :: project(tp2) :: Nil) + case tp if tp.isMaybeType => + // A maybe type `T ? E` erases to Object and is inhabited by the values of `T` + // together with `null`, which represents the invalid case. Since `Typ(tp)` stands + // for the valid values only, `null` has to be added as a separate space. + Or(Typ(tp, decomposed = true) :: nullSpace :: Nil) case tp => Typ(tp, decomposed = true) } @@ -659,6 +664,10 @@ object SpaceEngine { val AppliedType(_, tp :: Nil) = unapp.prefix.widen.dealias: @unchecked scrutineeTp <:< tp } + // `Ok.unapply` returns its argument, and the emitted non-empty test for a maybe + // type is a `!= null` test (see PatternMatcher#emitCondition). So `Ok(_)` matches + // every non-null value of a maybe type, which is what `Typ(scrutineeTp)` stands for. + || scrutineeTp.isMaybeType && unapp.symbol == defn.Magic_OkUnapply } /** Decompose a type into subspaces -- assume the type can be decomposed */ @@ -754,6 +763,12 @@ object SpaceEngine { } extension (tp: Type) + /** Is `tp` a maybe type `T ? E`? Such a type is inhabited by the values of `T` + * plus `null`, which stands for the invalid case. + */ + def isMaybeType(using Context): Boolean = + tp.isRef(defn.MagicMaybeClass) + def isDecomposableToChildren(using Context): Boolean = val cls = tp.classSymbol // e.g. Foo[List[Int]] = class List tp.hasSimpleKind // can't decompose higher-kinded types diff --git a/compiler/src/dotty/tools/dotc/typer/Typer.scala b/compiler/src/dotty/tools/dotc/typer/Typer.scala index 65b700babab0..5252d737a176 100644 --- a/compiler/src/dotty/tools/dotc/typer/Typer.scala +++ b/compiler/src/dotty/tools/dotc/typer/Typer.scala @@ -3795,8 +3795,8 @@ class Typer(@constructorOnly nestingLevel: Int = 0) extends Namer typedAppliedTypeTree( if op.name == tpnme.throws && Feature.enabled(Feature.saferExceptions) then desugar.throws(l, op, r) - //else if op.name == tpnme.? && Feature.magicEnabled then - // cpy.AppliedTypeTree(tree)(untpd.ref(defn.MaybeClass.typeRef), l :: r :: Nil) + else if op.name == tpnme.? && Feature.magicEnabled then + cpy.AppliedTypeTree(tree)(untpd.ref(defn.MagicMaybeClass.typeRef), l :: r :: Nil) else cpy.AppliedTypeTree(tree)(op, l :: r :: Nil)) else if (ctx.mode.is(Mode.Pattern)) diff --git a/library/src-bootstrapped/scala/magic/$Maybe.scala b/library/src-bootstrapped/scala/magic/Maybe.scala similarity index 80% rename from library/src-bootstrapped/scala/magic/$Maybe.scala rename to library/src-bootstrapped/scala/magic/Maybe.scala index 800c4f169c4f..77f946d0588c 100644 --- a/library/src-bootstrapped/scala/magic/$Maybe.scala +++ b/library/src-bootstrapped/scala/magic/Maybe.scala @@ -5,7 +5,7 @@ import annotation.experimental /** Under experimental.magic, a trait backing maybe types `T?` */ @experimental -sealed trait `$Maybe`[+T] extends Any, Matchable: +sealed trait Maybe[+T, +E] extends Any, Matchable: def isEmpty: Boolean def get: T diff --git a/library/src-bootstrapped/scala/magic/Ok.scala b/library/src-bootstrapped/scala/magic/Ok.scala index 3bcf645470a2..19463bcdceea 100644 --- a/library/src-bootstrapped/scala/magic/Ok.scala +++ b/library/src-bootstrapped/scala/magic/Ok.scala @@ -5,10 +5,10 @@ import annotation.experimental @experimental object Ok: - inline def apply[T](x: T): `$Maybe`[T] = { + inline def apply[T, E](x: T): Maybe[T, E] = { if x == null then new Valid(null) else if x.isInstanceOf[Valid] then new Valid(x) else x - }.asInstanceOf[`$Maybe`[T]] + }.asInstanceOf[Maybe[T, E]] - def unapply(x: `$Maybe`[Any]): x.type = x + def unapply(x: Maybe[Any, Any]): x.type = x diff --git a/library/src/scala/magic/runtime/Fail.scala b/library/src/scala/magic/runtime/Fail.scala new file mode 100644 index 000000000000..f5adb11288c6 --- /dev/null +++ b/library/src/scala/magic/runtime/Fail.scala @@ -0,0 +1,6 @@ +package scala.magic.runtime + +import annotation.experimental + +@experimental +class Invalid[+E](val elem: E) diff --git a/tests/run-tasty-inspector/stdlibExperimentalDefinitions.scala b/tests/run-tasty-inspector/stdlibExperimentalDefinitions.scala index 6b740f309f8b..d779b870ce7e 100644 --- a/tests/run-tasty-inspector/stdlibExperimentalDefinitions.scala +++ b/tests/run-tasty-inspector/stdlibExperimentalDefinitions.scala @@ -102,12 +102,13 @@ val experimentalDefinitionInLibrary = Set( "scala.specialize.Specialized$", // New feature: magic - "scala.magic.$Maybe", + "scala.magic.Maybe", "scala.magic.Ok", "scala.magic.Ok$", "scala.magic.compiletime", "scala.magic.compiletime$", "scala.magic.runtime.Valid", + "scala.magic.runtime.Invalid", ) From f350d6c08cde5193454e44d99ace5e3a214f7b83 Mon Sep 17 00:00:00 2001 From: odersky Date: Mon, 17 Aug 2026 23:04:52 +0200 Subject: [PATCH 10/28] Binary maybe --- compiler/src/dotty/tools/dotc/ast/tpd.scala | 4 + .../dotty/tools/dotc/core/Definitions.scala | 3 + .../tools/dotc/core/SymDenotations.scala | 11 +- .../dotty/tools/dotc/core/TypeComparer.scala | 6 +- .../dotty/tools/dotc/core/TypeErasure.scala | 14 +-- .../src/dotty/tools/dotc/core/Types.scala | 11 +- .../dotc/transform/InterceptedMethods.scala | 2 +- .../tools/dotc/transform/PatternMatcher.scala | 26 ++-- .../tools/dotc/transform/patmat/Space.scala | 17 ++- .../BootstrappedOnlyCompilationTests.scala | 9 +- .../src-bootstrapped/scala/magic/Err.scala | 17 +++ library/src-bootstrapped/scala/magic/Ok.scala | 4 +- library/src/scala/magic/runtime/Fail.scala | 2 +- tests/neg-bootstrapped/orelse-subtyping.check | 15 +++ tests/neg-bootstrapped/orelse-subtyping.scala | 13 ++ tests/new/test.scala | 22 ++-- tests/run-bootstrapped/orelse.scala | 112 ++++++++++++++++++ .../stdlibExperimentalDefinitions.scala | 4 +- 18 files changed, 239 insertions(+), 53 deletions(-) create mode 100644 library/src-bootstrapped/scala/magic/Err.scala create mode 100644 tests/neg-bootstrapped/orelse-subtyping.check create mode 100644 tests/neg-bootstrapped/orelse-subtyping.scala create mode 100644 tests/run-bootstrapped/orelse.scala diff --git a/compiler/src/dotty/tools/dotc/ast/tpd.scala b/compiler/src/dotty/tools/dotc/ast/tpd.scala index f5f3641b74c4..45611cb4c385 100644 --- a/compiler/src/dotty/tools/dotc/ast/tpd.scala +++ b/compiler/src/dotty/tools/dotc/ast/tpd.scala @@ -1104,6 +1104,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/core/Definitions.scala b/compiler/src/dotty/tools/dotc/core/Definitions.scala index 49b5b23a739b..1003122fcf8f 100644 --- a/compiler/src/dotty/tools/dotc/core/Definitions.scala +++ b/compiler/src/dotty/tools/dotc/core/Definitions.scala @@ -485,6 +485,7 @@ class Definitions { @tu lazy val MagicPackageClass: ClassSymbol = requiredPackage("scala.magic").moduleClass.asClass @tu lazy val MagicMaybeClass: ClassSymbol = requiredClass("scala.magic.Maybe") @tu lazy val MagicValidClass: ClassSymbol = requiredClass("scala.magic.runtime.Valid") + @tu lazy val MagicFailClass: ClassSymbol = requiredClass("scala.magic.runtime.Fail") @tu lazy val MagicOkModule: Symbol = requiredModule("scala.magic.Ok") @tu lazy val Magic_OkUnapply: Symbol = MagicOkModule.requiredMethod(nme.unapply) @@ -1604,6 +1605,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) diff --git a/compiler/src/dotty/tools/dotc/core/SymDenotations.scala b/compiler/src/dotty/tools/dotc/core/SymDenotations.scala index 647a45817326..55f12a55d005 100644 --- a/compiler/src/dotty/tools/dotc/core/SymDenotations.scala +++ b/compiler/src/dotty/tools/dotc/core/SymDenotations.scala @@ -676,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 @@ -938,7 +938,6 @@ object SymDenotations { || symbol == defn.AnyClass || symbol == defn.AnyValClass || symbol == defn.MatchableClass - || symbol == defn.MagicMaybeClass else isNullableClassAfterErasure /** Is this symbol a class of which `null` is a value after erasure? @@ -1091,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 6ef358df3f5a..25de022a0d4a 100644 --- a/compiler/src/dotty/tools/dotc/core/TypeComparer.scala +++ b/compiler/src/dotty/tools/dotc/core/TypeComparer.scala @@ -1042,6 +1042,8 @@ class TypeComparer(@constructorOnly initctx: Context) extends ConstraintHandling // Same as above; this.type is also a singleton type in spec language !ctx.explicitNulls && isNullable(tp.underlying) case tp: RefinedOrRecType => isNullable(tp.parent) + case AppliedType(tycon, _ :: errArg :: Nil) if tycon.isRef(defn.MagicMaybeClass) => + isSubType(defn.UnitType, errArg) case tp: AppliedType => isNullable(tp.tycon) case AndType(tp1, tp2) => isNullable(tp1) && isNullable(tp2) case OrType(tp1, tp2) => isNullable(tp1) || isNullable(tp2) @@ -1975,7 +1977,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 +1989,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 7c4905317b18..59d08e76fc61 100644 --- a/compiler/src/dotty/tools/dotc/core/TypeErasure.scala +++ b/compiler/src/dotty/tools/dotc/core/TypeErasure.scala @@ -79,7 +79,7 @@ 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 @@ -218,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) @@ -793,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) @@ -902,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)) @@ -914,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 { @@ -999,7 +999,7 @@ class TypeErasure(sourceLanguage: SourceLanguage, semiEraseVCs: Boolean, isConst val arg = tp.args.head if arg.isNotNull && arg.derivesFrom(defn.ObjectClass) then apply(arg) - else defn.ObjectClass.typeRef + 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 diff --git a/compiler/src/dotty/tools/dotc/core/Types.scala b/compiler/src/dotty/tools/dotc/core/Types.scala index f68267dac74c..34056abec8ea 100644 --- a/compiler/src/dotty/tools/dotc/core/Types.scala +++ b/compiler/src/dotty/tools/dotc/core/Types.scala @@ -385,7 +385,7 @@ object Types extends TypeUtils { 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 MagicMaybeType(nullable) => !nullable case tp: TypeBounds => tp.hi.isNotNull case tp: TypeProxy => tp.underlying.isNotNull case AndType(tp1, tp2) => tp1.isNotNull || tp2.isNotNull @@ -5790,6 +5790,15 @@ object Types extends TypeUtils { def unapply(tp: MatchAlias): Option[Type] = Some(tp.alias) } + object MagicMaybeType { + /** Matches types T ? E, returns E == Unit */ + def unapply(tp: Type)(using Context): Option[Boolean] = tp.dealias match + case AppliedType(tycon, _ :: errArg :: Nil) if tycon.isRef(defn.MagicMaybeClass) => + Some(defn.unitSuperClasses.contains(errArg.classSymbol)) + case _ => + None + } + // ----- Annotated and Import types ----------------------------------------------- /** An annotated type tpe @ annot */ diff --git a/compiler/src/dotty/tools/dotc/transform/InterceptedMethods.scala b/compiler/src/dotty/tools/dotc/transform/InterceptedMethods.scala index ad91ddcaabc4..7ffbe4990c34 100644 --- a/compiler/src/dotty/tools/dotc/transform/InterceptedMethods.scala +++ b/compiler/src/dotty/tools/dotc/transform/InterceptedMethods.scala @@ -86,7 +86,7 @@ class InterceptedMethods extends MiniPhase { val sym = tree.fun.symbol if sym == defn.Any_!= then - qual.select(defn.Any_==).appliedToTermArgs(tree.args).select(defn.Boolean_!).withSpan(tree.span) + qual.select(defn.Any_==).appliedToTermArgs(tree.args).not.withSpan(tree.span) else if ctx.explicitNulls then if sym == defn.Any_toString && !qual.tpe.isNotNull then ref(defn.Objects_toString).appliedTo(qual) diff --git a/compiler/src/dotty/tools/dotc/transform/PatternMatcher.scala b/compiler/src/dotty/tools/dotc/transform/PatternMatcher.scala index 2f7c2e3826e9..a321bcbc6b39 100644 --- a/compiler/src/dotty/tools/dotc/transform/PatternMatcher.scala +++ b/compiler/src/dotty/tools/dotc/transform/PatternMatcher.scala @@ -496,6 +496,10 @@ object PatternMatcher { patternPlan(casted, pat, onSuccess) }) case UnApply(extractor, implicits, args) => + val mt @ MethodType(_) = extractor.tpe.widen.runtimeChecked + val admitsNull = mt.paramInfos.headOption match + case Some(MagicMaybeType(nullable)) => nullable + case _ => false val unappPlan = if (scrutinee.info.isBottomType) // Generate a throwaway but type-correct plan. // This plan will never execute because it'll be guarded by a `NonNullTest`. @@ -510,12 +514,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) @@ -822,13 +826,17 @@ object PatternMatcher { val scrutinee = plan.scrutinee (plan.test: @unchecked) match case NonEmptyTest => - if scrutinee.tpe.widen.isRef(defn.MagicMaybeClass) then - scrutinee.testNotNull - else - constToLiteral( - scrutinee - .select(nme.isEmpty, _.info.isParameterless) - .select(nme.UNARY_!, _.info.isParameterless)) + scrutinee.tpe.widenDealias match + case AppliedType(tycon, _ :: errArg :: Nil) if tycon.isRef(defn.MagicMaybeClass) => + val test = scrutinee.testNotNull + if errArg.isRef(defn.UnitClass) + then test + else test.and(scrutinee.isInstance(defn.MagicFailClass.typeRef).not) + case _ => + constToLiteral( + scrutinee + .select(nme.isEmpty, _.info.isParameterless) + .select(nme.UNARY_!, _.info.isParameterless)) case 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 7a0adc6a480a..a4337e86b3e7 100644 --- a/compiler/src/dotty/tools/dotc/transform/patmat/Space.scala +++ b/compiler/src/dotty/tools/dotc/transform/patmat/Space.scala @@ -435,8 +435,8 @@ object SpaceEngine { private def project(tp: Type)(using Context): Space = tp match { case OrType(tp1, tp2) => Or(project(tp1) :: project(tp2) :: Nil) - case tp if tp.isMaybeType => - // A maybe type `T ? E` erases to Object and is inhabited by the values of `T` + case MagicMaybeType(/*nullable=*/true) => + // A maybe type `T?` is inhabited by the values of `T` // together with `null`, which represents the invalid case. Since `Typ(tp)` stands // for the valid values only, `null` has to be added as a separate space. Or(Typ(tp, decomposed = true) :: nullSpace :: Nil) @@ -667,7 +667,7 @@ object SpaceEngine { // `Ok.unapply` returns its argument, and the emitted non-empty test for a maybe // type is a `!= null` test (see PatternMatcher#emitCondition). So `Ok(_)` matches // every non-null value of a maybe type, which is what `Typ(scrutineeTp)` stands for. - || scrutineeTp.isMaybeType && unapp.symbol == defn.Magic_OkUnapply + || unapp.symbol == defn.Magic_OkUnapply } /** Decompose a type into subspaces -- assume the type can be decomposed */ @@ -763,12 +763,6 @@ object SpaceEngine { } extension (tp: Type) - /** Is `tp` a maybe type `T ? E`? Such a type is inhabited by the values of `T` - * plus `null`, which stands for the invalid case. - */ - def isMaybeType(using Context): Boolean = - tp.isRef(defn.MagicMaybeClass) - def isDecomposableToChildren(using Context): Boolean = val cls = tp.classSymbol // e.g. Foo[List[Int]] = class List tp.hasSimpleKind // can't decompose higher-kinded types @@ -1115,7 +1109,10 @@ object SpaceEngine { def checkReachability(m: Match)(using Context): Unit = trace(i"checkReachability($m)"): val selTyp = toUnderlying(m.selector.tpe).dealias - val isNullable = selTyp.isInstanceOf[FlexibleType] || selTyp.classSymbol.isNullableClass + val isNullable = selTyp match + case MagicMaybeType(nullable) => nullable + case _: FlexibleType => true + case _ => selTyp.classSymbol.isNullableClass val targetSpace = trace(i"targetSpace($selTyp)"): if isNullable && !ctx.mode.is(Mode.SafeNulls) then project(OrType(selTyp, ConstantType(Constant(null)), soft = false)) diff --git a/compiler/test/dotty/tools/dotc/BootstrappedOnlyCompilationTests.scala b/compiler/test/dotty/tools/dotc/BootstrappedOnlyCompilationTests.scala index 6b8739c7b9ba..6b21b807eee1 100644 --- a/compiler/test/dotty/tools/dotc/BootstrappedOnlyCompilationTests.scala +++ b/compiler/test/dotty/tools/dotc/BootstrappedOnlyCompilationTests.scala @@ -97,10 +97,17 @@ class BootstrappedOnlyCompilationTests { .checkExpectedErrors() } + @Test def negBootstrappedOnly: Unit = { + given TestGroup = TestGroup("warnBootstrappedOnly") + aggregateTests( + compileFilesInDir("tests/neg-bootstrapped", defaultOptions) + ).checkExpectedErrors() + } + @Test def warnBootstrappedOnly: Unit = { given TestGroup = TestGroup("warnBootstrappedOnly") aggregateTests( - compileFilesInDir("tests/warn-bootstrapped", defaultOptions), + compileFilesInDir("tests/warn-bootstrapped", defaultOptions) ) } diff --git a/library/src-bootstrapped/scala/magic/Err.scala b/library/src-bootstrapped/scala/magic/Err.scala new file mode 100644 index 000000000000..72191ed7c97e --- /dev/null +++ b/library/src-bootstrapped/scala/magic/Err.scala @@ -0,0 +1,17 @@ +package scala.magic + +import language.experimental.magic +import scala.magic.runtime +import annotation.experimental + +@experimental +object Err: + 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]): E? = x match + case null => Ok(().asInstanceOf[E]) + case x: runtime.Fail[E] => Ok(x.elem) + case _ => null + diff --git a/library/src-bootstrapped/scala/magic/Ok.scala b/library/src-bootstrapped/scala/magic/Ok.scala index 19463bcdceea..1fdceb886bb2 100644 --- a/library/src-bootstrapped/scala/magic/Ok.scala +++ b/library/src-bootstrapped/scala/magic/Ok.scala @@ -5,10 +5,10 @@ import annotation.experimental @experimental object Ok: - inline def apply[T, E](x: T): Maybe[T, E] = { + inline def apply[T](x: T): Maybe[T, Nothing] = { if x == null then new Valid(null) else if x.isInstanceOf[Valid] then new Valid(x) else x - }.asInstanceOf[Maybe[T, E]] + }.asInstanceOf[Maybe[T, Nothing]] def unapply(x: Maybe[Any, Any]): x.type = x diff --git a/library/src/scala/magic/runtime/Fail.scala b/library/src/scala/magic/runtime/Fail.scala index f5adb11288c6..a4eed1ba2cd1 100644 --- a/library/src/scala/magic/runtime/Fail.scala +++ b/library/src/scala/magic/runtime/Fail.scala @@ -3,4 +3,4 @@ package scala.magic.runtime import annotation.experimental @experimental -class Invalid[+E](val elem: E) +class Fail[+E](val elem: E) diff --git a/tests/neg-bootstrapped/orelse-subtyping.check b/tests/neg-bootstrapped/orelse-subtyping.check new file mode 100644 index 000000000000..f5692b011035 --- /dev/null +++ b/tests/neg-bootstrapped/orelse-subtyping.check @@ -0,0 +1,15 @@ +-- [E007] Type Mismatch Error: tests/neg-bootstrapped/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 + | + | One of the following imports might fix the problem: + | + | import scala.reflect.Selectable.reflectiveSelectable + | import scala.util.chaining.scalaUtilChainingOps + | + | + | longer explanation available when compiling with `-explain` diff --git a/tests/neg-bootstrapped/orelse-subtyping.scala b/tests/neg-bootstrapped/orelse-subtyping.scala new file mode 100644 index 000000000000..02135564212f --- /dev/null +++ b/tests/neg-bootstrapped/orelse-subtyping.scala @@ -0,0 +1,13 @@ +//> using options -Yexplicit-nulls + +import language.experimental.magic +import scala.magic.* + +val x: String ? String = null // error + +val y: String ? Unit = null // ok + +def foo[E](x: String ? E): String ? E = x + +val z = foo(null) +val _: String? = z // Unit is inferred diff --git a/tests/new/test.scala b/tests/new/test.scala index 3dc239d18b14..78703571b033 100644 --- a/tests/new/test.scala +++ b/tests/new/test.scala @@ -1,16 +1,14 @@ -mport language.experimental.erasedDefinitions +import language.experimental.magic +import scala.magic.* +import scala.util.Either -class CanSerialize[T] +def toEither[T, E](x: T ? E): Either[E, T] = x match + case Ok(y) => Right(y) + case Err(e) => Left(e) -inline given CanSerialize[String] = CanSerialize() -inline given [T: CanSerialize] => CanSerialize[List[T]] = CanSerialize() -def safeWriteObject[T <: java.io.Serializable](out: java.io.ObjectOutputStream, x: T)(using erased CanSerialize[T]) = - out.writeObject(x) +object Extract: + def unapply[T](x: T): T ? String = Ok(x) -def writeList[T](out: java.io.ObjectOutputStream, xs: List[T])(using erased CanSerialize[T]) = - safeWriteObject(out, xs) - -@main def Test(out: java.io.ObjectOutputStream) = - writeList(out, List("a", "b")) // ok - writeList(out, List[Int => Int](x => x + 1, y => y * 2)) // error \ No newline at end of file +@main def Test = 22 match + case Extract(s) => println(s) diff --git a/tests/run-bootstrapped/orelse.scala b/tests/run-bootstrapped/orelse.scala new file mode 100644 index 000000000000..1816076b85c3 --- /dev/null +++ b/tests/run-bootstrapped/orelse.scala @@ -0,0 +1,112 @@ +//> using options -Yexplicit-nulls +import language.experimental.magic +import scala.magic.* +import scala.util.Either + +class C: + def toEitherAny[T](x: Any): Either[Any, Any] = x match + case Ok(y) => Right(y) + case Err(e) => Left(e) + +def toEitherIntStr(x: Int ? String): Either[String, Int] = x match + case Ok(y) => Right(y) + case Err(e) => Left(e) + +def toEither[T, E](x: T ? E): Either[E, T] = x match + case Ok(y) => Right(y) + case Err(e) => Left(e) + +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? = x match + case Pos(y) => y + case WithTail(s) => s + case _ => null + +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)) + println(x1) + println(x2) + println(x3) + println(x4) + println(y1) + println(y3) + println(y4) + println(z1) + println(z2) + + + +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") + 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-tasty-inspector/stdlibExperimentalDefinitions.scala b/tests/run-tasty-inspector/stdlibExperimentalDefinitions.scala index d779b870ce7e..b8b11fe9beb1 100644 --- a/tests/run-tasty-inspector/stdlibExperimentalDefinitions.scala +++ b/tests/run-tasty-inspector/stdlibExperimentalDefinitions.scala @@ -108,7 +108,9 @@ val experimentalDefinitionInLibrary = Set( "scala.magic.compiletime", "scala.magic.compiletime$", "scala.magic.runtime.Valid", - "scala.magic.runtime.Invalid", + "scala.magic.Err", + "scala.magic.Err$", + "scala.magic.runtime.Fail", ) From 8ea3d2f47d9afe7ce3915817e4f7df6214630337 Mon Sep 17 00:00:00 2001 From: odersky Date: Tue, 18 Aug 2026 11:22:14 +0200 Subject: [PATCH 11/28] Optimizations for maybe matching - Drop OK unapply call, which is known to be the identity - Simplify leading null test for maybe types --- .../src/dotty/tools/dotc/transform/PatternMatcher.scala | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/transform/PatternMatcher.scala b/compiler/src/dotty/tools/dotc/transform/PatternMatcher.scala index a321bcbc6b39..13f99e18bc8f 100644 --- a/compiler/src/dotty/tools/dotc/transform/PatternMatcher.scala +++ b/compiler/src/dotty/tools/dotc/transform/PatternMatcher.scala @@ -419,7 +419,10 @@ object PatternMatcher { else if unappType.derivesFrom(defn.BooleanClass) then TestPlan(GuardTest, unapp, unapp.span, onSuccess) else - letAbstract(unapp) { unappResult => + val unappCore = unapp match + case Apply(fn, arg :: Nil) if fn.symbol == defn.Magic_OkUnapply => arg + case _ => unapp + letAbstract(unappCore) { unappResult => val isUnapplySeq = unapp.symbol.name == nme.unapplySeq if isProductMatch(unappType, args.length) && !isUnapplySeq then val selectors = productSelectors(unappType).take(args.length) @@ -828,7 +831,7 @@ object PatternMatcher { case NonEmptyTest => scrutinee.tpe.widenDealias match case AppliedType(tycon, _ :: errArg :: Nil) if tycon.isRef(defn.MagicMaybeClass) => - val test = scrutinee.testNotNull + val test = nullLiteral.select(defn.Any_!=).appliedTo(scrutinee) if errArg.isRef(defn.UnitClass) then test else test.and(scrutinee.isInstance(defn.MagicFailClass.typeRef).not) From 2db561fa3242f204d3cc3967e84d49593bdc7cdb Mon Sep 17 00:00:00 2001 From: odersky Date: Tue, 18 Aug 2026 23:27:12 +0200 Subject: [PATCH 12/28] Fixes for handling orelse types --- compiler/src/dotty/tools/dotc/ast/tpd.scala | 7 + .../dotty/tools/dotc/core/Definitions.scala | 3 + .../dotty/tools/dotc/core/TypeErasure.scala | 8 +- .../src/dotty/tools/dotc/core/Types.scala | 10 +- .../tools/dotc/transform/PatternMatcher.scala | 186 ++++++++++-------- .../src-bootstrapped/scala/magic/Err.scala | 4 +- library/src/scala/magic/runtime/Fail.scala | 3 +- tests/run-bootstrapped/orelse.check | 22 +++ tests/run-bootstrapped/orelse.scala | 6 +- 9 files changed, 159 insertions(+), 90 deletions(-) create mode 100644 tests/run-bootstrapped/orelse.check diff --git a/compiler/src/dotty/tools/dotc/ast/tpd.scala b/compiler/src/dotty/tools/dotc/ast/tpd.scala index 45611cb4c385..6ad5f1c61b34 100644 --- a/compiler/src/dotty/tools/dotc/ast/tpd.scala +++ b/compiler/src/dotty/tools/dotc/ast/tpd.scala @@ -1094,6 +1094,13 @@ object tpd extends Trees.Instance[Type] with TypedTreeInfo { receiver.select(defn.Object_ne).appliedTo(nullLit).withSpan(tree.span) } + /** `null == tree` if cond, else `null != tree` + * Simpler than `testNotNull`. TODO: Can we replace testNotNull with this? + */ + def nullTest(cond: Boolean)(using Context) = + nullLiteral.select(if cond then defn.Any_== else defn.Any_!=) + .appliedTo(tree) + /** If inititializer tree is `_`, the default value of its type, * otherwise the tree itself. */ diff --git a/compiler/src/dotty/tools/dotc/core/Definitions.scala b/compiler/src/dotty/tools/dotc/core/Definitions.scala index 1003122fcf8f..57e4f57c0468 100644 --- a/compiler/src/dotty/tools/dotc/core/Definitions.scala +++ b/compiler/src/dotty/tools/dotc/core/Definitions.scala @@ -490,6 +490,9 @@ class Definitions { @tu lazy val MagicOkModule: Symbol = requiredModule("scala.magic.Ok") @tu lazy val Magic_OkUnapply: Symbol = MagicOkModule.requiredMethod(nme.unapply) + @tu lazy val MagicErrModule: Symbol = requiredModule("scala.magic.Err") + @tu lazy val Magic_ErrUnapply: Symbol = MagicErrModule.requiredMethod(nme.unapply) + @tu lazy val MagicCompiletimeModule: Symbol = requiredModule("scala.magic.compiletime") @tu lazy val Magic_spec: Symbol = MagicCompiletimeModule.requiredMethod("$spec") @tu lazy val Magic_wrappedType: Symbol = MagicCompiletimeModule.requiredMethod("$wrappedType") diff --git a/compiler/src/dotty/tools/dotc/core/TypeErasure.scala b/compiler/src/dotty/tools/dotc/core/TypeErasure.scala index 59d08e76fc61..c7bb89aa263b 100644 --- a/compiler/src/dotty/tools/dotc/core/TypeErasure.scala +++ b/compiler/src/dotty/tools/dotc/core/TypeErasure.scala @@ -995,9 +995,15 @@ 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 = tp.args.head + 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 diff --git a/compiler/src/dotty/tools/dotc/core/Types.scala b/compiler/src/dotty/tools/dotc/core/Types.scala index 34056abec8ea..9b420cf57cf4 100644 --- a/compiler/src/dotty/tools/dotc/core/Types.scala +++ b/compiler/src/dotty/tools/dotc/core/Types.scala @@ -385,7 +385,7 @@ object Types extends TypeUtils { case tp: ConstantType => tp.value.value != null case tp: FlexibleType => false case tp: ClassInfo => !tp.cls.isNullableClass && !tp.isNothingType - case MagicMaybeType(nullable) => !nullable + case MagicMaybeType(_, _, nullable) => !nullable case tp: TypeBounds => tp.hi.isNotNull case tp: TypeProxy => tp.underlying.isNotNull case AndType(tp1, tp2) => tp1.isNotNull || tp2.isNotNull @@ -5791,10 +5791,10 @@ object Types extends TypeUtils { } object MagicMaybeType { - /** Matches types T ? E, returns E == Unit */ - def unapply(tp: Type)(using Context): Option[Boolean] = tp.dealias match - case AppliedType(tycon, _ :: errArg :: Nil) if tycon.isRef(defn.MagicMaybeClass) => - Some(defn.unitSuperClasses.contains(errArg.classSymbol)) + /** Matches types T ? E, returns (T, E, E >: Unit) */ + def unapply(tp: Type)(using Context): Option[(Type, Type, Boolean)] = tp.dealias match + case AppliedType(tycon, resArg :: errArg :: Nil) if tycon.isRef(defn.MagicMaybeClass) => + Some((resArg, errArg, defn.unitSuperClasses.contains(errArg.classSymbol))) case _ => None } diff --git a/compiler/src/dotty/tools/dotc/transform/PatternMatcher.scala b/compiler/src/dotty/tools/dotc/transform/PatternMatcher.scala index 13f99e18bc8f..5e2c2716c436 100644 --- a/compiler/src/dotty/tools/dotc/transform/PatternMatcher.scala +++ b/compiler/src/dotty/tools/dotc/transform/PatternMatcher.scala @@ -202,6 +202,7 @@ 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 IsFailTest extends Test // scrutinee.isInstanceOf[Fail] val noLengthTest = LengthTest(0, exact = false) @@ -374,38 +375,53 @@ object PatternMatcher { matchArgsPlan(selectors.take(arity - 1), args.take(arity - 1), matchSeq) } + def tupleApp(i: Int, receiver: Tree) = // manually inlining the call to NonEmptyTuple#apply, because it's an inline method + ref(defn.RuntimeTuplesModule) + .select(defn.RuntimeTuples_apply) + .appliedTo( + receiver.ensureConforms(defn.NonEmptyTupleTypeRef), // If scrutinee is a named tuple, cast to underlying tuple + Literal(Constant(i))) + + def getOfGetMatch(gm: Tree, isErrMatch: Boolean = false) = + val getSelection = gm.select(nme.get, _.info.isParameterless) + if gm.tpe.widen.isRef(defn.MagicMaybeClass) then + if isErrMatch then + val MagicMaybeType(_, errArg, nullable) = gm.tpe.widen.runtimeChecked + val select = gm.asInstance(defn.MagicFailClass.typeRef.appliedTo(defn.AnyType)) + .select(nme.elem) + if nullable + then If( + gm.nullTest(cond = true), + unitLiteral.asInstance(errArg), + select) + else select + else + val validTpe = defn.MagicValidClass.typeRef + If(gm.isInstance(validTpe), + gm.asInstance(validTpe).select(nme.elem), + gm + ).asInstance(getSelection.tpe.widen) + else getSelection + /** Plan for matching the result of an unapply against argument patterns `args` */ def unapplyPlan(unapp: Tree, args: List[Tree]): Plan = { def caseClass = unapp.symbol.owner.linkedClass lazy val caseAccessors = caseClass.caseAccessors val unappType = unapp.tpe.widen.stripNamedTuple.simplified + //println(i"unapp $tree, $unapp, $unappType") def isSyntheticScala2Unapply(sym: Symbol) = sym.is(Synthetic) && sym.owner.is(Scala2x) - def tupleApp(i: Int, receiver: Tree) = // manually inlining the call to NonEmptyTuple#apply, because it's an inline method - ref(defn.RuntimeTuplesModule) - .select(defn.RuntimeTuples_apply) - .appliedTo( - receiver.ensureConforms(defn.NonEmptyTupleTypeRef), // If scrutinee is a named tuple, cast to underlying tuple - Literal(Constant(i))) - - def getOfGetMatch(gm: Tree) = - val getSelection = gm.select(nme.get, _.info.isParameterless) - if gm.tpe.widen.isRef(defn.MagicMaybeClass) then - val validTpe = defn.MagicValidClass.typeRef - If(gm.isInstance(validTpe), - gm.asInstance(validTpe).select(nme.elem), - gm) - .asInstance(getSelection.tpe.widen) - else getSelection - - // Disable Scala2Unapply optimization if the argument is a named argument for a single-element named tuple to - // enable selecting the field. See i23131.scala for test cases. val wasUnaryNamedTupleSelectArgForNamedTuple = args.length == 1 && args.head.removeAttachment(FirstTransform.WasNamedArg).isDefined && isGetMatch(unappType) && getOfGetMatch(unapp).tpe.widenDealias.isNamedTupleType - if (isSyntheticScala2Unapply(unapp.symbol) && caseAccessors.length == args.length && !wasUnaryNamedTupleSelectArgForNamedTuple) + if isSyntheticScala2Unapply(unapp.symbol) + && caseAccessors.length == args.length + && !wasUnaryNamedTupleSelectArgForNamedTuple + // Disable Scala2Unapply optimization if the argument is a named argument for a single-element named tuple to + // enable selecting the field. See i23131.scala for test cases. + then def tupleSel(sym: Symbol) = // If scrutinee is a named tuple, cast to underlying tuple, so that we can // continue to select with _1, _2, ... @@ -419,64 +435,75 @@ object PatternMatcher { else if unappType.derivesFrom(defn.BooleanClass) then TestPlan(GuardTest, unapp, unapp.span, onSuccess) else - val unappCore = unapp match - case Apply(fn, arg :: Nil) if fn.symbol == defn.Magic_OkUnapply => arg - case _ => unapp - letAbstract(unappCore) { unappResult => - val isUnapplySeq = unapp.symbol.name == nme.unapplySeq - if isProductMatch(unappType, args.length) && !isUnapplySeq then - val selectors = productSelectors(unappType).take(args.length) - .map(ref(unappResult).select(_)) - matchArgsPlan(selectors, args, onSuccess) - else if isUnapplySeq && unapplySeqTypeElemTp(unappType.finalResultType).exists then - unapplySeqPlan(unappResult, args) - else if isUnapplySeq && isProductSeqMatch(unappType, args.length, unapp.srcPos) then - val selectors = productSelectors(unappType).map(ref(unappResult).select(_)) - unapplyProductSeqPlan(selectors, args) - else if unappResult.info <:< defn.NonEmptyTupleTypeRef then - val components = - (0 until unappResult.denot.info.tupleElementTypes.getOrElse(Nil).length) - .toList.map(tupleApp(_, ref(unappResult))) - matchArgsPlan(components, args, onSuccess) - else { - assert(isGetMatch(unappType)) - val argsPlan = { - val get = getOfGetMatch(ref(unappResult)) - if (isUnapplySeq) - letAbstract(get) { getResult => - if unapplySeqTypeElemTp(get.tpe).exists then - unapplySeqPlan(getResult, args) - else if isGenericTuple(getResult.info) then - val elemTypes = getResult.info.tupleElementTypes.getOrElse(Nil) - val selectors = elemTypes.zipWithIndex.map { (tp, i) => - val tree = tupleApp(i, ref(getResult)) - if i == elemTypes.length - 1 then tree.cast(tp) else tree - } - unapplyProductSeqPlan(selectors, args) - else { - val selectors = productSelectors(getResult.info).map(ref(getResult).select(_)) - unapplyProductSeqPlan(selectors, args) - } - } - else - letAbstract(get) { getResult => - // Special case: Normally, we pull out the argument wholesale if - // there is only one. But if the argument is a named argument for - // a single-element named tuple, we have to select the field instead. - // NamedArg trees are eliminated in FirstTransform but for named arguments - // of patterns we add a WasNamedArg attachment, which is used to guide the - // logic here. See i22900.scala for test cases. - val selectors = args match - case arg :: Nil if !wasUnaryNamedTupleSelectArgForNamedTuple => - ref(getResult) :: Nil - case _ => - productSelectors(getResult.info).map(ref(getResult).select(_)) - matchArgsPlan(selectors, args, onSuccess) + unapp match + case Apply(fn, arg :: Nil) if fn.symbol == defn.Magic_OkUnapply => + unappResultPlan(unapp, args, arg.symbol, unappType, wasUnaryNamedTupleSelectArgForNamedTuple) + case Apply(fn, arg :: Nil) if fn.symbol == defn.Magic_ErrUnapply => + unappResultPlan(unapp, args, arg.symbol, unappType, wasUnaryNamedTupleSelectArgForNamedTuple, isErrMatch = true) + case _ => + letAbstract(unapp): unappResult => + unappResultPlan(unapp, args, unappResult, unappType, wasUnaryNamedTupleSelectArgForNamedTuple) + } + + def unappResultPlan( + unapp: Tree, args: List[Tree], unappResult: Symbol, unappType: Type, + wasUnaryNamedTupleSelectArgForNamedTuple: Boolean, + isErrMatch: Boolean = false): 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), isErrMatch) + 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( + if isErrMatch then IsFailTest else NonEmptyTest, + unappResult, unapp.span, argsPlan) + } } // begin patternPlan @@ -501,7 +528,7 @@ object PatternMatcher { case UnApply(extractor, implicits, args) => val mt @ MethodType(_) = extractor.tpe.widen.runtimeChecked val admitsNull = mt.paramInfos.headOption match - case Some(MagicMaybeType(nullable)) => nullable + case Some(MagicMaybeType(_, _, nullable)) => nullable case _ => false val unappPlan = if (scrutinee.info.isBottomType) // Generate a throwaway but type-correct plan. @@ -831,7 +858,7 @@ object PatternMatcher { case NonEmptyTest => scrutinee.tpe.widenDealias match case AppliedType(tycon, _ :: errArg :: Nil) if tycon.isRef(defn.MagicMaybeClass) => - val test = nullLiteral.select(defn.Any_!=).appliedTo(scrutinee) + val test = scrutinee.nullTest(cond = false) if errArg.isRef(defn.UnitClass) then test else test.and(scrutinee.isInstance(defn.MagicFailClass.typeRef).not) @@ -840,6 +867,11 @@ object PatternMatcher { scrutinee .select(nme.isEmpty, _.info.isParameterless) .select(nme.UNARY_!, _.info.isParameterless)) + case IsFailTest => + val MagicMaybeType(_, _, nullable) = scrutinee.tpe.widen.runtimeChecked + val typeTest = scrutinee.isInstance(defn.MagicFailClass.typeRef) + if nullable then scrutinee.nullTest(cond = true).or(typeTest) + else typeTest case NonNullTest => scrutinee.testNotNull case GuardTest => diff --git a/library/src-bootstrapped/scala/magic/Err.scala b/library/src-bootstrapped/scala/magic/Err.scala index 72191ed7c97e..7324ddf7f900 100644 --- a/library/src-bootstrapped/scala/magic/Err.scala +++ b/library/src-bootstrapped/scala/magic/Err.scala @@ -7,8 +7,8 @@ import annotation.experimental @experimental object Err: inline def apply[E](e: E): Maybe[Nothing, E] = - if e == () then null - else new runtime.Fail(e).asInstanceOf[Maybe[Nothing, E]] + (if e == () then null else new runtime.Fail(e)) + .asInstanceOf[Maybe[Nothing, E]] def unapply[E](x: Maybe[Any, E]): E? = x match case null => Ok(().asInstanceOf[E]) diff --git a/library/src/scala/magic/runtime/Fail.scala b/library/src/scala/magic/runtime/Fail.scala index a4eed1ba2cd1..32238d3d6deb 100644 --- a/library/src/scala/magic/runtime/Fail.scala +++ b/library/src/scala/magic/runtime/Fail.scala @@ -3,4 +3,5 @@ package scala.magic.runtime import annotation.experimental @experimental -class Fail[+E](val elem: E) +class Fail[+E](val elem: E): + override def toString = s"Fail($elem)" diff --git a/tests/run-bootstrapped/orelse.check b/tests/run-bootstrapped/orelse.check new file mode 100644 index 000000000000..0bf2c3133cd0 --- /dev/null +++ b/tests/run-bootstrapped/orelse.check @@ -0,0 +1,22 @@ +Right(s) +Left(()) +Right(s) +Left(bad) +Right(1) +Right(22) +Left(bad) +Right(ss) +Left(BAD) +pos 1 1 +neg -1 +nonempty abc bc +empty +poly abc = bc +poly 22 = 22 +nopoly +nopoly -1 +poly2 abc = bc +poly2 22 = 22 +nopoly2 +nopoly2 -1 +nopoly2 true diff --git a/tests/run-bootstrapped/orelse.scala b/tests/run-bootstrapped/orelse.scala index 1816076b85c3..55743dff2fd4 100644 --- a/tests/run-bootstrapped/orelse.scala +++ b/tests/run-bootstrapped/orelse.scala @@ -30,10 +30,10 @@ object WithTail: if s.isEmpty then null else s.substring(1) object Poly: - def unapply[T](x: T): T? = x match + def unapply[T](x: T): T ? String = x match case Pos(y) => y case WithTail(s) => s - case _ => null + case _ => Err("no match") def f[T, E](x: T, e: E) = val x1 = toEither(Ok("s")) @@ -57,8 +57,6 @@ def f[T, E](x: T, e: E) = println(z1) println(z2) - - def posTest(x: Int) = x match case Pos(y) => println(s"pos $x $y") case _ => println(s"neg $x") From 84799870c1c17acf3261dae199062bdd422f0cae Mon Sep 17 00:00:00 2001 From: odersky Date: Wed, 19 Aug 2026 12:48:51 +0200 Subject: [PATCH 13/28] Adapt Space logic Also, move back bootstrapped library and tests to regular. Since we now inline Ok.unapply, we are no longer affected by difference in erasure between bootstrapped and non-bootstrapped. --- .../src/dotty/tools/dotc/core/Types.scala | 4 ++ .../tools/dotc/transform/patmat/Space.scala | 62 +++++++++++++++---- .../dotty/tools/dotc/CompilationTests.scala | 2 +- .../scala/magic/Err.scala | 7 +-- .../scala/magic/Maybe.scala | 0 .../scala/magic/Ok.scala | 0 .../orelse-subtyping.check | 2 +- .../orelse-subtyping.scala | 0 tests/pos/maybe-conversions.scala | 30 +++++++++ tests/{run-bootstrapped => run}/maybe.scala | 0 tests/{run-bootstrapped => run}/orelse.scala | 0 tests/warn-bootstrapped/maybe-typetest.scala | 18 ------ tests/warn/maybe-conversions.check | 32 ++++++++++ tests/warn/maybe-conversions.scala | 16 +++++ .../maybe-typetest.check | 18 ++---- tests/warn/maybe-typetest.scala | 18 ++++++ 16 files changed, 159 insertions(+), 50 deletions(-) rename library/{src-bootstrapped => src}/scala/magic/Err.scala (54%) rename library/{src-bootstrapped => src}/scala/magic/Maybe.scala (100%) rename library/{src-bootstrapped => src}/scala/magic/Ok.scala (100%) rename tests/{neg-bootstrapped => neg}/orelse-subtyping.check (84%) rename tests/{neg-bootstrapped => neg}/orelse-subtyping.scala (100%) create mode 100644 tests/pos/maybe-conversions.scala rename tests/{run-bootstrapped => run}/maybe.scala (100%) rename tests/{run-bootstrapped => run}/orelse.scala (100%) delete mode 100644 tests/warn-bootstrapped/maybe-typetest.scala create mode 100644 tests/warn/maybe-conversions.check create mode 100644 tests/warn/maybe-conversions.scala rename tests/{warn-bootstrapped => warn}/maybe-typetest.check (59%) create mode 100644 tests/warn/maybe-typetest.scala diff --git a/compiler/src/dotty/tools/dotc/core/Types.scala b/compiler/src/dotty/tools/dotc/core/Types.scala index 9b420cf57cf4..d7c9443ad10e 100644 --- a/compiler/src/dotty/tools/dotc/core/Types.scala +++ b/compiler/src/dotty/tools/dotc/core/Types.scala @@ -5791,6 +5791,10 @@ object Types extends TypeUtils { } object MagicMaybeType { + /** The maybe type `resTp ? errTp` */ + def apply(resTp: Type, errTp: Type)(using Context) = + defn.MagicMaybeClass.typeRef.appliedTo(resTp, errTp) + /** Matches types T ? E, returns (T, E, E >: Unit) */ def unapply(tp: Type)(using Context): Option[(Type, Type, Boolean)] = tp.dealias match case AppliedType(tycon, resArg :: errArg :: Nil) if tycon.isRef(defn.MagicMaybeClass) => diff --git a/compiler/src/dotty/tools/dotc/transform/patmat/Space.scala b/compiler/src/dotty/tools/dotc/transform/patmat/Space.scala index a4337e86b3e7..574f11c30ca3 100644 --- a/compiler/src/dotty/tools/dotc/transform/patmat/Space.scala +++ b/compiler/src/dotty/tools/dotc/transform/patmat/Space.scala @@ -411,7 +411,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 MagicMaybeType(_, _, /*nullable=*/true) + if fun.symbol == defn.Magic_ErrUnapply && pats.forall(isWildcardArg) => + Or(prod :: nullSpace :: Nil) + case _ => + prod case Typed(pat @ UnApply(_, _, _), _) => project(pat) @@ -435,14 +445,20 @@ object SpaceEngine { private def project(tp: Type)(using Context): Space = tp match { case OrType(tp1, tp2) => Or(project(tp1) :: project(tp2) :: Nil) - case MagicMaybeType(/*nullable=*/true) => - // A maybe type `T?` is inhabited by the values of `T` - // together with `null`, which represents the invalid case. Since `Typ(tp)` stands - // for the valid values only, `null` has to be added as a separate space. - Or(Typ(tp, decomposed = true) :: nullSpace :: Nil) case tp => Typ(tp, decomposed = true) } + /** The two spaces a maybe type `T ? E` decomposes into: the `Ok` values, which + * are represented by `T ? Nothing`, and the `Err` values. The latter are `null` + * if `E` is a supertype of `Unit` (the invalid value is then `null` itself), and + * are represented by `Nothing ? E` otherwise. + */ + private def maybeParts(resTp: Type, errTp: Type, nullable: Boolean)(using Context): List[Type] = + val errPart = + if nullable then ConstantType(Constant(null)) + else MagicMaybeType(defn.NothingType, errTp) + MagicMaybeType(resTp, defn.NothingType) :: errPart :: Nil + private def unapplySeqInfo(resTp: Type, pos: SrcPos)(using Context): (Int, Type, Type) = { var resultTp = resTp var elemTp = unapplySeqTypeElemTp(resultTp) @@ -566,12 +582,23 @@ object SpaceEngine { && tp1 =:= tp2 } + /** Return term parameter types of the extractor `unapp`. */ + def signature(unapp: TermRef, scrutineeTp: Type, argLen: Int)(using Context): List[Type] = + // `Ok.unapply` extracts the result component of a maybe type, `Err.unapply` its error + // component. We cannot infer these from the extractor's signature, since the type of + // the component that is not extracted cannot be constrained from the scrutinee type, + // which would make the inferred component type `Any`. + scrutineeTp.dealias match + case MagicMaybeType(resTp, _, _) if unapp.symbol == defn.Magic_OkUnapply => resTp :: Nil + case MagicMaybeType(_, errTp, _) if unapp.symbol == defn.Magic_ErrUnapply => errTp :: Nil + case _ => extractorSignature(unapp, scrutineeTp, argLen) + /** Return term parameter types of the extractor `unapp`. * Parameter types of the case class type `tp`. Adapted from `unapplyPlan` in patternMatcher */ - def signature(unapp: TermRef, scrutineeTp: Type, argLen: Int)(using Context): List[Type] = trace(i"signature($unapp, $scrutineeTp, $argLen)") { + private def extractorSignature(unapp: TermRef, scrutineeTp: Type, argLen: Int)(using Context): List[Type] = trace(i"signature($unapp, $scrutineeTp, $argLen)") { val unappSym = unapp.symbol - val mt: MethodType = unapp.widen match { + val mt: MethodType = unapp.widenDealias match { case mt: MethodType => mt case pt: PolyType => scrutineeTp match @@ -664,10 +691,14 @@ object SpaceEngine { val AppliedType(_, tp :: Nil) = unapp.prefix.widen.dealias: @unchecked scrutineeTp <:< tp } - // `Ok.unapply` returns its argument, and the emitted non-empty test for a maybe - // type is a `!= null` test (see PatternMatcher#emitCondition). So `Ok(_)` matches - // every non-null value of a maybe type, which is what `Typ(scrutineeTp)` stands for. - || unapp.symbol == defn.Magic_OkUnapply + || scrutineeTp.match + // `Ok(_)` covers a maybe type without error values, `Err(_)` covers a maybe type + // without result values. These are the two spaces a maybe type decomposes into, + // see `maybeParts`. + case MagicMaybeType(resTp, errTp, _) => + unapp.symbol == defn.Magic_OkUnapply && errTp.isNothingType + || unapp.symbol == defn.Magic_ErrUnapply && resTp.isNothingType + case _ => false } /** Decompose a type into subspaces -- assume the type can be decomposed */ @@ -686,6 +717,8 @@ object SpaceEngine { case tp if !TypeComparer.provablyDisjoint(tp, tpB) => AndType(tp, tpB) case OrType(tp1, tp2) => List(tp1, tp2) + case MagicMaybeType(resTp, errTp, nullable) + if !resTp.isNothingType && !errTp.isNothingType => maybeParts(resTp, errTp, nullable) case tp if tp.isRef(defn.BooleanClass) => List(ConstantType(Constant(true)), ConstantType(Constant(false))) case tp if tp.isRef(defn.UnitClass) => ConstantType(Constant(())) :: Nil case tp @ NamedType(Parts(parts), _) => if parts.exists(_ eq tp) then ListOfNoType else parts.map(tp.derivedSelect) @@ -880,6 +913,9 @@ object SpaceEngine { case Typ(tp: TermRef, _) => if (flattenList && tp <:< defn.NilType) "" else tp.symbol.showName + case Typ(MagicMaybeType(resTp, errTp, _), _) if resTp.isNothingType ^ errTp.isNothingType => + // the spaces a maybe type decomposes into, see `maybeParts` + if resTp.isNothingType then "Err(_)" else "Ok(_)" case Typ(tp, decomposed) => val cls = tp.classSymbol if ctx.definitions.isTupleNType(tp.stripNamedTuple) then @@ -1110,7 +1146,7 @@ object SpaceEngine { def checkReachability(m: Match)(using Context): Unit = trace(i"checkReachability($m)"): val selTyp = toUnderlying(m.selector.tpe).dealias val isNullable = selTyp match - case MagicMaybeType(nullable) => nullable + case MagicMaybeType(_, _, nullable) => nullable case _: FlexibleType => true case _ => selTyp.classSymbol.isNullableClass val targetSpace = trace(i"targetSpace($selTyp)"): diff --git a/compiler/test/dotty/tools/dotc/CompilationTests.scala b/compiler/test/dotty/tools/dotc/CompilationTests.scala index 5c7f1a4eb5e3..df46be8c6bc5 100644 --- a/compiler/test/dotty/tools/dotc/CompilationTests.scala +++ b/compiler/test/dotty/tools/dotc/CompilationTests.scala @@ -140,7 +140,7 @@ class CompilationTests { implicit val testGroup: TestGroup = TestGroup("compileWarn") val compilationTest = withCoverage(aggregateTests( compileFilesInDir("tests/warn", defaultOptions), - )) + )).checkWarnings() runWithCoverageOrFallback[WarnTestWithCoverage](compilationTest) } diff --git a/library/src-bootstrapped/scala/magic/Err.scala b/library/src/scala/magic/Err.scala similarity index 54% rename from library/src-bootstrapped/scala/magic/Err.scala rename to library/src/scala/magic/Err.scala index 7324ddf7f900..8dc164204a8e 100644 --- a/library/src-bootstrapped/scala/magic/Err.scala +++ b/library/src/scala/magic/Err.scala @@ -6,12 +6,9 @@ import annotation.experimental @experimental object Err: - inline def apply[E](e: E): Maybe[Nothing, E] = + 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]): E? = x match - case null => Ok(().asInstanceOf[E]) - case x: runtime.Fail[E] => Ok(x.elem) - case _ => null + def unapply[E](x: Maybe[Any, E]): Maybe[E, Nothing] = ??? diff --git a/library/src-bootstrapped/scala/magic/Maybe.scala b/library/src/scala/magic/Maybe.scala similarity index 100% rename from library/src-bootstrapped/scala/magic/Maybe.scala rename to library/src/scala/magic/Maybe.scala diff --git a/library/src-bootstrapped/scala/magic/Ok.scala b/library/src/scala/magic/Ok.scala similarity index 100% rename from library/src-bootstrapped/scala/magic/Ok.scala rename to library/src/scala/magic/Ok.scala diff --git a/tests/neg-bootstrapped/orelse-subtyping.check b/tests/neg/orelse-subtyping.check similarity index 84% rename from tests/neg-bootstrapped/orelse-subtyping.check rename to tests/neg/orelse-subtyping.check index f5692b011035..22ec1b09aadd 100644 --- a/tests/neg-bootstrapped/orelse-subtyping.check +++ b/tests/neg/orelse-subtyping.check @@ -1,4 +1,4 @@ --- [E007] Type Mismatch Error: tests/neg-bootstrapped/orelse-subtyping.scala:6:25 -------------------------------------- +-- [E007] Type Mismatch Error: tests/neg/orelse-subtyping.scala:6:25 --------------------------------------------------- 6 |val x: String ? String = null // error | ^^^^ | Found: Null diff --git a/tests/neg-bootstrapped/orelse-subtyping.scala b/tests/neg/orelse-subtyping.scala similarity index 100% rename from tests/neg-bootstrapped/orelse-subtyping.scala rename to tests/neg/orelse-subtyping.scala diff --git a/tests/pos/maybe-conversions.scala b/tests/pos/maybe-conversions.scala new file mode 100644 index 000000000000..63851fe99173 --- /dev/null +++ b/tests/pos/maybe-conversions.scala @@ -0,0 +1,30 @@ +//> using options -Yexplicit-nulls +import language.experimental.magic +import scala.magic.* +import scala.util.Either + +def toOptionAny[T](x: Any): Option[Any] = x match + case Ok(y) => Some(y) + case null => None + +def toOption[T](x: T?): Option[T] = x match + case Ok(y) => Some(y) + case null => None + +def toOptionStr(x: String?): Option[String] = x match + case Ok(y) => Some(y) + case null => None + +def toEither[T, E](x: T ? E): Either[E, T] = x match + case Ok(y) => Right(y) + case Err(e) => Left(e) + +def toEitherIntStr[T, E](x: Int ? String): Either[String, Int] = x match + case Ok(y) => Right(y) + case Err(e) => Left(e) + +def toEitherIntStrBAD[T, E](x: Int ? String): Either[String, Int] = x match + case Ok(y) => Right(y) // warn + +def toEitherIntStrBAD2[T, E](x: Int ? String): Either[String, Int] = x match + case Err(y) => Left(y) // warn diff --git a/tests/run-bootstrapped/maybe.scala b/tests/run/maybe.scala similarity index 100% rename from tests/run-bootstrapped/maybe.scala rename to tests/run/maybe.scala diff --git a/tests/run-bootstrapped/orelse.scala b/tests/run/orelse.scala similarity index 100% rename from tests/run-bootstrapped/orelse.scala rename to tests/run/orelse.scala diff --git a/tests/warn-bootstrapped/maybe-typetest.scala b/tests/warn-bootstrapped/maybe-typetest.scala deleted file mode 100644 index 9687560a2d84..000000000000 --- a/tests/warn-bootstrapped/maybe-typetest.scala +++ /dev/null @@ -1,18 +0,0 @@ -//> using options -Yexplicit-nulls -Werror -import language.experimental.magic -import scala.magic.* - -def Test[T](x: T) = - x match - case y: String? => println(y) // error - case _ => - Ok(x) match - case y: String? => println(y) // error - case _ => // error - x match - case y: Option[String] => println(y)// error - case _ => - Some(x) match - case y: Option[String] => println(y)// error - case _ => // error - diff --git a/tests/warn/maybe-conversions.check b/tests/warn/maybe-conversions.check new file mode 100644 index 000000000000..f80aabe96302 --- /dev/null +++ b/tests/warn/maybe-conversions.check @@ -0,0 +1,32 @@ +-- [E029] Pattern Match Exhaustivity Warning: tests/warn/maybe-conversions.scala:6:47 ---------------------------------- +6 |def toOptionMissingNull[T](x: T?): Option[T] = x match // warn + | ^ + | match may not be exhaustive. + | + | It would fail on pattern case: null + | + | longer explanation available when compiling with `-explain` +-- [E029] Pattern Match Exhaustivity Warning: tests/warn/maybe-conversions.scala:9:45 ---------------------------------- +9 |def toOptionMissingOk[T](x: T?): Option[T] = x match // warn + | ^ + | match may not be exhaustive. + | + | It would fail on pattern case: Ok(_) + | + | longer explanation available when compiling with `-explain` +-- [E029] Pattern Match Exhaustivity Warning: tests/warn/maybe-conversions.scala:12:63 --------------------------------- +12 |def toEitherMissingErr(x: Int ? String): Either[String, Int] = x match // warn + | ^ + | match may not be exhaustive. + | + | It would fail on pattern case: Err(_) + | + | longer explanation available when compiling with `-explain` +-- [E029] Pattern Match Exhaustivity Warning: tests/warn/maybe-conversions.scala:15:62 --------------------------------- +15 |def toEitherMissingOk(x: Int ? String): Either[String, Int] = x match // warn + | ^ + | match may not be exhaustive. + | + | It would fail on pattern case: Ok(_) + | + | longer explanation available when compiling with `-explain` diff --git a/tests/warn/maybe-conversions.scala b/tests/warn/maybe-conversions.scala new file mode 100644 index 000000000000..85f35452f467 --- /dev/null +++ b/tests/warn/maybe-conversions.scala @@ -0,0 +1,16 @@ +//> using options -Yexplicit-nulls +import language.experimental.magic +import scala.magic.* +import scala.util.Either + +def toOptionMissingNull[T](x: T?): Option[T] = x match // warn + case Ok(y) => Some(y) + +def toOptionMissingOk[T](x: T?): Option[T] = x match // warn + case null => None + +def toEitherMissingErr(x: Int ? String): Either[String, Int] = x match // warn + case Ok(y) => Right(y) + +def toEitherMissingOk(x: Int ? String): Either[String, Int] = x match // warn + case Err(e) => Left(e) diff --git a/tests/warn-bootstrapped/maybe-typetest.check b/tests/warn/maybe-typetest.check similarity index 59% rename from tests/warn-bootstrapped/maybe-typetest.check rename to tests/warn/maybe-typetest.check index 0c327e511970..0855780529a9 100644 --- a/tests/warn-bootstrapped/maybe-typetest.check +++ b/tests/warn/maybe-typetest.check @@ -1,31 +1,25 @@ -- [E030] Match case Unreachable Warning: tests/warn/maybe-typetest.scala:11:9 ----------------------------------------- -11 | case _ => // warn +11 | case _ => // warn unreachable | ^ | Unreachable case -- [E030] Match case Unreachable Warning: tests/warn/maybe-typetest.scala:17:9 ----------------------------------------- -17 | case _ => // warn +17 | case _ => // warn unreachable | ^ | Unreachable case -- [E092] Pattern Match Unchecked Warning: tests/warn/maybe-typetest.scala:7:9 ----------------------------------------- -7 | case y: String? => println(y) // warn +7 | case y: String? => println(y) // warn typetest | ^ - |the type test for scala.magic.$Maybe[String] cannot be checked at runtime because its type arguments can't be determined from T + | the type test for String? cannot be checked at runtime because its type arguments can't be determined from T | | longer explanation available when compiling with `-explain` --- [E092] Pattern Match Unchecked Warning: tests/warn/maybe-typetest.scala:10:9 ---------------------------------------- -10 | case y: String? => println(y) // warn - | ^ - |the type test for scala.magic.$Maybe[String] cannot be checked at runtime because its type arguments can't be determined from scala.magic.$Maybe[T] - | - | longer explanation available when compiling with `-explain` -- [E092] Pattern Match Unchecked Warning: tests/warn/maybe-typetest.scala:13:9 ---------------------------------------- -13 | case y: Option[String] => println(y)// warn +13 | 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 T | | longer explanation available when compiling with `-explain` -- [E092] Pattern Match Unchecked Warning: tests/warn/maybe-typetest.scala:16:9 ---------------------------------------- -16 | case y: Option[String] => println(y)// warn +16 | case y: Option[String] => println(y)// warn typetest | ^ |the type test for Option[String] cannot be checked at runtime because its type arguments can't be determined from Some[T] | diff --git a/tests/warn/maybe-typetest.scala b/tests/warn/maybe-typetest.scala new file mode 100644 index 000000000000..88e4b5087b37 --- /dev/null +++ b/tests/warn/maybe-typetest.scala @@ -0,0 +1,18 @@ +//> using options -Yexplicit-nulls +import language.experimental.magic +import scala.magic.* + +def Test[T](x: T) = + x match + case y: String? => println(y) // warn typetest + case _ => + Ok(x) match + case y: String? => println(y) // TODO why not warn typetest? + case _ => // warn unreachable + x match + case y: Option[String] => println(y)// warn typetest + case _ => + Some(x) match + case y: Option[String] => println(y)// warn typetest + case _ => // warn unreachable + From c66c66fbd4842ffe25a0509644ed761dd2cda8fb Mon Sep 17 00:00:00 2001 From: odersky Date: Wed, 19 Aug 2026 18:37:09 +0200 Subject: [PATCH 14/28] Fix maybe widening in TypeComparer Plus a lot more tests --- .../dotty/tools/dotc/core/TypeComparer.scala | 8 +- ...it-global-scala2-library-tasty.excludelist | 1 + .../test/dotc/pos-test-pickling.excludelist | 1 + .../BootstrappedOnlyCompilationTests.scala | 14 - .../dotty/tools/dotc/CompilationTests.scala | 2 +- tests/explicit-nulls/pos/opt-maybe.scala | 11 + .../pos/match-complete-maybe.scala | 114 ++++ .../pos/unapply-implicit-arg-pos-maybe.scala | 16 + .../unapplySeq-implicit-arg-pos-maybe.scala | 16 + .../warn/unapply-implicit-arg-maybe.check | 8 + .../warn/unapply-implicit-arg-maybe.scala | 16 + .../warn/unapply-implicit-arg2-maybe.check | 12 + .../warn/unapply-implicit-arg2-maybe.scala | 16 + .../warn/unapply-implicit-arg3-maybe.check | 14 + .../warn/unapply-implicit-arg3-maybe.scala | 16 + tests/neg/bad-unapplies-maybe.check | 39 ++ tests/neg/bad-unapplies-maybe.scala | 30 ++ tests/neg/experimentalUnapply-maybe.scala | 22 + tests/neg/i1793-maybe.scala | 9 + tests/neg/i21841-maybe.check | 6 + tests/neg/i21841-maybe.scala | 24 + tests/neg/i23156-maybe.scala | 8 + tests/neg/i2378-maybe.scala | 32 ++ tests/neg/i24168-maybe.scala | 14 + tests/neg/i3989c-maybe.scala | 18 + tests/neg/i8530-b-maybe.check | 7 + tests/neg/i8530-b-maybe.scala | 11 + tests/neg/i8894-maybe.scala | 15 + tests/neg/infix-maybe.check | 30 ++ tests/neg/infix-maybe.scala | 71 +++ tests/neg/orelse-subtyping.check | 6 - tests/neg/patmat2-maybe.scala | 36 ++ ...table-pattern-binding-messages-maybe.check | 48 ++ ...table-pattern-binding-messages-maybe.scala | 18 + ...e-pattern-binding-messages-old-maybe.check | 48 ++ ...e-pattern-binding-messages-old-maybe.scala | 18 + tests/neg/t7868-maybe.scala | 15 + tests/neg/tryPatternMatchEq-maybe.scala | 36 ++ tests/neg/unchecked-patterns-maybe.scala | 29 ++ tests/patmat/3543-maybe.scala | 54 ++ tests/patmat/dotty-maybe.scala | 32 ++ tests/patmat/i2363-maybe.check | 16 + tests/patmat/i2363-maybe.scala | 27 + tests/patmat/i2502-maybe.check | 8 + tests/patmat/i2502-maybe.scala | 19 + tests/patmat/i2502b-maybe.check | 8 + tests/patmat/i2502b-maybe.scala | 19 + tests/patmat/optionless-maybe.check | 8 + tests/patmat/optionless-maybe.scala | 34 ++ tests/patmat/patmat-extractor-maybe.check | 12 + tests/patmat/patmat-extractor-maybe.scala | 19 + tests/patmat/t8511-maybe.check | 8 + tests/patmat/t8511-maybe.scala | 27 + .../pos-custom-args/captures/i26586.scala | 18 + .../captures/i24729-maybe.scala | 23 + tests/pos/StringContext-maybe.scala | 490 ++++++++++++++++++ tests/pos/byname-implicits-8-maybe.scala | 34 ++ tests/pos/extractor-types-maybe.scala | 32 ++ tests/pos/first-class-patterns-maybe.scala | 25 + tests/pos/i1318-maybe.scala | 40 ++ tests/pos/i14896-maybe.scala | 4 + tests/pos/i15188-maybe.scala | 11 + tests/pos/i15188b-maybe.scala | 10 + 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/i18601b-maybe.scala | 27 + tests/pos/i20107-maybe.scala | 9 + tests/pos/i2104-maybe.scala | 22 + tests/pos/i2104b-maybe.scala | 18 + tests/pos/i23022-maybe.scala | 14 + tests/pos/i23459-maybe.scala | 24 + tests/pos/i25663-maybe.scala | 7 + tests/pos/i6621-maybe.scala | 11 + tests/pos/i8083-maybe.scala | 28 + tests/pos/i8530-maybe.scala | 29 ++ tests/pos/i8577-maybe.scala | 23 + tests/pos/i8972-maybe.scala | 11 + tests/pos/i8997-maybe.scala | 9 + tests/pos/inline-i1773-maybe.scala | 16 + tests/pos/inline-unapply-maybe.scala | 17 + tests/pos/maybe-conversions.scala | 6 - tests/pos/misc-unapply_pos-maybe.scala | 29 ++ tests/pos/simpleExtractors-1-maybe.scala | 31 ++ ...pattern-bindings-3.0-migration-maybe.scala | 39 ++ .../strict-pattern-bindings-3.1-maybe.scala | 39 ++ tests/pos/t1048-maybe.scala | 16 + tests/pos/t1260-maybe.scala | 20 + tests/pos/t3136-maybe.scala | 21 + tests/pos/t5041-maybe.scala | 11 + tests/pos/t6675-maybe.scala | 23 + tests/pos/t6994-maybe.scala | 10 + tests/pos/t796-maybe.scala | 28 + tests/pos/t8045-maybe.scala | 19 + tests/pos/t8128-maybe.scala | 18 + tests/pos/unapplyComplex-maybe.scala | 41 ++ tests/pos/unapplyVal-maybe.scala | 39 ++ tests/run/LazyLists-maybe.scala | 112 ++++ tests/run/Typeable-maybe.check | 8 + tests/run/Typeable-maybe.scala | 62 +++ .../run/fully-abstract-interface-maybe.check | 29 ++ .../run/fully-abstract-interface-maybe.scala | 331 ++++++++++++ tests/run/fully-abstract-nat-1-maybe.check | 13 + tests/run/fully-abstract-nat-1-maybe.scala | 144 +++++ tests/run/fully-abstract-nat-2-maybe.check | 13 + tests/run/fully-abstract-nat-2-maybe.scala | 155 ++++++ tests/run/fully-abstract-nat-3-maybe.check | 13 + tests/run/fully-abstract-nat-3-maybe.scala | 159 ++++++ tests/run/fully-abstract-nat-maybe.check | 35 ++ tests/run/fully-abstract-nat-maybe.scala | 289 +++++++++++ tests/run/i13968-maybe.scala | 28 + tests/run/i1748-maybe.check | 2 + tests/run/i1748-maybe.scala | 16 + tests/run/i1773-maybe.check | 2 + tests/run/i1773-maybe.scala | 16 + tests/run/i1779-maybe.check | 1 + tests/run/i1779-maybe.scala | 15 + tests/run/i4177-maybe.scala | 20 + tests/run/i8530-b-maybe.scala | 23 + tests/run/i8530-maybe.check | 7 + tests/run/i8530-maybe.scala | 48 ++ tests/run/i8577a-maybe.scala | 15 + tests/run/i8577b-maybe.scala | 15 + tests/run/i8577c-maybe.scala | 15 + tests/run/i8577d-maybe.scala | 15 + tests/run/i8577e-maybe.scala | 19 + tests/run/i8577f-maybe.scala | 15 + tests/run/i8577g-maybe.scala | 15 + tests/run/i8577h-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.scala | 5 +- tests/run/named-patterns-maybe.check | 20 + tests/run/named-patterns-maybe.scala | 75 +++ tests/run/orelse.scala | 6 +- tests/run/patmat-maybe.check | 4 + tests/run/patmat-maybe.scala | 47 ++ tests/run/patmat-option-named-maybe.scala | 23 + tests/run/patmat-spec-maybe.scala | 63 +++ tests/run/patmatch-classtag-maybe.scala | 47 ++ tests/run/reducable-maybe.scala | 64 +++ tests/run/string-extractor-maybe.check | 9 + tests/run/string-extractor-maybe.scala | 65 +++ tests/run/t1048-maybe.check | 2 + tests/run/t1048-maybe.scala | 23 + tests/run/t1220-maybe.scala | 17 + tests/run/t4415-maybe.scala | 88 ++++ tests/run/t6111-maybe.check | 2 + tests/run/t6111-maybe.scala | 31 ++ tests/run/t7214-maybe.scala | 61 +++ tests/run/tryPatternMatch-maybe.check | 20 + tests/run/tryPatternMatch-maybe.scala | 143 +++++ tests/run/tuple-patterns-maybe.check | 9 + tests/run/tuple-patterns-maybe.scala | 42 ++ tests/run/type-test-binding-maybe.check | 2 + tests/run/type-test-binding-maybe.scala | 36 ++ 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/unchecked-patterns-maybe.scala | 13 + tests/run/virtpatmat_stringinterp-maybe.check | 1 + tests/run/virtpatmat_stringinterp-maybe.scala | 18 + 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-typetest.check | 6 + tests/warn/maybe-typetest.scala | 2 +- .../strict-pattern-bindings-3.2-maybe.scala | 39 ++ 174 files changed, 5677 insertions(+), 34 deletions(-) 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/pos/unapply-implicit-arg-pos-maybe.scala create mode 100644 tests/init-global/pos/unapplySeq-implicit-arg-pos-maybe.scala create mode 100644 tests/init-global/warn/unapply-implicit-arg-maybe.check create mode 100644 tests/init-global/warn/unapply-implicit-arg-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/init-global/warn/unapply-implicit-arg3-maybe.check create mode 100644 tests/init-global/warn/unapply-implicit-arg3-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/experimentalUnapply-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/i23156-maybe.scala create mode 100644 tests/neg/i2378-maybe.scala create mode 100644 tests/neg/i24168-maybe.scala create mode 100644 tests/neg/i3989c-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/infix-maybe.check create mode 100644 tests/neg/infix-maybe.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/refutable-pattern-binding-messages-old-maybe.check create mode 100644 tests/neg/refutable-pattern-binding-messages-old-maybe.scala create mode 100644 tests/neg/t7868-maybe.scala create mode 100644 tests/neg/tryPatternMatchEq-maybe.scala create mode 100644 tests/neg/unchecked-patterns-maybe.scala create mode 100644 tests/patmat/3543-maybe.scala create mode 100644 tests/patmat/dotty-maybe.scala create mode 100644 tests/patmat/i2363-maybe.check create mode 100644 tests/patmat/i2363-maybe.scala create mode 100644 tests/patmat/i2502-maybe.check create mode 100644 tests/patmat/i2502-maybe.scala create mode 100644 tests/patmat/i2502b-maybe.check create mode 100644 tests/patmat/i2502b-maybe.scala create mode 100644 tests/patmat/optionless-maybe.check create mode 100644 tests/patmat/optionless-maybe.scala create mode 100644 tests/patmat/patmat-extractor-maybe.check create mode 100644 tests/patmat/patmat-extractor-maybe.scala create mode 100644 tests/patmat/t8511-maybe.check create mode 100644 tests/patmat/t8511-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/byname-implicits-8-maybe.scala create mode 100644 tests/pos/extractor-types-maybe.scala create mode 100644 tests/pos/first-class-patterns-maybe.scala create mode 100644 tests/pos/i1318-maybe.scala create mode 100644 tests/pos/i14896-maybe.scala create mode 100644 tests/pos/i15188-maybe.scala create mode 100644 tests/pos/i15188b-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/i18601b-maybe.scala create mode 100644 tests/pos/i20107-maybe.scala create mode 100644 tests/pos/i2104-maybe.scala create mode 100644 tests/pos/i2104b-maybe.scala create mode 100644 tests/pos/i23022-maybe.scala create mode 100644 tests/pos/i23459-maybe.scala create mode 100644 tests/pos/i25663-maybe.scala create mode 100644 tests/pos/i6621-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/i8997-maybe.scala create mode 100644 tests/pos/inline-i1773-maybe.scala create mode 100644 tests/pos/inline-unapply-maybe.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/strict-pattern-bindings-3.0-migration-maybe.scala create mode 100644 tests/pos/strict-pattern-bindings-3.1-maybe.scala create mode 100644 tests/pos/t1048-maybe.scala create mode 100644 tests/pos/t1260-maybe.scala create mode 100644 tests/pos/t3136-maybe.scala create mode 100644 tests/pos/t5041-maybe.scala create mode 100644 tests/pos/t6675-maybe.scala create mode 100644 tests/pos/t6994-maybe.scala create mode 100644 tests/pos/t796-maybe.scala create mode 100644 tests/pos/t8045-maybe.scala create mode 100644 tests/pos/t8128-maybe.scala create mode 100644 tests/pos/unapplyComplex-maybe.scala create mode 100644 tests/pos/unapplyVal-maybe.scala create mode 100644 tests/run/LazyLists-maybe.scala create mode 100644 tests/run/Typeable-maybe.check create mode 100644 tests/run/Typeable-maybe.scala create mode 100644 tests/run/fully-abstract-interface-maybe.check create mode 100644 tests/run/fully-abstract-interface-maybe.scala create mode 100644 tests/run/fully-abstract-nat-1-maybe.check create mode 100644 tests/run/fully-abstract-nat-1-maybe.scala create mode 100644 tests/run/fully-abstract-nat-2-maybe.check create mode 100644 tests/run/fully-abstract-nat-2-maybe.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/fully-abstract-nat-maybe.check create mode 100644 tests/run/fully-abstract-nat-maybe.scala create mode 100644 tests/run/i13968-maybe.scala create mode 100644 tests/run/i1748-maybe.check create mode 100644 tests/run/i1748-maybe.scala create mode 100644 tests/run/i1773-maybe.check create mode 100644 tests/run/i1773-maybe.scala create mode 100644 tests/run/i1779-maybe.check create mode 100644 tests/run/i1779-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/i8577b-maybe.scala create mode 100644 tests/run/i8577c-maybe.scala create mode 100644 tests/run/i8577d-maybe.scala create mode 100644 tests/run/i8577e-maybe.scala create mode 100644 tests/run/i8577f-maybe.scala create mode 100644 tests/run/i8577g-maybe.scala create mode 100644 tests/run/i8577h-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/named-patterns-maybe.check create mode 100644 tests/run/named-patterns-maybe.scala create mode 100644 tests/run/patmat-maybe.check create mode 100644 tests/run/patmat-maybe.scala create mode 100644 tests/run/patmat-option-named-maybe.scala create mode 100644 tests/run/patmat-spec-maybe.scala create mode 100644 tests/run/patmatch-classtag-maybe.scala create mode 100644 tests/run/reducable-maybe.scala create mode 100644 tests/run/string-extractor-maybe.check create mode 100644 tests/run/string-extractor-maybe.scala create mode 100644 tests/run/t1048-maybe.check create mode 100644 tests/run/t1048-maybe.scala create mode 100644 tests/run/t1220-maybe.scala create mode 100644 tests/run/t4415-maybe.scala create mode 100644 tests/run/t6111-maybe.check create mode 100644 tests/run/t6111-maybe.scala create mode 100644 tests/run/t7214-maybe.scala create mode 100644 tests/run/tryPatternMatch-maybe.check create mode 100644 tests/run/tryPatternMatch-maybe.scala create mode 100644 tests/run/tuple-patterns-maybe.check create mode 100644 tests/run/tuple-patterns-maybe.scala create mode 100644 tests/run/type-test-binding-maybe.check create mode 100644 tests/run/type-test-binding-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/unchecked-patterns-maybe.scala create mode 100644 tests/run/virtpatmat_stringinterp-maybe.check create mode 100644 tests/run/virtpatmat_stringinterp-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/strict-pattern-bindings-3.2-maybe.scala diff --git a/compiler/src/dotty/tools/dotc/core/TypeComparer.scala b/compiler/src/dotty/tools/dotc/core/TypeComparer.scala index 25de022a0d4a..297525d7e225 100644 --- a/compiler/src/dotty/tools/dotc/core/TypeComparer.scala +++ b/compiler/src/dotty/tools/dotc/core/TypeComparer.scala @@ -1508,6 +1508,12 @@ class TypeComparer(@constructorOnly initctx: Context) extends ConstraintHandling case _ => false } && recordGadtUsageIf(true) + /** T <: T? if T is not null */ + def byMaybeWidening: Boolean = tp2 match + case MagicMaybeType(res2, err2, _) if tp1.isNotNull => + recur(tp1, res2) && isSubType(err2, defn.UnitType) + case _ => false + tycon2 match { case param2: TypeParamRef => isMatchingApply(tp1) || @@ -1516,9 +1522,9 @@ class TypeComparer(@constructorOnly initctx: Context) extends ConstraintHandling case tycon2: TypeRef => isMatchingApply(tp1) || byGadtBounds + || byMaybeWidening || defn.isCompiletimeAppliedType(tycon2.symbol) && compareCompiletimeAppliedType(tp2, tp1, fromBelow = true) - || tycon2.symbol == defn.MagicMaybeClass && tp1.isNotNull || tycon2.info.match case info2: TypeBounds => compareLower(info2, tyconIsTypeRef = true) diff --git a/compiler/test/dotc/pos-init-global-scala2-library-tasty.excludelist b/compiler/test/dotc/pos-init-global-scala2-library-tasty.excludelist index e95a5b8b8c8f..3351cfa9f818 100644 --- a/compiler/test/dotc/pos-init-global-scala2-library-tasty.excludelist +++ b/compiler/test/dotc/pos-init-global-scala2-library-tasty.excludelist @@ -2,6 +2,7 @@ patmat.scala patmat-interpolator.scala unapplySeq-implicit-arg-pos.scala +unapplySeq-implicit-arg-pos-maybe.scala global-cycle11.scala ## warns! and fails under -Werror, noticed at #25571 diff --git a/compiler/test/dotc/pos-test-pickling.excludelist b/compiler/test/dotc/pos-test-pickling.excludelist index 709e233b54b3..e8429ae78df2 100644 --- a/compiler/test/dotc/pos-test-pickling.excludelist +++ b/compiler/test/dotc/pos-test-pickling.excludelist @@ -29,6 +29,7 @@ i9804.scala i13433.scala i16649-irrefutable.scala strict-pattern-bindings-3.0-migration.scala +strict-pattern-bindings-3.0-migration-maybe.scala i17186b.scala i11982a.scala i17255 diff --git a/compiler/test/dotty/tools/dotc/BootstrappedOnlyCompilationTests.scala b/compiler/test/dotty/tools/dotc/BootstrappedOnlyCompilationTests.scala index 6b21b807eee1..dfd3098a4d3b 100644 --- a/compiler/test/dotty/tools/dotc/BootstrappedOnlyCompilationTests.scala +++ b/compiler/test/dotty/tools/dotc/BootstrappedOnlyCompilationTests.scala @@ -97,20 +97,6 @@ class BootstrappedOnlyCompilationTests { .checkExpectedErrors() } - @Test def negBootstrappedOnly: Unit = { - given TestGroup = TestGroup("warnBootstrappedOnly") - aggregateTests( - compileFilesInDir("tests/neg-bootstrapped", defaultOptions) - ).checkExpectedErrors() - } - - @Test def warnBootstrappedOnly: Unit = { - given TestGroup = TestGroup("warnBootstrappedOnly") - aggregateTests( - compileFilesInDir("tests/warn-bootstrapped", defaultOptions) - ) - } - @Test def negWithCompiler: Unit = { implicit val testGroup: TestGroup = TestGroup("compileNegWithCompiler") aggregateTests( diff --git a/compiler/test/dotty/tools/dotc/CompilationTests.scala b/compiler/test/dotty/tools/dotc/CompilationTests.scala index df46be8c6bc5..5c7f1a4eb5e3 100644 --- a/compiler/test/dotty/tools/dotc/CompilationTests.scala +++ b/compiler/test/dotty/tools/dotc/CompilationTests.scala @@ -140,7 +140,7 @@ class CompilationTests { implicit val testGroup: TestGroup = TestGroup("compileWarn") val compilationTest = withCoverage(aggregateTests( compileFilesInDir("tests/warn", defaultOptions), - )).checkWarnings() + )) runWithCoverageOrFallback[WarnTestWithCoverage](compilationTest) } diff --git a/tests/explicit-nulls/pos/opt-maybe.scala b/tests/explicit-nulls/pos/opt-maybe.scala new file mode 100644 index 000000000000..ff4e6406ecb4 --- /dev/null +++ b/tests/explicit-nulls/pos/opt-maybe.scala @@ -0,0 +1,11 @@ + +import language.experimental.magic +import scala.magic.* +type Opt[+A] = A | Null +object Opt: + + def unapply[A](o: Opt[A]): A? = + if o != null then Ok(o.asInstanceOf[o.type & A]) + else null + +end Opt \ No newline at end of file diff --git a/tests/init-global/pos/match-complete-maybe.scala b/tests/init-global/pos/match-complete-maybe.scala new file mode 100644 index 000000000000..60c7d404058a --- /dev/null +++ b/tests/init-global/pos/match-complete-maybe.scala @@ -0,0 +1,114 @@ +//> using options -Yexplicit-nulls +import language.experimental.magic +object Matcher { + // Chained Match + val chained_match_xs: List[Any] = List(1, 2, 3) + val chained_match_x = chained_match_xs match { + case Nil => "empty" + case _ => "nonempty" + } match { + case "empty" => 0 + case "nonempty" => 1 + } + println(chained_match_x) + + // Vararg Splices + val vararg_arr = Array(0, 1, 2, 3) + val vararg_lst = List(vararg_arr*) // vararg splice argument + // Throws an exception? + val vararg_splice = vararg_lst match + case List(0, 1, xs*) => 1 // binds xs to Seq(2, 3) + case List(1, _*) => 0 // wildcard pattern + case _ => 2 + println(vararg_splice) + println(vararg_lst) + + // Pattern Definitions + val patter_def_xs: List[Any] = List(1, 2, 3) + val (patter_def_x: Any) :: _ = patter_def_xs : @unchecked + println(patter_def_x) + + val patter_def_pair = (1, true) + val (patter_def_a, patter_def_b) = patter_def_pair + println(patter_def_a) + + val elems: List[(Int, Int)] = List((1, 2), (3, 4), (5, 6)) + + for ((x,y) <- elems) do println(x) + + def main(args: Array[String]) = { + // println(chained_match_x) + println(vararg_splice) + // println(patter_def_x) + // println( + } +} + +// Patter Matching Using Extractors + +// Option Extractors +case class Person(name: String, age: Int) +object Person { + def unapply(person: Person): (String, Int)? = (person.name, person.age) +} + +object OptionMatcher { + val person = Person("Alice", 25) + + val result = person match { + case Person(name, age) => s"Name: $name, Age: $age" + case _ => "Not a person" + } + println(result) +} + +// Boolean Extractors +object Adult { + def unapply(person: Person): Boolean = person.age >= 18 +} + +object BooleanMatcher { + val person = Person("Charlie", 17) + + val adultResult = person match { + case Adult() => s"${person.name} is an adult" + case _ => s"${person.name} is not an adult" + } + + println(adultResult) +} + +// Variadic Extractors +// Add cases for exceptions +// +// Adding some warning test cases +// - + +object VariadicExtractor { + // Define an unapply method that takes a List and returns an Option of Seq + def unapplySeq[A](list: List[A]): Seq[A]? = list +} + +object PatternMatchExample { + def describeList(list: List[Int]): String = list match { + case VariadicExtractor(1, 2, rest @ _*) => + s"Starts with 1, 2 followed by: ${rest.mkString(", ")}" + case VariadicExtractor(1, rest @ _*) => + s"Starts with 1 followed by: ${rest.mkString(", ")}" + case VariadicExtractor(first, second, rest @ _*) => + s"Starts with $first, $second followed by: ${rest.mkString(", ")}" + case VariadicExtractor(single) => + s"Only one element: $single" + case VariadicExtractor() => + "Empty list" + case _ => + "Unknown pattern" + } + + // Test cases + println(describeList(List(1, 2, 3, 4, 5))) // Output: Starts with 1, 2 followed by: 3, 4, 5 + println(describeList(List(1, 3, 4, 5))) // Output: Starts with 1 followed by: 3, 4, 5 + println(describeList(List(2, 3, 4, 5))) // Output: Starts with 2, 3 followed by: 4, 5 + println(describeList(List(1))) // Output: Only one element: 1 + println(describeList(List())) // Output: Empty list +} diff --git a/tests/init-global/pos/unapply-implicit-arg-pos-maybe.scala b/tests/init-global/pos/unapply-implicit-arg-pos-maybe.scala new file mode 100644 index 000000000000..4af2d9bb8333 --- /dev/null +++ b/tests/init-global/pos/unapply-implicit-arg-pos-maybe.scala @@ -0,0 +1,16 @@ +//> using options -Yexplicit-nulls +import language.experimental.magic +object Bar { + class Foo { + def m1(i: Int) = i + i1 + def m2(i: Int) = i + 2 + } + def unapply(using f1: Foo)(using f2: Foo)(i: Int)(using f3: Foo): Int? = + if i == 0 then f1.m1(i1) + f3.m1(i1) else f2.m2(i) + f3.m2(i) + + given Foo = new Foo + val i1: Int = 0 + val i2: Int = i1 match + case Bar(i) => i + case _ => 0 +} \ No newline at end of file diff --git a/tests/init-global/pos/unapplySeq-implicit-arg-pos-maybe.scala b/tests/init-global/pos/unapplySeq-implicit-arg-pos-maybe.scala new file mode 100644 index 000000000000..7a740d0ca712 --- /dev/null +++ b/tests/init-global/pos/unapplySeq-implicit-arg-pos-maybe.scala @@ -0,0 +1,16 @@ +//> using options -Yexplicit-nulls +import language.experimental.magic +object Bar { + class Foo { + def m1(seq: Seq[Int]) = 0 +: seq + def m2(seq: Seq[Int]) = i1 +: seq + } + def unapplySeq(using f1: Foo)(using f2: Foo)(seqi: Seq[Int])(using f3: Foo): Seq[Int]? = + if seqi(0) == 0 then f1.m1(seqi) else f2.m2(seqi) + + given Foo = new Foo + val i1: Int = 0 + val i2: Int = Seq(i1) match + case Bar(i) => i + case _ => 0 +} diff --git a/tests/init-global/warn/unapply-implicit-arg-maybe.check b/tests/init-global/warn/unapply-implicit-arg-maybe.check new file mode 100644 index 000000000000..246e394a252c --- /dev/null +++ b/tests/init-global/warn/unapply-implicit-arg-maybe.check @@ -0,0 +1,8 @@ +-- Warning: tests/init-global/warn/unapply-implicit-arg-maybe.scala:13:16 ---------------------------------------------- +13 | val i2: Int = i2 match // warn + | ^^ + | Access uninitialized field value i2. Calling trace: + | ├── object Bar { [ unapply-implicit-arg-maybe.scala:3 ] + | │ ^ + | └── val i2: Int = i2 match // warn [ unapply-implicit-arg-maybe.scala:13 ] + | ^^ diff --git a/tests/init-global/warn/unapply-implicit-arg-maybe.scala b/tests/init-global/warn/unapply-implicit-arg-maybe.scala new file mode 100644 index 000000000000..3f223239c479 --- /dev/null +++ b/tests/init-global/warn/unapply-implicit-arg-maybe.scala @@ -0,0 +1,16 @@ +//> using options -Yexplicit-nulls +import language.experimental.magic +object Bar { + class Foo { + def m1(i: Int) = i+1 + def m2(i: Int) = i+2 + } + def unapply(using f1: Foo)(i: Int): Int? = + if i == 0 then f1.m1(i) else f1.m2(i) + + given Foo = new Foo + val i1: Int = 0 + val i2: Int = i2 match // warn + case Bar(i) => i + case _ => 0 +} \ No newline at end of file diff --git a/tests/init-global/warn/unapply-implicit-arg2-maybe.check b/tests/init-global/warn/unapply-implicit-arg2-maybe.check new file mode 100644 index 000000000000..40a0e2cdc6ee --- /dev/null +++ b/tests/init-global/warn/unapply-implicit-arg2-maybe.check @@ -0,0 +1,12 @@ +-- Warning: tests/init-global/warn/unapply-implicit-arg2-maybe.scala:9:40 ---------------------------------------------- +9 | if i == 0 then f1.m1(i1) else f1.m2(i2) // warn + | ^^ + | Access uninitialized field value i2. Calling trace: + | ├── object Bar { [ unapply-implicit-arg2-maybe.scala:3 ] + | │ ^ + | ├── case Bar(i) => i [ unapply-implicit-arg2-maybe.scala:14 ] + | │ ^^^^^^ + | ├── def unapply(using f1: Foo)(i: Int): Int? = [ unapply-implicit-arg2-maybe.scala:8 ] + | │ ^ + | └── if i == 0 then f1.m1(i1) else f1.m2(i2) // warn [ unapply-implicit-arg2-maybe.scala:9 ] + | ^^ diff --git a/tests/init-global/warn/unapply-implicit-arg2-maybe.scala b/tests/init-global/warn/unapply-implicit-arg2-maybe.scala new file mode 100644 index 000000000000..04eff7686bb1 --- /dev/null +++ b/tests/init-global/warn/unapply-implicit-arg2-maybe.scala @@ -0,0 +1,16 @@ +//> using options -Yexplicit-nulls +import language.experimental.magic +object Bar { + class Foo { + def m1(i: Int) = i+1 + def m2(i: Int) = i+2 + } + def unapply(using f1: Foo)(i: Int): Int? = + if i == 0 then f1.m1(i1) else f1.m2(i2) // warn + + given Foo = new Foo + val i1: Int = 0 + val i2: Int = i1 match + case Bar(i) => i + case _ => 0 +} diff --git a/tests/init-global/warn/unapply-implicit-arg3-maybe.check b/tests/init-global/warn/unapply-implicit-arg3-maybe.check new file mode 100644 index 000000000000..da0664b4f7d3 --- /dev/null +++ b/tests/init-global/warn/unapply-implicit-arg3-maybe.check @@ -0,0 +1,14 @@ +-- Warning: tests/init-global/warn/unapply-implicit-arg3-maybe.scala:6:25 ---------------------------------------------- +6 | def m2(i: Int) = i + i2 // warn + | ^^ + | Access uninitialized field value i2. Calling trace: + | ├── object Bar { [ unapply-implicit-arg3-maybe.scala:3 ] + | │ ^ + | ├── case Bar(i) => i [ unapply-implicit-arg3-maybe.scala:14 ] + | │ ^^^^^^ + | ├── def unapply(using f1: Foo)(i: Int): Int? = [ unapply-implicit-arg3-maybe.scala:8 ] + | │ ^ + | ├── if i == 0 then f1.m1(i) else f1.m2(i) [ unapply-implicit-arg3-maybe.scala:9 ] + | │ ^^^^^^^^ + | └── def m2(i: Int) = i + i2 // warn [ unapply-implicit-arg3-maybe.scala:6 ] + | ^^ diff --git a/tests/init-global/warn/unapply-implicit-arg3-maybe.scala b/tests/init-global/warn/unapply-implicit-arg3-maybe.scala new file mode 100644 index 000000000000..7acc075c36b8 --- /dev/null +++ b/tests/init-global/warn/unapply-implicit-arg3-maybe.scala @@ -0,0 +1,16 @@ +//> using options -Yexplicit-nulls +import language.experimental.magic +object Bar { + class Foo { + def m1(i: Int) = i + i1 + def m2(i: Int) = i + i2 // warn + } + def unapply(using f1: Foo)(i: Int): Int? = + if i == 0 then f1.m1(i) else f1.m2(i) + + given Foo = new Foo + val i1: Int = 0 + val i2: Int = i1 match + case Bar(i) => i + case _ => 0 +} diff --git a/tests/neg/bad-unapplies-maybe.check b/tests/neg/bad-unapplies-maybe.check new file mode 100644 index 000000000000..c76f6b45d0c0 --- /dev/null +++ b/tests/neg/bad-unapplies-maybe.check @@ -0,0 +1,39 @@ +-- [E051] Reference Error: tests/neg/bad-unapplies-maybe.scala:24:9 ---------------------------------------------------- +24 | case A("2") => // error (cannot resolve overloading) + | ^ + | Ambiguous overload. The overloaded alternatives of method unapply in object A with types + | (x: B): String? + | (x: A): String? + | both match arguments (C) + | + | longer explanation available when compiling with `-explain` +-- [E127] Pattern Match Error: tests/neg/bad-unapplies-maybe.scala:25:9 ------------------------------------------------ +25 | case B("2") => // error (cannot be used as an extractor) + | ^ + |B cannot be used as an extractor in a pattern because it lacks an unapply or unapplySeq method with the appropriate signature + | + | longer explanation available when compiling with `-explain` +-- [E127] Pattern Match Error: tests/neg/bad-unapplies-maybe.scala:26:9 ------------------------------------------------ +26 | case D("2") => // error (cannot be used as an extractor) + | ^ + |D cannot be used as an extractor in a pattern because it lacks an unapply or unapplySeq method with the appropriate signature + | + | longer explanation available when compiling with `-explain` +-- [E050] Type Error: tests/neg/bad-unapplies-maybe.scala:27:9 --------------------------------------------------------- +27 | case E("2") => // error (value unapply in object E does not take parameters) + | ^ + | value unapply in object E does not take parameters + | + | longer explanation available when compiling with `-explain` +-- [E107] Syntax Error: tests/neg/bad-unapplies-maybe.scala:28:10 ------------------------------------------------------ +28 | case F("2") => // error (Wrong number of argument patterns for F; expected: ()) + | ^^^^^^ + | Wrong number of argument patterns for F; expected: () + | + | longer explanation available when compiling with `-explain` +-- [E189] Not Found Error: tests/neg/bad-unapplies-maybe.scala:29:9 ---------------------------------------------------- +29 | case G("2") => // error (Not found: G) + | ^ + | no pattern match extractor named G was found + | + | longer explanation available when compiling with `-explain` diff --git a/tests/neg/bad-unapplies-maybe.scala b/tests/neg/bad-unapplies-maybe.scala new file mode 100644 index 000000000000..db811c5ab09d --- /dev/null +++ b/tests/neg/bad-unapplies-maybe.scala @@ -0,0 +1,30 @@ +//> using options -Yexplicit-nulls +import language.experimental.magic +trait A +trait B +class C extends A, B +object A: + def unapply(x: A): String? = x.toString + def unapply(x: B): String? = x.toString + +object B + +object D: + def unapply(x: A, y: B): String? = x.toString + +object E: + val unapply: Option[String] = Some("") + +object F: + def unapply(x: Int): Boolean = true + + +@main def Test = + C() match + case A("2") => // error (cannot resolve overloading) + case B("2") => // error (cannot be used as an extractor) + case D("2") => // error (cannot be used as an extractor) + case E("2") => // error (value unapply in object E does not take parameters) + case F("2") => // error (Wrong number of argument patterns for F; expected: ()) + case G("2") => // error (Not found: G) +end Test diff --git a/tests/neg/experimentalUnapply-maybe.scala b/tests/neg/experimentalUnapply-maybe.scala new file mode 100644 index 000000000000..c1dce46b7b85 --- /dev/null +++ b/tests/neg/experimentalUnapply-maybe.scala @@ -0,0 +1,22 @@ +//> using options -Yexplicit-nulls + + +import language.experimental.magic +import scala.annotation.experimental + +@experimental +class A + +object Extractor1: + def unapply(s: Any): A? = ??? // error + +object Extractor2: + @experimental + def unapply(s: Any): Int? = ??? + +def test: Unit = + (??? : Any) match + case _: A => // error // error + case Extractor1(_) => // error + case Extractor2(_) => // error + () diff --git a/tests/neg/i1793-maybe.scala b/tests/neg/i1793-maybe.scala new file mode 100644 index 000000000000..70b24305d48d --- /dev/null +++ b/tests/neg/i1793-maybe.scala @@ -0,0 +1,9 @@ +//> using options -Yexplicit-nulls +import language.experimental.magic +object Test { + import scala.ref.WeakReference + def unapply[T <: AnyVal](wr: WeakReference[T]): T? = { + val x = wr.underlying.get + if (x != null) x else null // error + } +} diff --git a/tests/neg/i21841-maybe.check b/tests/neg/i21841-maybe.check new file mode 100644 index 000000000000..101175423ff8 --- /dev/null +++ b/tests/neg/i21841-maybe.check @@ -0,0 +1,6 @@ +-- [E108] Declaration Error: tests/neg/i21841-maybe.scala:22:13 -------------------------------------------------------- +22 | case v[T](l, r) => () // error + | ^^^^^^^^^^ + | (Test.Expr[Test.T], Test.Expr[Test.T])? is not a valid result type of an unapplySeq method of an extractor. + | + | longer explanation available when compiling with `-explain` diff --git a/tests/neg/i21841-maybe.scala b/tests/neg/i21841-maybe.scala new file mode 100644 index 000000000000..9303c79bde66 --- /dev/null +++ b/tests/neg/i21841-maybe.scala @@ -0,0 +1,24 @@ +//> using options -Yexplicit-nulls +import language.experimental.magic +object Test { + + sealed trait T + sealed trait Arrow[A, B] + + type ArgsTo[S1, Target] <: NonEmptyTuple = S1 match { + case Arrow[a, Target] => Tuple1[Expr[a]] + case Arrow[a, b] => Expr[a] *: ArgsTo[b, Target] + } + + sealed trait Expr[S] : + def unapplySeq[Target](e: Expr[Target]): ArgsTo[S, Target]? = ??? + + case class Variable[S](id: String) extends Expr[S] + + val v = Variable[Arrow[T, Arrow[T, T]]]("v") + val e : Expr[T] = ??? + + e match + case v[T](l, r) => () // error + case _ => () +} diff --git a/tests/neg/i23156-maybe.scala b/tests/neg/i23156-maybe.scala new file mode 100644 index 000000000000..c71c39ea2a95 --- /dev/null +++ b/tests/neg/i23156-maybe.scala @@ -0,0 +1,8 @@ +//> using options -Yexplicit-nulls +import language.experimental.magic +object Unpack { + (1, 2) match { + case Unpack(first, _) => first + } + def unapply(e: (Int, Int)): T? = ??? // error +} \ No newline at end of file diff --git a/tests/neg/i2378-maybe.scala b/tests/neg/i2378-maybe.scala new file mode 100644 index 000000000000..2271e7afdc98 --- /dev/null +++ b/tests/neg/i2378-maybe.scala @@ -0,0 +1,32 @@ +//> using options -Yexplicit-nulls +import language.experimental.magic +trait Cap + +trait Toolbox { + type Tree + + val tpd: TypedTrees + trait TypedTrees { + type Tree + } + + val Apply: ApplyImpl + + trait ApplyImpl { + def unapply(tree: Tree): (Tree, Seq[Tree])? + def unapply(tree: tpd.Tree)(implicit c: Cap): (tpd.Tree, Seq[tpd.Tree])? + } +} + +class Test(val tb: Toolbox) { + import tb.* + implicit val cap: Cap = null.asInstanceOf[Cap] + + def foo(tree: Tree): Int = (tree: Any) match { + case tb.Apply(fun, args) => 3 // error: ambiguous overload of unapply + } + + def bar(tree: tpd.Tree): Int = (tree: Any) match { + case Apply(fun, args) => 3 // error: ambiguous overload of unapply + } +} diff --git a/tests/neg/i24168-maybe.scala b/tests/neg/i24168-maybe.scala new file mode 100644 index 000000000000..bb792a4c470e --- /dev/null +++ b/tests/neg/i24168-maybe.scala @@ -0,0 +1,14 @@ +//> using options -Yexplicit-nulls +import language.experimental.magic +trait Generic extends Selectable: + def applyDynamic(name: String)(args: Any*): Any = () + +val foo: Generic { + def unapply(x: Int): Unit? +} = new Generic: + def unapply(x: Int): Unit? = () + +def x = + 42 match + case foo(()) => println("lol") // error + diff --git a/tests/neg/i3989c-maybe.scala b/tests/neg/i3989c-maybe.scala new file mode 100644 index 000000000000..f2455c430652 --- /dev/null +++ b/tests/neg/i3989c-maybe.scala @@ -0,0 +1,18 @@ +//> using options -Yexplicit-nulls +import language.experimental.magic +import scala.magic.* +import scala.Option +object Test extends App { + trait A[+X] + class B[+X](val x: X) extends A[X] + object B { + def unapply[X](b: B[X]): X? = Ok(b.x) + } + + class C[+X](x: Any) extends B[Any](x) with A[X] + def f(a: A[Int]): Int = a match { + case B(i) => i // error + case _ => 0 + } + f(new C[Int]("foo")) +} diff --git a/tests/neg/i8530-b-maybe.check b/tests/neg/i8530-b-maybe.check new file mode 100644 index 000000000000..a45bf861440f --- /dev/null +++ b/tests/neg/i8530-b-maybe.check @@ -0,0 +1,7 @@ +-- [E007] Type Mismatch Error: tests/neg/i8530-b-maybe.scala:10:25 ----------------------------------------------------- +10 | case Some(xs) => xs // error + | ^^ + | Found: (xs : List[String | Null]) + | Required: List[String]? + | + | longer explanation available when compiling with `-explain` diff --git a/tests/neg/i8530-b-maybe.scala b/tests/neg/i8530-b-maybe.scala new file mode 100644 index 000000000000..14b38813004f --- /dev/null +++ b/tests/neg/i8530-b-maybe.scala @@ -0,0 +1,11 @@ +//> using options -Yexplicit-nulls +import language.experimental.magic +import scala.compiletime.erasedValue + +class MyRegex[Pattern <: String & Singleton/*Literal constant*/]: + inline def unapplySeq(s: CharSequence): List[String]? = + inline erasedValue[Pattern] match + case "foo" => if s == "foo" then Nil else null + case _ => valueOf[Pattern].r.unapplySeq(s) match + case Some(xs) => xs // error + case None => null diff --git a/tests/neg/i8894-maybe.scala b/tests/neg/i8894-maybe.scala new file mode 100644 index 000000000000..7479b04ea265 --- /dev/null +++ b/tests/neg/i8894-maybe.scala @@ -0,0 +1,15 @@ +//> using options -Yexplicit-nulls +import language.experimental.magic +trait Extractor { + inline def unapplySeq(inline tn: String): Seq[String]? +} + +extension (inline sc: StringContext) transparent inline def poql: Extractor = new Extractor { + inline def unapplySeq(inline tn: String): Seq[String]? = ??? // error: Implementation restriction: nested inline methods are not supported +} + +object x { + "x" match { + case poql" $x" => x // error: Deferred inline method unapplySeq in trait Extractor cannot be invoked + } +} diff --git a/tests/neg/infix-maybe.check b/tests/neg/infix-maybe.check new file mode 100644 index 000000000000..303d5ef2b12e --- /dev/null +++ b/tests/neg/infix-maybe.check @@ -0,0 +1,30 @@ +-- Error: tests/neg/infix-maybe.scala:27:4 ----------------------------------------------------------------------------- +27 | c mop 2 // error: should not be used as infix operator + | ^^^ + | Alphanumeric method mop is not declared infix; it should not be used as infix operator. + | Instead, use method syntax .mop(...) or backticked identifier `mop`. + | The latter can be rewritten automatically under -rewrite -source 3.4-migration. +-- Error: tests/neg/infix-maybe.scala:28:4 ----------------------------------------------------------------------------- +28 | c meth 2 // error: should not be used as infix operator + | ^^^^ + | Alphanumeric method meth is not declared infix; it should not be used as infix operator. + | Instead, use method syntax .meth(...) or backticked identifier `meth`. + | The latter can be rewritten automatically under -rewrite -source 3.4-migration. +-- Error: tests/neg/infix-maybe.scala:46:14 ---------------------------------------------------------------------------- +46 | val x1: Int Map String = ??? // error + | ^^^ + | Alphanumeric type Map is not declared infix; it should not be used as infix operator. + | Instead, use prefix syntax Map[...] or backticked identifier `Map`. + | The latter can be rewritten automatically under -rewrite -source 3.4-migration. +-- Error: tests/neg/infix-maybe.scala:48:14 ---------------------------------------------------------------------------- +48 | val x3: Int AndC String = ??? // error + | ^^^^ + | Alphanumeric type AndC is not declared infix; it should not be used as infix operator. + | Instead, use prefix syntax AndC[...] or backticked identifier `AndC`. + | The latter can be rewritten automatically under -rewrite -source 3.4-migration. +-- Error: tests/neg/infix-maybe.scala:62:8 ----------------------------------------------------------------------------- +62 | val _ Pair _ = p // error + | ^^^^ + | Alphanumeric extractor Pair is not declared infix; it should not be used as infix operator. + | Instead, use prefix syntax Pair(...) or backticked identifier `Pair`. + | The latter can be rewritten automatically under -rewrite -source 3.4-migration. diff --git a/tests/neg/infix-maybe.scala b/tests/neg/infix-maybe.scala new file mode 100644 index 000000000000..cc800dc83370 --- /dev/null +++ b/tests/neg/infix-maybe.scala @@ -0,0 +1,71 @@ +//> using options -source future -deprecation -Yexplicit-nulls + +// Compile with -strict -Xfatal-warnings -deprecation +import language.experimental.magic +class C: + infix def op(x: Int): Int = ??? + def meth(x: Int): Int = ??? + def matching(x: Int => Int) = ??? + def +(x: Int): Int = ??? + +object C: + given AnyRef: + extension (x: C) + infix def iop (y: Int) = ??? + def mop (y: Int) = ??? + def ++ (y: Int) = ??? + +val c = C() +def test() = { + c op 2 + c iop 2 + c.meth(2) + c ++ 2 + + c.op(2) + c.iop(2) + c mop 2 // error: should not be used as infix operator + c meth 2 // error: should not be used as infix operator + c `meth` 2 // OK, sincd `meth` is backquoted + c + 3 // OK, since `+` is symbolic + 1 to 2 // OK, since `to` is defined by Scala-2 + c meth { // OK, since `meth` is followed by `{...}` + 3 + } + c matching { // OK, since `meth` is followed by `{...}` + case x => x + } + + infix class Or[X, Y] + class AndC[X, Y] + infix type And[X, Y] = AndC[X, Y] + infix type &&[X, Y] = AndC[X, Y] + + class Map[X, Y] + + val x1: Int Map String = ??? // error + val x2: Int Or String = ??? // OK since Or is declared `infix` + val x3: Int AndC String = ??? // error + val x4: Int `AndC` String = ??? // OK + val x5: Int And String = ??? // OK + val x6: Int && String = ??? + + case class Pair[T](x: T, y: T) + infix case class Q[T](x: T, y: T) + + object PP { + infix def unapply[T](x: Pair[T]): (T, T)? = (x.x, x.y) + } + + val p = Pair(1, 2) + val Pair(_, _) = p + val _ Pair _ = p // error + val _ `Pair` _ = p // OK + val (_ PP _) = p: @unchecked // OK + + val q = Q(1, 2) + val Q(_, _) = q + val _ Q _ = q // OK + + +} \ No newline at end of file diff --git a/tests/neg/orelse-subtyping.check b/tests/neg/orelse-subtyping.check index 22ec1b09aadd..392f6f7ffe04 100644 --- a/tests/neg/orelse-subtyping.check +++ b/tests/neg/orelse-subtyping.check @@ -6,10 +6,4 @@ | Note that implicit conversions were not tried because the result of an implicit conversion | must be more specific than String ? String | - | One of the following imports might fix the problem: - | - | import scala.reflect.Selectable.reflectiveSelectable - | import scala.util.chaining.scalaUtilChainingOps - | - | | longer explanation available when compiling with `-explain` diff --git a/tests/neg/patmat2-maybe.scala b/tests/neg/patmat2-maybe.scala new file mode 100644 index 000000000000..b93e63a0a3b9 --- /dev/null +++ b/tests/neg/patmat2-maybe.scala @@ -0,0 +1,36 @@ +//> using options -Yexplicit-nulls +import language.experimental.magic +import java.io.IOException +import java.lang.NullPointerException +import java.lang.IllegalArgumentException + +object IAE { + def unapply(e: Exception): String? = + if (e.isInstanceOf[IllegalArgumentException]) 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..e734ac88c868 --- /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 | 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:11 ----------------------------------------------- +12 | for ((x: String) <- xs) do () // error: pattern type more specialized + | ^^^^^^ + | pattern's type String is more specialized than the right hand side expression's type AnyRef + | + | If the narrowing is intentional, this can be communicated by adding the `case` keyword before the full pattern, + | which will result in a filtering for expression (using `withFilter`). + | This patch can be rewritten automatically under -rewrite -source 3.2-migration. +-- Error: tests/neg/refutable-pattern-binding-messages-maybe.scala:16:13 ----------------------------------------------- +16 | for none @ None <- ys do () // error: pattern type does not match + | ^^^^ + | pattern's type None.type does not match the right hand side expression's type (x$1 : Option[?]) + | + | If the narrowing is intentional, this can be communicated by adding the `case` keyword before the full pattern, + | which will result in a filtering for expression (using `withFilter`). + | This patch can be rewritten automatically under -rewrite -source 3.2-migration. +-- Warning: tests/neg/refutable-pattern-binding-messages-maybe.scala:6:14 ---------------------------------------------- +6 | val Positive(p) = 5 // warn: 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. +-- Warning: tests/neg/refutable-pattern-binding-messages-maybe.scala:11:20 --------------------------------------------- +11 | val i :: is = List(1, 2, 3) // warn: 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. +-- Warning: tests/neg/refutable-pattern-binding-messages-maybe.scala:17:10 --------------------------------------------- +17 | val 1 = 2 // warn: 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..deb671a7be07 --- /dev/null +++ b/tests/neg/refutable-pattern-binding-messages-maybe.scala @@ -0,0 +1,18 @@ +//> using options -source 3.8 -Yexplicit-nulls +import language.experimental.magic +object Test { + // refutable extractor + object Positive { def unapply(i: Int): Int? = if i > 0 then i else null } + val Positive(p) = 5 // warn: 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) // warn: 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 // warn: pattern type does not match +} diff --git a/tests/neg/refutable-pattern-binding-messages-old-maybe.check b/tests/neg/refutable-pattern-binding-messages-old-maybe.check new file mode 100644 index 000000000000..85c687a43a1e --- /dev/null +++ b/tests/neg/refutable-pattern-binding-messages-old-maybe.check @@ -0,0 +1,48 @@ +-- Error: tests/neg/refutable-pattern-binding-messages-old-maybe.scala:7:14 -------------------------------------------- +7 | for Positive(i) <- List(1, 2, 3) do () // error: refutable extractor + | ^^^^^^^^^^^ + | pattern binding uses refutable extractor `Test.Positive` + | + | If this usage is intentional, this can be communicated by adding the `case` keyword before the full pattern, + | which will result in a filtering for expression (using `withFilter`). + | This patch can be rewritten automatically under -rewrite -source 3.2-migration. +-- Error: tests/neg/refutable-pattern-binding-messages-old-maybe.scala:12:11 ------------------------------------------- +12 | for ((x: String) <- xs) do () // error: pattern type more specialized + | ^^^^^^ + | pattern's type String is more specialized than the right hand side expression's type AnyRef + | + | If the narrowing is intentional, this can be communicated by adding the `case` keyword before the full pattern, + | which will result in a filtering for expression (using `withFilter`). + | This patch can be rewritten automatically under -rewrite -source 3.2-migration. +-- Error: tests/neg/refutable-pattern-binding-messages-old-maybe.scala:16:13 ------------------------------------------- +16 | for none @ None <- ys do () // error: pattern type does not match + | ^^^^ + | pattern's type None.type does not match the right hand side expression's type (x$1 : Option[?]) + | + | If the narrowing is intentional, this can be communicated by adding the `case` keyword before the full pattern, + | which will result in a filtering for expression (using `withFilter`). + | This patch can be rewritten automatically under -rewrite -source 3.2-migration. +-- Warning: tests/neg/refutable-pattern-binding-messages-old-maybe.scala:6:14 ------------------------------------------ +6 | val Positive(p) = 5 // warn: refutable extractor + | ^^^^^^^^^^^^^^^ + | pattern binding uses refutable extractor `Test.Positive` + | + | If this usage is intentional, this can be communicated by adding `: @unchecked` after the expression, + | which may result in a MatchError at runtime. + | This patch can be rewritten automatically under -rewrite -source 3.2-migration. +-- Warning: tests/neg/refutable-pattern-binding-messages-old-maybe.scala:11:20 ----------------------------------------- +11 | val i :: is = List(1, 2, 3) // warn: 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 `: @unchecked` after the expression, + | which may result in a MatchError at runtime. + | This patch can be rewritten automatically under -rewrite -source 3.2-migration. +-- Warning: tests/neg/refutable-pattern-binding-messages-old-maybe.scala:17:10 ----------------------------------------- +17 | val 1 = 2 // warn: 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 `: @unchecked` after the expression, + | which may result in a MatchError at runtime. + | This patch can be rewritten automatically under -rewrite -source 3.2-migration. diff --git a/tests/neg/refutable-pattern-binding-messages-old-maybe.scala b/tests/neg/refutable-pattern-binding-messages-old-maybe.scala new file mode 100644 index 000000000000..3691515be5e5 --- /dev/null +++ b/tests/neg/refutable-pattern-binding-messages-old-maybe.scala @@ -0,0 +1,18 @@ +//> using options -source 3.7 -Yexplicit-nulls +import language.experimental.magic +object Test { + // refutable extractor + object Positive { def unapply(i: Int): Int? = if i > 0 then i else null } + val Positive(p) = 5 // warn: 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) // warn: 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 // warn: pattern type does not match +} diff --git a/tests/neg/t7868-maybe.scala b/tests/neg/t7868-maybe.scala new file mode 100644 index 000000000000..d57cda9f6ca4 --- /dev/null +++ b/tests/neg/t7868-maybe.scala @@ -0,0 +1,15 @@ +//> using options -Yexplicit-nulls +import language.experimental.magic +object A { + def unapply(n: Int): Int? = n + + def run = (0: Short) match { + case A(_) => // error: this case is unreachable since class Short is not a subclass of class Int + case _ => + } + + def run2 = (0: Short) match { + case x: Int => // error: this case is unreachable since class Short is not a subclass of class Int + case _ => + } +} diff --git a/tests/neg/tryPatternMatchEq-maybe.scala b/tests/neg/tryPatternMatchEq-maybe.scala new file mode 100644 index 000000000000..651636373e00 --- /dev/null +++ b/tests/neg/tryPatternMatchEq-maybe.scala @@ -0,0 +1,36 @@ +//> using options -Yexplicit-nulls +import language.experimental.magic +import java.io.IOException +import java.lang.NullPointerException +import java.lang.IllegalArgumentException + +object IAE { + def unapply(e: Exception): String? = + if (e.isInstanceOf[IllegalArgumentException]) 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 `a` => // error: cannot compare + case EX => + case IAE(msg) => + case e: IllegalArgumentException => + } + } +} diff --git a/tests/neg/unchecked-patterns-maybe.scala b/tests/neg/unchecked-patterns-maybe.scala new file mode 100644 index 000000000000..8107111759b1 --- /dev/null +++ b/tests/neg/unchecked-patterns-maybe.scala @@ -0,0 +1,29 @@ +//> using options -Werror -Yexplicit-nulls + +import language.experimental.magic +object Test { + + val (y1: Some[Int]) = Some(1): Option[Int] @unchecked // OK + val y2: Some[Int] @unchecked = Some(1): Option[Int] // error + + val x :: xs = List(1, 2, 3) // warn + val (1, c) = (1, 2) // warn + val 1 *: cs = 1 *: Tuple() // warn + + val (_: Int | _: AnyRef) = ??? : AnyRef // warn + + val 1 = 2 // warn + + 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 // warn + val Some(s1) = Option(1) // warn + 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 +} +// nopos-error: No warnings can be incurred under -Werror (or -Xfatal-warnings) diff --git a/tests/patmat/3543-maybe.scala b/tests/patmat/3543-maybe.scala new file mode 100644 index 000000000000..f74355f08c77 --- /dev/null +++ b/tests/patmat/3543-maybe.scala @@ -0,0 +1,54 @@ +//> using options -Yexplicit-nulls +import language.experimental.magic +class Test { + class Foo { + def unapply(x: String): String? = ??? + } + + def test(xs: List[String]): Unit = { + val Yes = new Foo + val No = new Foo + + xs match { + case Yes(x) :: ls => println("Yes") + case No(y) :: ls => println("No") + case _ => + } + } +} + +class Test2 { + class Foo(x: Boolean) { + def unapply(y: String): Boolean = x + } + + def test(xs: List[String]): Unit = { + val Yes = new Foo(true) + val No = new Foo(false) + + xs match { + case No() :: ls => println("No") + case Yes() :: ls => println("Yes") + case _ => + } + } +} + +class Test3 { + import scala.util.matching.Regex + + def main(args: Array[String]): Unit = { + foo("c" :: Nil, false) + } + + def foo(remaining: List[String], inCodeBlock: Boolean): Unit = { + remaining match { + case CodeBlockEndRegex(before) :: ls => + case SymbolTagRegex(name) :: ls if !inCodeBlock => println("OK") + case _ => + } + } + + val CodeBlockEndRegex = new Regex("(b)") + val SymbolTagRegex = new Regex("(c)") +} diff --git a/tests/patmat/dotty-maybe.scala b/tests/patmat/dotty-maybe.scala new file mode 100644 index 000000000000..73889619bf67 --- /dev/null +++ b/tests/patmat/dotty-maybe.scala @@ -0,0 +1,32 @@ +//> using options -Yexplicit-nulls +import language.experimental.magic +object IntEqualityTestTreeMaker { + def unapply(xs: Int): Int? = ??? +} + +class Test { + def isBelow(n: Int, s: String): Boolean = false + + def foo(xs: List[(Int, String)]): Unit = xs.filter(isBelow.tupled) match { + case Nil => + case matches => + } + + def linkCompanions(xs: List[(Int, Int)]): Unit = { + xs.groupBy(_._1).foreach { + case (_, List(x1, x2)) => + case _ => () + } + } + + def bar(xs: List[(Int, String)]): Unit = xs match { + case (x, s) :: Nil => + case Nil => + case _ => + } + + def patmat(alts: List[List[Int]]): Unit = alts.forall { + case List(IntEqualityTestTreeMaker(_)) => false + case _ => true + } +} \ No newline at end of file diff --git a/tests/patmat/i2363-maybe.check b/tests/patmat/i2363-maybe.check new file mode 100644 index 000000000000..2372f149ba18 --- /dev/null +++ b/tests/patmat/i2363-maybe.check @@ -0,0 +1,16 @@ +-- [E029] Pattern Match Exhaustivity Warning: tests/patmat/i2363-maybe.scala:17:32 +17 | def foo(x: List[Expr]): Int = x match { + | ^ + | match may not be exhaustive. + | + | It would fail on pattern case: List(_, _*) + | + | longer explanation available when compiling with `-explain` +-- [E029] Pattern Match Exhaustivity Warning: tests/patmat/i2363-maybe.scala:23:26 +23 | def bar(x: Expr): Int = x match { + | ^ + | match may not be exhaustive. + | + | It would fail on pattern case: _: IntExpr, _: BooleanExpr + | + | longer explanation available when compiling with `-explain` diff --git a/tests/patmat/i2363-maybe.scala b/tests/patmat/i2363-maybe.scala new file mode 100644 index 000000000000..374450427c4f --- /dev/null +++ b/tests/patmat/i2363-maybe.scala @@ -0,0 +1,27 @@ +//> using options -Yexplicit-nulls +import language.experimental.magic +sealed trait Expr +class IntExpr extends Expr +class BooleanExpr extends Expr + +object IntExpr { + def unapply(expr: Expr): IntExpr? = ??? +} + +object BooleanExpr { + def unapply(expr: Expr): BooleanExpr? = ??? +} + + +class Test { + def foo(x: List[Expr]): Int = x match { + case IntExpr(_) :: xs => 1 + case BooleanExpr(_) :: xs => 1 + case Nil => 2 + } + + def bar(x: Expr): Int = x match { + case IntExpr(_) => 1 + case BooleanExpr(_) => 2 + } +} diff --git a/tests/patmat/i2502-maybe.check b/tests/patmat/i2502-maybe.check new file mode 100644 index 000000000000..c94f58825422 --- /dev/null +++ b/tests/patmat/i2502-maybe.check @@ -0,0 +1,8 @@ +-- [E029] Pattern Match Exhaustivity Warning: tests/patmat/i2502-maybe.scala:7:35 +7 | def classOrArrayType: String = this match { + | ^^^^ + | match may not be exhaustive. + | + | It would fail on pattern case: _: BTypes.this.ClassBType + | + | longer explanation available when compiling with `-explain` diff --git a/tests/patmat/i2502-maybe.scala b/tests/patmat/i2502-maybe.scala new file mode 100644 index 000000000000..ac660be002b7 --- /dev/null +++ b/tests/patmat/i2502-maybe.scala @@ -0,0 +1,19 @@ +//> using options -Yexplicit-nulls +import language.experimental.magic +abstract class BTypes { + trait BType + + sealed trait RefBType extends BType { + def classOrArrayType: String = this match { + case ClassBType(internalName) => internalName + case a: ArrayBType => "" + } + } + + final class ClassBType(val internalName: String) extends RefBType + class ArrayBType extends RefBType + + object ClassBType { + def unapply(x: ClassBType): String? = null + } +} \ No newline at end of file diff --git a/tests/patmat/i2502b-maybe.check b/tests/patmat/i2502b-maybe.check new file mode 100644 index 000000000000..859da05bb41e --- /dev/null +++ b/tests/patmat/i2502b-maybe.check @@ -0,0 +1,8 @@ +-- [E029] Pattern Match Exhaustivity Warning: tests/patmat/i2502b-maybe.scala:7:35 +7 | def classOrArrayType: String = this match { + | ^^^^ + | match may not be exhaustive. + | + | It would fail on pattern case: _: BTypes.this.ClassBType + | + | longer explanation available when compiling with `-explain` diff --git a/tests/patmat/i2502b-maybe.scala b/tests/patmat/i2502b-maybe.scala new file mode 100644 index 000000000000..327fe7dcad26 --- /dev/null +++ b/tests/patmat/i2502b-maybe.scala @@ -0,0 +1,19 @@ +//> using options -Yexplicit-nulls +import language.experimental.magic +abstract class BTypes { + trait BType + + sealed trait RefBType extends BType { + def classOrArrayType: String = this match { + case ClassBType(internalName) => internalName + case a: ArrayBType => "" + } + } + + final case class ClassBType(val internalName: String) extends RefBType + class ArrayBType extends RefBType + + object ClassBType { + def unapply(x: RefBType): String? = null + } +} diff --git a/tests/patmat/optionless-maybe.check b/tests/patmat/optionless-maybe.check new file mode 100644 index 000000000000..7b84b1f21e4c --- /dev/null +++ b/tests/patmat/optionless-maybe.check @@ -0,0 +1,8 @@ +-- [E029] Pattern Match Exhaustivity Warning: tests/patmat/optionless-maybe.scala:30:44 +30 | def qux(t: Tree)(implicit 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..f86ba1f100ac --- /dev/null +++ b/tests/patmat/optionless-maybe.scala @@ -0,0 +1,34 @@ +//> using options -Yexplicit-nulls +import language.experimental.magic +sealed trait Tree +case class Ident(name: String) extends Tree + +object Ident1 { + def unapply(tree: Tree): Ident = ??? +} + +trait Cap +object Ident2 { + def unapply(tree: Tree)(implicit any: Cap): Ident = ??? +} + +object Ident3 { + def unapply(tree: Tree)(implicit any: Cap): Ident? = ??? +} + + + +class Test { + def foo(t: Tree): Unit = t match { + case Ident1(t) => + } + + def bar(t: Tree)(implicit c: Cap): Unit = t match { + case Ident2(t) => + } + + def qux(t: Tree)(implicit c: Cap): Unit = t match { + case Ident3(t) => + } + +} \ No newline at end of file diff --git a/tests/patmat/patmat-extractor-maybe.check b/tests/patmat/patmat-extractor-maybe.check new file mode 100644 index 000000000000..38f48e78403b --- /dev/null +++ b/tests/patmat/patmat-extractor-maybe.check @@ -0,0 +1,12 @@ +-- [E029] Pattern Match Exhaustivity Warning: tests/patmat/patmat-extractor-maybe.scala:15:30 +15 | def foo(x: Node): Boolean = x match { // unexhaustive + | ^ + | match may not be exhaustive. + | + | It would fail on pattern case: NodeA(_), NodeB(_), NodeC(_) + | + | longer explanation available when compiling with `-explain` +-- [E030] Match case Unreachable Warning: tests/patmat/patmat-extractor-maybe.scala:17:13 +17 | case Node(NodeA(4), NodeB(false)) => true // unreachable code + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + | Unreachable case diff --git a/tests/patmat/patmat-extractor-maybe.scala b/tests/patmat/patmat-extractor-maybe.scala new file mode 100644 index 000000000000..613cbbf256df --- /dev/null +++ b/tests/patmat/patmat-extractor-maybe.scala @@ -0,0 +1,19 @@ +//> using options -Yexplicit-nulls +import language.experimental.magic +sealed trait Node +case class NodeA(i: Int) extends Node +case class NodeB(b: Boolean) extends Node +case class NodeC(s: String) extends Node + +object Node { + def unapply(node: Node): (Node, Node)? = ??? +} + +// currently scalac can't do anything with following +// it's possible to do better in our case +object Test { + def foo(x: Node): Boolean = x match { // unexhaustive + case Node(NodeA(_), NodeB(_)) => true + case Node(NodeA(4), NodeB(false)) => true // unreachable code + } +} \ No newline at end of file diff --git a/tests/patmat/t8511-maybe.check b/tests/patmat/t8511-maybe.check new file mode 100644 index 000000000000..b094816491ec --- /dev/null +++ b/tests/patmat/t8511-maybe.check @@ -0,0 +1,8 @@ +-- [E029] Pattern Match Exhaustivity Warning: tests/patmat/t8511-maybe.scala:20:42 +20 | private def logic(head: Expr): String = head match { + | ^^^^ + | match may not be exhaustive. + | + | It would fail on pattern case: Bar(_), Baz(), EatsExhaustiveWarning(_) + | + | longer explanation available when compiling with `-explain` diff --git a/tests/patmat/t8511-maybe.scala b/tests/patmat/t8511-maybe.scala new file mode 100644 index 000000000000..2a4ff200e4b3 --- /dev/null +++ b/tests/patmat/t8511-maybe.scala @@ -0,0 +1,27 @@ +//> using options -Yexplicit-nulls +import language.experimental.magic +sealed trait Expr +final case class Foo(other: Option[String]) extends Expr +final case class Bar(someConstant: String) extends Expr +final case class Baz() extends Expr +final case class EatsExhaustiveWarning(other: Reference) extends Expr + +sealed trait Reference { + val value: String +} + +object Reference { + def unapply(reference: Reference): (String)? = { + reference.value + } +} + +object EntryPoint { + private def logic(head: Expr): String = head match { + case Foo(_) => + ??? + // Commenting this line only causes the exhaustive search warning to be emitted + case EatsExhaustiveWarning(Reference(text)) => + ??? + } +} \ 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..99162b59602f --- /dev/null +++ b/tests/pos-custom-args/captures/i24729-maybe.scala @@ -0,0 +1,23 @@ +//> using options -Yexplicit-nulls +import language.experimental.magic +import scala.collection.SeqOps + +inline def foo[A, B](self: A, y: B): (A, B) = (self, y) +def bar[A, B](self: A, y: B): (A, B) = (self, y) + +object Test: + def test[A, CC[_] <: Seq[?], C <: SeqOps[A, CC, C]](t: (C & SeqOps[A, CC, C])^) = + foo(t.head, t.tail) + bar(t.head, t.tail) + +extension [A](self: A) inline def --> [B](y: B): (A, B) = (self, y) +extension [A](inline self: A) inline def ---> [B](inline y: B): (A, B) = (self, y) + +object +: { + def unapply[A, CC[_] <: Seq[?], C <: SeqOps[A, CC, C]](t: (C & SeqOps[A, CC, C])^): (A, C^{t})? = + if(t.isEmpty) 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) 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..ad60182f2ce7 --- /dev/null +++ b/tests/pos/StringContext-maybe.scala @@ -0,0 +1,490 @@ +//> using options -Yexplicit-nulls +/* + * Scala (https://www.scala-lang.org) + * + * Copyright EPFL and Lightbend, Inc. + * + * Licensed under Apache License 2.0 + * (http://www.apache.org/licenses/LICENSE-2.0). + * + * See the NOTICE file distributed with this work for + * additional information regarding copyright ownership. + */ + +package scala + +import language.experimental.magic +import java.lang.StringBuilder as JLSBuilder +import scala.annotation.tailrec + +/** This class provides the basic mechanism to do String Interpolation. + * String Interpolation allows users + * to embed variable references directly in *processed* string literals. + * Here's an example: + * {{{ + * val name = "James" + * println(s"Hello, \$name") // Hello, James + * }}} + * + * Any processed string literal is rewritten as an instantiation and + * method call against this class. For example: + * {{{ + * s"Hello, \$name" + * }}} + * + * is rewritten to be: + * + * {{{ + * StringContext("Hello, ", "").s(name) + * }}} + * + * By default, this class provides the `raw`, `s` and `f` methods as + * available interpolators. + * + * To provide your own string interpolator, create an implicit class + * which adds a method to `StringContext`. Here's an example: + * {{{ + * implicit class JsonHelper(private val sc: StringContext) extends AnyVal { + * def json(args: Any*): JSONObject = ... + * } + * val x: JSONObject = json"{ a: \$a }" + * }}} + * + * Here the `JsonHelper` extension class implicitly adds the `json` method to + * `StringContext` which can be used for `json` string literals. + * + * @param parts The parts that make up the interpolated string, + * without the expressions that get inserted by interpolation. + */ +case class StringContext(parts: String*) { + + import StringContext.{checkLengths as scCheckLengths, glob, standardInterpolator as scStandardInterpolator} + + @deprecated("use same-named method on StringContext companion object", "2.13.0") + def checkLengths(args: scala.collection.Seq[Any]): Unit = scCheckLengths(args, parts) + + /** The simple string interpolator. + * + * It inserts its arguments between corresponding parts of the string context. + * It also treats standard escape sequences as defined in the Scala specification. + * Here's an example of usage: + * {{{ + * val name = "James" + * println(s"Hello, \$name") // Hello, James + * }}} + * In this example, the expression \$name is replaced with the `toString` of the + * variable `name`. + * The `s` interpolator can take the `toString` of any arbitrary expression within + * a `\${}` block, for example: + * {{{ + * println(s"1 + 1 = \${1 + 1}") + * }}} + * will print the string `1 + 1 = 2`. + * + * @param `args` The arguments to be inserted into the resulting string. + * @throws IllegalArgumentException + * if the number of `parts` in the enclosing `StringContext` does not exceed + * the number of arguments `arg` by exactly 1. + * @throws StringContext.InvalidEscapeException + * if a `parts` string contains a backslash (`\`) character + * that does not start a valid escape sequence. + * @note The Scala compiler may replace a call to this method with an equivalent, but more efficient, + * use of a StringBuilder. + */ + def s(args: Any*): String = ??? // fasttracked to scala.tools.reflect.FastStringInterpolator::interpolateS + object s { + /** The simple string matcher. + * + * Attempts to match the input string to the given interpolated patterns via + * a naive globbing, that is the reverse of the simple interpolator. + * + * Here is an example usage: + * + * {{{ + * val s"Hello, \$name" = "Hello, James" + * println(name) // "James" + * }}} + * + * In this example, the string "James" ends up matching the location where the pattern + * `\$name` is positioned, and thus ends up bound to that variable. + * + * Multiple matches are supported: + * + * {{{ + * val s"\$greeting, \$name" = "Hello, James" + * println(greeting) // "Hello" + * println(name) // "James" + * }}} + * + * And the `s` matcher can match an arbitrary pattern within the `\${}` block, for example: + * + * {{{ + * val TimeSplitter = "([0-9]+)[.:]([0-9]+)".r + * val s"The time is \${TimeSplitter(hours, mins)}" = "The time is 10.50" + * println(hours) // 10 + * println(mins) // 50 + * }}} + * + * Here, we use the `TimeSplitter` regex within the `s` matcher, further splitting the + * matched string "10.50" into its constituent parts + */ + def unapplySeq(s: String): Seq[String]? = glob(parts, s) + } + /** The raw string interpolator. + * + * It inserts its arguments between corresponding parts of the string context. + * As opposed to the simple string interpolator `s`, this one does not treat + * standard escape sequences as defined in the Scala specification. + * + * For example, the raw processed string `raw"a\nb"` is equal to the scala string `"a\\nb"`. + * + * ''Note:'' Even when using the raw interpolator, Scala will process Unicode escapes. + * Unicode processing in the raw interpolator is deprecated as of scala 2.13.2 and + * will be removed in the future + * For example: + * {{{ + * scala> raw"\u005cu0023" + * res0: String = # + * }}} + * + * @param `args` The arguments to be inserted into the resulting string. + * @throws IllegalArgumentException + * if the number of `parts` in the enclosing `StringContext` does not exceed + * the number of arguments `arg` by exactly 1. + * @note The Scala compiler may replace a call to this method with an equivalent, but more efficient, + * use of a StringBuilder. + */ + def raw(args: Any*): String = ??? // fasttracked to scala.tools.reflect.FastStringInterpolator::interpolateRaw + + @deprecated("Use the static method StringContext.standardInterpolator instead of the instance method", "2.13.0") + def standardInterpolator(process: String => String, args: Seq[Any]): String = scStandardInterpolator(process, args, parts) + + /** The formatted string interpolator. + * + * It inserts its arguments between corresponding parts of the string context. + * It also treats standard escape sequences as defined in the Scala specification. + * Finally, if an interpolated expression is followed by a `parts` string + * that starts with a formatting specifier, the expression is formatted according to that + * specifier. All specifiers allowed in Java format strings are handled, and in the same + * way they are treated in Java. + * + * For example: + * {{{ + * val height = 1.9d + * val name = "James" + * println(f"\$name%s is \$height%2.2f meters tall") // James is 1.90 meters tall + * }}} + * + * @param `args` The arguments to be inserted into the resulting string. + * @throws IllegalArgumentException + * if the number of `parts` in the enclosing `StringContext` does not exceed + * the number of arguments `arg` by exactly 1. + * @throws StringContext.InvalidEscapeException + * if a `parts` string contains a backslash (`\`) character + * that does not start a valid escape sequence. + * + * Note: The `f` method works by assembling a format string from all the `parts` strings and using + * `java.lang.String.format` to format all arguments with that format string. The format string is + * obtained by concatenating all `parts` strings, and performing two transformations: + * + * 1. Let a _formatting position_ be a start of any `parts` string except the first one. + * If a formatting position does not refer to a `%` character (which is assumed to + * start a format specifier), then the string format specifier `%s` is inserted. + * + * 2. Any `%` characters not in formatting positions must begin one of the conversions + * `%%` (the literal percent) or `%n` (the platform-specific line separator). + */ + def f[A >: Any](args: A*): String = ??? // fasttracked to scala.tools.reflect.FormatInterpolator::interpolateF +} + +object StringContext { + /** + * Linear time glob-matching implementation. + * Adapted from https://research.swtch.com/glob + * + * @param patternChunks The non-wildcard portions of the input pattern, + * separated by wildcards + * @param input The input you wish to match against + * @return None if there is no match, Some containing the sequence of matched + * wildcard strings if there is a match + */ + def glob(patternChunks: Seq[String], input: String): Seq[String]? = { + var patternIndex = 0 + var inputIndex = 0 + var nextPatternIndex = 0 + var nextInputIndex = 0 + + val numWildcards = patternChunks.length - 1 + val matchStarts = Array.fill(numWildcards)(-1) + val matchEnds = Array.fill(numWildcards)(-1) + + val nameLength = input.length + // The final pattern is as long as all the chunks, separated by 1-character + // glob-wildcard placeholders + val patternLength = { + var n = numWildcards + for(chunk <- patternChunks) { + 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) { + if (first) first = false + else { + arr(i) = -1 + i += 1 + } + for(c <- chunk) { + 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) { + if (j < numWildcards) { + i += chunk.length + arr(i) = j + i += 1 + j += 1 + } + } + arr + } + + while(patternIndex < patternLength || inputIndex < nameLength) { + 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) { + 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) { + patternIndex += 1 + inputIndex += 1 + true + } else { + false + } + } + } else false + + // Mismatch. Maybe restart. + if (!continue) { + if (0 < nextInputIndex && nextInputIndex <= nameLength) { + 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) "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[this] 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) { + val usRead = uindex - startindex + val digitsRead = dindex + (codepoint.asInstanceOf[Char], usRead + digitsRead) + } + else if (dindex + uindex >= len) + throw new InvalidUnicodeEscapeException(src, startindex, uindex + dindex) + else { + val ch = src(dindex + uindex) + val e = ch.asDigit + if(e >= 0 && e <= 15) loopCP(dindex + 1, (codepoint << 4) + e) + else throw new InvalidUnicodeEscapeException(src, startindex, uindex + dindex) + } + } + if(uindex >= len) 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') 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[this] 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) { + //require(str(next) == '\\') + if (next > i) b.append(str, i, next) + var idx = next + 1 + if (idx >= len) 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') readUEscape(str, idx) + else (c, 1) + idx += advance + b append ch + loop(idx, str.indexOf('\\', idx)) + } else { + if (i < len) b.append(str, i, len) + b.toString + } + } + loop(0, first) + } + + //replace escapes with given first escape + private[this] 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) { + //require(str(next) == '\\') + if (next > i) b.append(str, i, next) + var idx = next + 1 + if (idx >= len) { + if (idx == len) 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) 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) { + 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) + throw new IllegalArgumentException("wrong number of arguments ("+ args.length + +") for interpolated string with "+ parts.length +" parts") + +} diff --git a/tests/pos/byname-implicits-8-maybe.scala b/tests/pos/byname-implicits-8-maybe.scala new file mode 100644 index 000000000000..d05a89a7f9d8 --- /dev/null +++ b/tests/pos/byname-implicits-8-maybe.scala @@ -0,0 +1,34 @@ +//> using options -Yexplicit-nulls +// shapeless's Lazy implemented in terms of byname implicits +import language.experimental.magic +import scala.magic.* +trait Lazy[T] { + lazy val value: T +} + +object Lazy { + implicit def apply[T](implicit t: => T): Lazy[T] = + new Lazy[T] { + lazy val value = t + } + + def unapply[T](lt: Lazy[T]): T? = Ok(lt.value) +} + +trait Foo { + type Out + def out: Out +} + +object Foo { + type Aux[Out0] = Foo { type Out = Out0 } + + implicit val fooInt: Aux[Int] = new Foo { type Out = Int ; def out = 23 } +} + +object Test { + def bar[T](t: T)(implicit foo: Lazy[Foo.Aux[T]]): T = foo.value.out + + val i = bar(13) + i: Int +} diff --git a/tests/pos/extractor-types-maybe.scala b/tests/pos/extractor-types-maybe.scala new file mode 100644 index 000000000000..aaff8098dbe6 --- /dev/null +++ b/tests/pos/extractor-types-maybe.scala @@ -0,0 +1,32 @@ +//> using options -Yexplicit-nulls +import language.experimental.magic +package p1 { + object Ex { def unapply(p: Any): (_ <: Int)? = null } + object Foo { val Ex(_) = null } +} +// a.scala:2: error: error during expansion of this match (this is a scalac bug). +// The underlying error was: type mismatch; +// found : Some[_$1(in value x$1)] where type _$1(in value x$1) +// required: Some[_$1(in method unapply)] +// object Foo { val Ex(_) = null } +// ^ +// one error found + +package p2 { + trait Other { + class Quux + object Baz { def unapply(x: Any): Quux? = null } + } + trait Reifiers { + def f(): Unit = { + val u2: Other = ??? + (null: Any) match { case u2.Baz(x) => println(x) } //: u2.Quux) } + // The underlying error was: type mismatch; + // found : Other#Quux + // required: u2.Quux + // x match { case u2.Baz(x) => println(x: u2.Quux) } + // ^ + // one error found + } + } +} diff --git a/tests/pos/first-class-patterns-maybe.scala b/tests/pos/first-class-patterns-maybe.scala new file mode 100644 index 000000000000..5ddd5510c4bd --- /dev/null +++ b/tests/pos/first-class-patterns-maybe.scala @@ -0,0 +1,25 @@ +//> using options -Yexplicit-nulls + + // Trait of all extractors with unapply methods +import language.experimental.magic + trait Matcher[A, B]: + def unapply(x: A): B? + + // An extractor defined by an unappy method + object Even extends Matcher[Int, Int]: + def unapply(x: Int): Int? = + if x % 2 == 0 then x else null + + // Method using a given extractor in pattern position + def collect[A, B](xs: List[A], m: Matcher[A, B]): List[B] = + xs match + case Nil => Nil + case m(x) :: xs1 => x :: collect(xs1, m) + case _ :: xs1 => collect(xs1, m) + + @main def test = + val xs = List(1, 2, 3, 4) + val ys = collect(xs, Even) + println(ys) + + diff --git a/tests/pos/i1318-maybe.scala b/tests/pos/i1318-maybe.scala new file mode 100644 index 000000000000..5a4c6ecd5edd --- /dev/null +++ b/tests/pos/i1318-maybe.scala @@ -0,0 +1,40 @@ +//> using options -Yexplicit-nulls +import language.experimental.magic +object Foo { + class S(i: Int) + case class T(i: Int) extends S(i) + + object T { + def unapply(s: S): (Int, Int)? = (5, 6) + // def unapply(o: Object): (Int, Int, Int)? = (5, 6, 7) + } + + val s = new S(5) + + s match { + // case T(x, y, z) => println(x + y + z) + case T(x, y) => println(x + y) + case T(x) => println(x) + case _ => println("not match") + } +} + +object Bar { + case class T(i: Int) + class S(i: Int) extends T(i) + + object T { + def unapply(s: S): (Int, Int)? = (5, 6) + // def unapply(o: Object): (Int, Int, Int)? = (5, 6, 7) + } + + val s = new S(5) + + s match { + // case T(x, y, z) => println(x + y + z) + case T(x, y) => println(x + y) + case T(x) => println(x) + case _ => println("not match") + } +} + diff --git a/tests/pos/i14896-maybe.scala b/tests/pos/i14896-maybe.scala new file mode 100644 index 000000000000..60c35ed70e66 --- /dev/null +++ b/tests/pos/i14896-maybe.scala @@ -0,0 +1,4 @@ +//> using options -Yexplicit-nulls +import language.experimental.magic +object Ex { def unapply(p: Any): (_ <: Int)? = null } +object Foo { val Ex(_) = null: @unchecked } \ No newline at end of file diff --git a/tests/pos/i15188-maybe.scala b/tests/pos/i15188-maybe.scala new file mode 100644 index 000000000000..f5b5140e9875 --- /dev/null +++ b/tests/pos/i15188-maybe.scala @@ -0,0 +1,11 @@ +//> using options -Yexplicit-nulls +import language.experimental.magic +object O + +extension [T] (ctx: O.type) inline def unapplySeq(input: T): Seq[T]? = Seq(input) + +@main +def Main = { + val O(x) = 3 + println(s"x: $x") +} diff --git a/tests/pos/i15188b-maybe.scala b/tests/pos/i15188b-maybe.scala new file mode 100644 index 000000000000..0189dab64640 --- /dev/null +++ b/tests/pos/i15188b-maybe.scala @@ -0,0 +1,10 @@ +//> using options -Yexplicit-nulls +import language.experimental.magic +class C + +extension (ctx: C) inline def unapply(input: String): String? = "hi" + +@main def run = { + val O = new C + val O(x) = "3" +} diff --git a/tests/pos/i17525-maybe.scala b/tests/pos/i17525-maybe.scala new file mode 100644 index 000000000000..39844e08d30f --- /dev/null +++ b/tests/pos/i17525-maybe.scala @@ -0,0 +1,7 @@ +//> using options -Yexplicit-nulls +import language.experimental.magic +object Extract { + transparent inline def unapply(value: String): Tuple? = (1, "two") +} +def fail(): Unit = "" match { case Extract(a, b) => f(a, b) } +def f(n: Int, s: String): Unit = () diff --git a/tests/pos/i1793-maybe.scala b/tests/pos/i1793-maybe.scala new file mode 100644 index 000000000000..4a266450b534 --- /dev/null +++ b/tests/pos/i1793-maybe.scala @@ -0,0 +1,9 @@ +//> using options -Yexplicit-nulls +import language.experimental.magic +object Test { + import scala.ref.WeakReference + def unapply[T <: AnyRef](wr: WeakReference[T]): T? = { + val x: T = wr.underlying.get + if (x != null) x else null + } +} diff --git a/tests/pos/i18175-maybe.scala b/tests/pos/i18175-maybe.scala new file mode 100644 index 000000000000..6516c9ae021e --- /dev/null +++ b/tests/pos/i18175-maybe.scala @@ -0,0 +1,108 @@ +//> using options -Yexplicit-nulls +import language.experimental.magic +import scala.compiletime.ops.int.{ +, -, Max } +import scala.compiletime.ops.string.{ Substring, Length, Matches, CharAt } + +class Regex[P] private() extends Serializable: + def unapply(s: CharSequence)(implicit n: Regex.Sanitizer[P]): P? = ??? + +object Regex: + def apply[R <: String & Singleton](regex: R): Regex[Compile[R]] = ??? + + abstract class Sanitizer[T] + object Sanitizer: + given Sanitizer[EmptyTuple] = ??? + given stringcase: [T <: Tuple: Sanitizer] => Sanitizer[String *: T] = ??? + given optioncase: [T <: Tuple: Sanitizer] => Sanitizer[Option[String] *: T] = ??? + given Sanitizer[String] = ??? + given Sanitizer[Option[String]] = ??? + + type Compile[R <: String] = Matches["", R] match + case _ => Reverse[EmptyTuple, Loop[R, 0, Length[R], EmptyTuple, IsPiped[R, 0, Length[R], 0]]] + + type Loop[R <: String, Lo <: Int, Hi <: Int, Acc <: Tuple, Opt <: Int] <: Tuple = Lo match + case Hi => Acc + case _ => CharAt[R, Lo] match + case '\\' => CharAt[R, Lo + 1] match + case 'Q' => Loop[R, ToClosingQE[R, Lo + 2], Hi, Acc, Opt] + case _ => Loop[R, Lo + 2, Hi, Acc, Opt] + case '[' => Loop[R, ToClosingBracket[R, Lo + 1, 0], Hi, Acc, Opt] + case ')' => Loop[R, Lo + 1, Hi, Acc, Max[0, Opt - 1]] + case '(' => Opt match + case 0 => IsMarked[R, ToClosingParenthesis[R, Lo + 1, 0], Hi] match + case true => IsCapturing[R, Lo + 1] match + case false => Loop[R, Lo + 1, Hi, Acc, 1] + case true => Loop[R, Lo + 1, Hi, Option[String] *: Acc, 1] + case false => IsCapturing[R, Lo + 1] match + case false => Loop[R, Lo + 1, Hi, Acc, IsPiped[R, Lo + 1, Hi, 0]] + case true => Loop[R, Lo + 1, Hi, String *: Acc, IsPiped[R, Lo + 1, Hi, 0]] + case _ => IsCapturing[R, Lo + 1] match + case false => Loop[R, Lo + 1, Hi, Acc, Opt + 1] + case true => Loop[R, Lo + 1, Hi, Option[String] *: Acc, Opt + 1] + case _ => Loop[R, Lo + 1, Hi, Acc, Opt] + + type IsCapturing[R <: String, At <: Int] <: Boolean = CharAt[R, At] match + case '?' => CharAt[R, At + 1] match + case '<' => CharAt[R, At + 2] match + case '=' | '!' => false + case _ => true + case _ => false + case _ => true + + type IsMarked[R <: String, At <: Int, Hi <: Int] <: Boolean = At match + case Hi => false + case _ => CharAt[R, At] match + case '?' | '*' => true + case '{' => CharAt[R, At + 1] match + case '0' => true + case _ => false + case _ => false + + type IsPiped[R <: String, At <: Int, Hi <: Int, Lvl <: Int] <: Int = At match + case Hi => 0 + case _ => CharAt[R, At] match + case '\\' => CharAt[R, At + 1] match + case 'Q' => IsPiped[R, ToClosingQE[R, At + 2], Hi, Lvl] + case _ => IsPiped[R, At + 2, Hi, Lvl] + case '[' => IsPiped[R, ToClosingBracket[R, At + 1, 0], Hi, Lvl] + case '(' => IsPiped[R, ToClosingParenthesis[R, At + 1, 0], Hi, Lvl + 1] + case '|' => 1 + case ')' => 0 + case _ => IsPiped[R, At + 1, Hi, Lvl] + + type ToClosingParenthesis[R <: String, At <: Int, Lvl <: Int] <: Int = CharAt[R, At] match + case '\\' => CharAt[R, At + 1] match + case 'Q' => ToClosingParenthesis[R, ToClosingQE[R, At + 2], Lvl] + case _ => ToClosingParenthesis[R, At + 2, Lvl] + case '[' => ToClosingParenthesis[R, ToClosingBracket[R, At + 1, 0], Lvl] + case ')' => Lvl match + case 0 => At + 1 + case _ => ToClosingParenthesis[R, At + 1, Lvl - 1] + case '(' => ToClosingParenthesis[R, At + 1, Lvl + 1] + case _ => ToClosingParenthesis[R, At + 1, Lvl] + + type ToClosingBracket[R <: String, At <: Int, Lvl <: Int] <: Int = CharAt[R, At] match + case '\\' => CharAt[R, At + 1] match + case 'Q' => ToClosingBracket[R, ToClosingQE[R, At + 2], Lvl] + case _ => ToClosingBracket[R, At + 2, Lvl] + case '[' => ToClosingBracket[R, At + 1, Lvl + 1] + case ']' => Lvl match + case 0 => At + 1 + case _ => ToClosingBracket[R, At + 1, Lvl - 1] + case _ => ToClosingBracket[R, At + 1, Lvl] + + type ToClosingQE[R <: String, At <: Int] <: Int = CharAt[R, At] match + case '\\' => CharAt[R, At + 1] match + case 'E' => At + 2 + case _ => ToClosingQE[R, At + 2] + case _ => ToClosingQE[R, At + 1] + + type Reverse[Acc <: Tuple, X <: Tuple] <: Tuple = X match + case x *: xs => Reverse[x *: Acc, xs] + case EmptyTuple => Acc + +object Test: + def main(args: Array[String]): Unit = + val r75 = Regex("(x|y|z[QW])*(longish|loquatious|excessive|overblown[QW])*") + "xyzQzWlongishoverblownW" match + case r75((Some(g0), Some(g1))) => ??? // failure diff --git a/tests/pos/i18601-maybe.scala b/tests/pos/i18601-maybe.scala new file mode 100644 index 000000000000..4ddfcf339151 --- /dev/null +++ b/tests/pos/i18601-maybe.scala @@ -0,0 +1,20 @@ +//> using options -Werror -Yexplicit-nulls +import language.experimental.magic +extension (sc: StringContext) + def m: StringContext = sc + def unapply(string: String): String? = + val pattern = sc.parts.head + if string.length == pattern.length then string else null + +class Test: + def parse(x: PartialFunction[String, String]) = x + + val pf = parse { + case m"x$s" => s + case m"xx$s" => s // was: unreachable + } + + // proof that the second case isn't unreachable (matches "ab") + def t1 = pf.applyOrElse("a", _ => ".") // "a" + def t2 = pf.applyOrElse("ab", _ => ".") // "ab" + def t3 = pf.applyOrElse("abc", _ => ".") // "." diff --git a/tests/pos/i18601b-maybe.scala b/tests/pos/i18601b-maybe.scala new file mode 100644 index 000000000000..08d4069be7f9 --- /dev/null +++ b/tests/pos/i18601b-maybe.scala @@ -0,0 +1,27 @@ +//> using options -Werror -Yexplicit-nulls + +// like pos/i18601 +// but with a dedicated SC class +// that made the false positive redundancy warning go away + +import language.experimental.magic +extension (sc: StringContext) + def m: SC = SC(sc) + +class SC(sc: StringContext): + 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: not unreachable (as a counter-example) + } + + // proof that the second case isn't unreachable (matches "ab") + def t1 = pf.applyOrElse("a", _ => ".") // "a" + def t2 = pf.applyOrElse("ab", _ => ".") // "ab" + def t3 = pf.applyOrElse("abc", _ => ".") // "." diff --git a/tests/pos/i20107-maybe.scala b/tests/pos/i20107-maybe.scala new file mode 100644 index 000000000000..f13204cef044 --- /dev/null +++ b/tests/pos/i20107-maybe.scala @@ -0,0 +1,9 @@ +//> using options -Yexplicit-nulls +import language.experimental.magic +import scala.magic.* +object foo: + transparent inline def unapply[F](e: F): F? = Ok(e.asInstanceOf[F]) + +class A: + def test(x: Int) = x match + case foo(e) => e diff --git a/tests/pos/i2104-maybe.scala b/tests/pos/i2104-maybe.scala new file mode 100644 index 000000000000..6687278d2efd --- /dev/null +++ b/tests/pos/i2104-maybe.scala @@ -0,0 +1,22 @@ +//> using options -Yexplicit-nulls +import language.experimental.magic +case class Pair[A, B](_1: A, _2: B) + +trait Cons[+H, +T] + +object Cons { + def apply[H, T](h: H, t: T): Cons[H, T] = ??? + def unapply[H, T](t: Cons[H, T]): Pair[H, T]? = ??? +} + + + +object Test { + def main(args: Array[String]): Unit = { + Cons(Option(1), None) match { + case Cons(Some(i), None) => + i: Int // error: found: Any(i), requires: Int + assert(i == 1) + } + } +} diff --git a/tests/pos/i2104b-maybe.scala b/tests/pos/i2104b-maybe.scala new file mode 100644 index 000000000000..fa6dfda0cf37 --- /dev/null +++ b/tests/pos/i2104b-maybe.scala @@ -0,0 +1,18 @@ +//> using options -Yexplicit-nulls +import language.experimental.magic +case class Pair[A, B](_1: A, _2: B) + +trait Cons[+H, +T] + +object Cons { + def apply[H, T](h: H, t: T): Cons[H, T] = ??? + def unapply[H, T](t: Cons[H, T]): Pair[H, T]? = ??? +} + +object Test { + def main(args: Array[String]): Unit = { + Cons(Option(1), None) match { + case Cons(Some(i), None) => + } + } +} diff --git a/tests/pos/i23022-maybe.scala b/tests/pos/i23022-maybe.scala new file mode 100644 index 000000000000..b94bc92bc00c --- /dev/null +++ b/tests/pos/i23022-maybe.scala @@ -0,0 +1,14 @@ +//> using options -Yexplicit-nulls +import language.experimental.magic +trait ExtractorWithImplicit: + + object Yikes: + def unapply(implicit M: String): Any? = ??? + + def expand: Any = + given String = "Hey" + "Wut" match + case Yikes(_) => ??? + case _ => ??? + + diff --git a/tests/pos/i23459-maybe.scala b/tests/pos/i23459-maybe.scala new file mode 100644 index 000000000000..46e1684ce81e --- /dev/null +++ b/tests/pos/i23459-maybe.scala @@ -0,0 +1,24 @@ +//> using options -Yexplicit-nulls +import language.experimental.magic +object TTest: + def unapplySeq(t: Int): Seq[Int]? = Seq(1, 2) + +case class Varargs(xs: Int*) + +def test = + 1 match + case TTest(x*) => () + + 1 match + case TTest(_*) => () + + 1 match + case TTest(1, rest*) => () + case TTest(_*) => () + + Varargs(1, 2, 3) match + case Varargs(x*) => () + + Varargs(1, 2, 3) match + case Varargs(1, rest*) => () + case Varargs(_*) => () diff --git a/tests/pos/i25663-maybe.scala b/tests/pos/i25663-maybe.scala new file mode 100644 index 000000000000..139ce5731905 --- /dev/null +++ b/tests/pos/i25663-maybe.scala @@ -0,0 +1,7 @@ +//> using options -Yexplicit-nulls +import language.experimental.magic +object Foo: + def unapplySeq(f: Int): (String *: Seq[Int] *: EmptyTuple)? = ??? + +def foo(f: Int) = f match + case Foo(name, ns*) => ??? diff --git a/tests/pos/i6621-maybe.scala b/tests/pos/i6621-maybe.scala new file mode 100644 index 000000000000..5ed3ada1a6af --- /dev/null +++ b/tests/pos/i6621-maybe.scala @@ -0,0 +1,11 @@ +//> using options -Werror -deprecation -feature -Yexplicit-nulls + +import language.experimental.magic +object Unapply { + def unapply(a: Any): (Int, Int)? = + (1, 2) +} + +object Test { + val Unapply(x, y) = "": @unchecked +} diff --git a/tests/pos/i8083-maybe.scala b/tests/pos/i8083-maybe.scala new file mode 100644 index 000000000000..775bcbc675c9 --- /dev/null +++ b/tests/pos/i8083-maybe.scala @@ -0,0 +1,28 @@ +//> using options -Yexplicit-nulls +import language.experimental.magic +trait A { + class B +} +abstract class Test2 { val a: A; val b: a.B } +object Test2 { + def unapply(that: Test2): (that.a.type, that.a.B)? = (that.a, that.b) +} +object anA extends A +object Test extends App { + val t: Test2 { val a: anA.type; val b: anA.B } = new Test2 { val a: anA.type = anA; val b = new a.B } + t match { + case Test2(u, v) => + u: A + u: t.a.type // error + v: t.a.B // error + } +} +object Test1 extends App { + object t extends Test2 { val a = anA; val b = new a.B } + t match { + case Test2(u, v) => + u: A + u: t.a.type // error + v: t.a.B // error + } +} \ No newline at end of file diff --git a/tests/pos/i8530-maybe.scala b/tests/pos/i8530-maybe.scala new file mode 100644 index 000000000000..5be76b1be05d --- /dev/null +++ b/tests/pos/i8530-maybe.scala @@ -0,0 +1,29 @@ +//> using options -Yexplicit-nulls +import language.experimental.magic +import scala.magic.* +object MyBoooleanUnapply: + inline def unapply(x: Int): Boolean = true + +object MyOptionUnapply: + inline def unapply(x: Int): Long? = x + +object MyUnapplyImplicits: + inline def unapply(x: Int)(using DummyImplicit): Long? = x + +object MyPolyUnapply: + inline def unapply[T](x: T): T? = Ok(x) + +object MySeqUnapply: + inline def unapplySeq(x: Int): Seq[Int] = Seq(x, x) + +object MyWhiteboxUnapply: + transparent inline def unapply(x: Int): Any? = Ok(x) + +def test: Unit = + val x = 5 match + case MyBoooleanUnapply() => + case MyOptionUnapply(y) => y: Long + case MyUnapplyImplicits(y) => y: Long + case MyPolyUnapply(a) => a: Int + case MySeqUnapply(a, b) => (a: Int, b: Int) + case MyWhiteboxUnapply(x) => x: Int diff --git a/tests/pos/i8577-maybe.scala b/tests/pos/i8577-maybe.scala new file mode 100644 index 000000000000..30576b9162ba --- /dev/null +++ b/tests/pos/i8577-maybe.scala @@ -0,0 +1,23 @@ +//> using options -Yexplicit-nulls +package i8577 + +import language.experimental.magic +type A; given A: A = ???; +type B; given B: B = ???; +type C; given C: C = ???; +type D; given D: D = ???; +type E; given E: E = ???; +type F; given F: F = ???; + + +object Macro: + opaque type StrCtx = StringContext + def apply(ctx: StringContext): StrCtx = ctx + def unapply(ctx: StrCtx): StringContext? = ctx + +def main: Unit = + extension (ctx: StringContext) def mac: Macro.StrCtx = Macro(ctx) + extension [T] (using A)(inline ctx: Macro.StrCtx)(using B) inline def unapplySeq[U](using C)(inline input: T)(using D)(using F): Seq[T]? = ??? + + (??? : Int) match + case mac"${x}" => 1 diff --git a/tests/pos/i8972-maybe.scala b/tests/pos/i8972-maybe.scala new file mode 100644 index 000000000000..aa8083de4ba3 --- /dev/null +++ b/tests/pos/i8972-maybe.scala @@ -0,0 +1,11 @@ +//> using options -Yexplicit-nulls +import language.experimental.magic +trait Num: + type Nat + +object IsInt: + def unapply(using num: Num)(sc: num.Nat): Int? = ??? + +def test(using num: Num)(x: num.Nat) = + x match + case IsInt(i) => diff --git a/tests/pos/i8997-maybe.scala b/tests/pos/i8997-maybe.scala new file mode 100644 index 000000000000..f33ce0e2a9c4 --- /dev/null +++ b/tests/pos/i8997-maybe.scala @@ -0,0 +1,9 @@ +//> using options -Yexplicit-nulls +import language.experimental.magic +object Foo: + def unapply(n: Int)(using x: DummyImplicit)(using y: Int): Int? = ??? + +def test = + given Int = 3 + 1 match + case Foo(_) => diff --git a/tests/pos/inline-i1773-maybe.scala b/tests/pos/inline-i1773-maybe.scala new file mode 100644 index 000000000000..a462f2498fd6 --- /dev/null +++ b/tests/pos/inline-i1773-maybe.scala @@ -0,0 +1,16 @@ +//> using options -Yexplicit-nulls +import language.experimental.magic +object Test { + implicit class Foo(sc: StringContext) { + object q { + def unapply(arg: Any): (Any, Any)? = + (sc.parts(0), sc.parts(1)) + } + } + + def main(args: Array[String]): Unit = { + val q"class $name extends $parent" = new Object + println(name) + println(parent) + } +} diff --git a/tests/pos/inline-unapply-maybe.scala b/tests/pos/inline-unapply-maybe.scala new file mode 100644 index 000000000000..7123d6fc62f3 --- /dev/null +++ b/tests/pos/inline-unapply-maybe.scala @@ -0,0 +1,17 @@ +//> using options -Yexplicit-nulls +import language.experimental.magic +object Test { + + class C(val x: Int, val y: Int) + + inline def unapply(c: C): Some[(Int, Int)] = Some((c.x, c.y)) + +} +object Test2 { + + class C(x: Int, y: Int) + + inline def unapply(c: C): (Int, Int)? = inline c match { + case x: C => (1, 1) + } +} diff --git a/tests/pos/maybe-conversions.scala b/tests/pos/maybe-conversions.scala index 63851fe99173..133fedc93060 100644 --- a/tests/pos/maybe-conversions.scala +++ b/tests/pos/maybe-conversions.scala @@ -22,9 +22,3 @@ def toEither[T, E](x: T ? E): Either[E, T] = x match def toEitherIntStr[T, E](x: Int ? String): Either[String, Int] = x match case Ok(y) => Right(y) case Err(e) => Left(e) - -def toEitherIntStrBAD[T, E](x: Int ? String): Either[String, Int] = x match - case Ok(y) => Right(y) // warn - -def toEitherIntStrBAD2[T, E](x: Int ? String): Either[String, Int] = x match - case Err(y) => Left(y) // warn diff --git a/tests/pos/misc-unapply_pos-maybe.scala b/tests/pos/misc-unapply_pos-maybe.scala new file mode 100644 index 000000000000..90794e3dcdc1 --- /dev/null +++ b/tests/pos/misc-unapply_pos-maybe.scala @@ -0,0 +1,29 @@ +//> using options -Yexplicit-nulls +import language.experimental.magic +object Test { + val xs = List(1) + val f: Int = { + xs match { + case List(x) => x + } + } +} + +// the following comes from ticket #230 +trait Foo { + def name: String + def unapply(x: String): Unit? = { + if (x == name) () else null + } +} +object Bar extends Foo { def name = "bar" } +object Baz extends Foo { def name = "baz" } + +object Test_ { + def matcher(s: String) = s match { + case Bar(x) => println("bar") + case Baz(x) => println("baz") +// ^ +// error: unreachable code + } + } diff --git a/tests/pos/simpleExtractors-1-maybe.scala b/tests/pos/simpleExtractors-1-maybe.scala new file mode 100644 index 000000000000..e47faf0fba21 --- /dev/null +++ b/tests/pos/simpleExtractors-1-maybe.scala @@ -0,0 +1,31 @@ +//> using options -Yexplicit-nulls +import language.experimental.magic +import scala.magic.* +class Foo { + def bar(x: Any): Unit = x match { + case Bar(a) => println(a) + case BarSeq(a) => println(a) + case BarSeq(a, b) => println(a) + } + def baz(x: Any): Unit = x match { + case Baz(a) => println(a) + case BazSeq(a) => println(a) + case BazSeq(a, b) => println(a) + } +} + +object Bar { + def unapply(arg: Any): Any? = Ok(arg) +} + +object BarSeq { + def unapplySeq(arg: Any): Seq[Any]? = List(arg) +} + +object Baz { + def unapply[T](arg: T): T? = Ok(arg) +} + +object BazSeq { + def unapplySeq[T](arg: T): Seq[T]? = List(arg) +} diff --git a/tests/pos/strict-pattern-bindings-3.0-migration-maybe.scala b/tests/pos/strict-pattern-bindings-3.0-migration-maybe.scala new file mode 100644 index 000000000000..d2ffb03d896d --- /dev/null +++ b/tests/pos/strict-pattern-bindings-3.0-migration-maybe.scala @@ -0,0 +1,39 @@ +//> using options -Werror -deprecation -feature -Yexplicit-nulls + +// These tests should pass under -Werror with source version less than 3.2 +import language.experimental.magic +import language.`3.0-migration` + +object Test: + // from filtering-fors.scala + val xs: List[AnyRef] = ??? + + for ((x: String) <- xs) do () + for (y@ (x: String) <- xs) do () + for ((x, y) <- xs) do () + + for ((x: String) <- xs if x.isEmpty) do () + for ((x: String) <- xs; y = x) do () + for ((x: String) <- xs; (y, z) <- xs) do () + for (case (x: String) <- xs; (y, z) <- xs) do () + for ((x: String) <- xs; case (y, z) <- xs) do () + + val pairs: List[AnyRef] = List((1, 2), "hello", (3, 4)) + for ((x, y) <- pairs) yield (y, x) + + // from unchecked-patterns.scala + val y :: ys = List(1, 2, 3) + val (1, c) = (1, 2) + val 1 *: cs = 1 *: Tuple() + + val (_: Int | _: AnyRef) = ??? : AnyRef + + val 1 = 2 + + 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 + val Some(s1) = Option(1) diff --git a/tests/pos/strict-pattern-bindings-3.1-maybe.scala b/tests/pos/strict-pattern-bindings-3.1-maybe.scala new file mode 100644 index 000000000000..9ff692cc6b89 --- /dev/null +++ b/tests/pos/strict-pattern-bindings-3.1-maybe.scala @@ -0,0 +1,39 @@ +//> using options -Werror -deprecation -feature -Yexplicit-nulls + +// These tests should pass under -Xfatal-warnings with source version less than 3.2 +import language.experimental.magic +import language.`3.1` + +object Test: + // from filtering-fors.scala + val xs: List[AnyRef] = ??? + + for ((x: String) <- xs) do () + for (y@ (x: String) <- xs) do () + for ((x, y) <- xs) do () + + for ((x: String) <- xs if x.isEmpty) do () + for ((x: String) <- xs; y = x) do () + for ((x: String) <- xs; (y, z) <- xs) do () + for (case (x: String) <- xs; (y, z) <- xs) do () + for ((x: String) <- xs; case (y, z) <- xs) do () + + val pairs: List[AnyRef] = List((1, 2), "hello", (3, 4)) + for ((x, y) <- pairs) yield (y, x) + + // from unchecked-patterns.scala + val y :: ys = List(1, 2, 3) + val (1, c) = (1, 2) + val 1 *: cs = 1 *: Tuple() + + val (_: Int | _: AnyRef) = ??? : AnyRef + + val 1 = 2 + + 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 + val Some(s1) = Option(1) diff --git a/tests/pos/t1048-maybe.scala b/tests/pos/t1048-maybe.scala new file mode 100644 index 000000000000..7f0263d6ec4e --- /dev/null +++ b/tests/pos/t1048-maybe.scala @@ -0,0 +1,16 @@ +//> using options -Yexplicit-nulls +import language.experimental.magic +trait T[U] { + def x: T[_ <: U] +} + +object T { + def unapply[U](t: T[U]): T[_ <: U]? = t.x +} + +object Test { + def f[W](t: T[W]) = t match { + case T(T(_)) => () + } +} + diff --git a/tests/pos/t1260-maybe.scala b/tests/pos/t1260-maybe.scala new file mode 100644 index 000000000000..661338374a24 --- /dev/null +++ b/tests/pos/t1260-maybe.scala @@ -0,0 +1,20 @@ +//> using options -Yexplicit-nulls +import language.experimental.magic +case class Foo(a: String, b: String) + +object Bar { + def unapply(s: String): Long? = + try { s.toLong } catch { case _ => null } +} + +object Test { + def main(args: Array[String]): Unit = { + val f = Foo("1", "2") + f match { + case Foo(Bar(1), Bar(2)) => 1 + case Foo(Bar(i), Bar(j)) if i >= 0 => 2 + case _ => 3 + } + } +} + diff --git a/tests/pos/t3136-maybe.scala b/tests/pos/t3136-maybe.scala new file mode 100644 index 000000000000..093ef28e8732 --- /dev/null +++ b/tests/pos/t3136-maybe.scala @@ -0,0 +1,21 @@ +//> using options -Yexplicit-nulls +import language.experimental.magic +class Type +class Symbol +case class PolyType(tps: List[Symbol], res: Type) extends Type +class OtherType extends Type + +// case class NullaryMethodType(tp: Type) extends Type + +object NullaryMethodType { + def apply(resTpe: Type): Type = PolyType(List(), resTpe) + def unapply(tp: Type): (Type)? = null +} + +object Test { + def TEST(tp: Type): String = + tp match { + case PolyType(ps1, PolyType(ps2, res @ PolyType(a, b))) => "1" + tp // couldn't find a simpler version that still crashes + case NullaryMethodType(meh) => "2" + meh + } +} diff --git a/tests/pos/t5041-maybe.scala b/tests/pos/t5041-maybe.scala new file mode 100644 index 000000000000..661b6b4a8dc0 --- /dev/null +++ b/tests/pos/t5041-maybe.scala @@ -0,0 +1,11 @@ +//> using options -Yexplicit-nulls +import language.experimental.magic +case class Token(text: String, startIndex: Int) + +object Comment { + def unapply(s: String): Token? = null +} + +object HiddenTokens { + "foo" match { case Comment(_) => } +} diff --git a/tests/pos/t6675-maybe.scala b/tests/pos/t6675-maybe.scala new file mode 100644 index 000000000000..b1db21b66360 --- /dev/null +++ b/tests/pos/t6675-maybe.scala @@ -0,0 +1,23 @@ +//> using options -Yexplicit-nulls +import language.experimental.magic +import scala.magic.* +object LeftOrRight { + def unapply[A](value: Either[A, A]): A? = value match { + case scala.Left(x) => Ok(x) + case scala.Right(x) => Ok(x) + } +} + +object Test { + (Left((0, 0)): Either[(Int, Int), (Int, Int)]) match { + case LeftOrRight(pair @ (a, b)) => a // false -Wshadow warning: "extractor pattern binds a single value to a Product2 of type (Int, Int)" + } + + (Left((0, 0)): Either[(Int, Int), (Int, Int)]) match { + case LeftOrRight((a, b)) => a // false -Wshadow warning: "extractor pattern binds a single value to a Product2 of type (Int, Int)" + } + + (Left((0, 0)): Either[(Int, Int), (Int, Int)]) match { + case LeftOrRight(a, b) => a // false -Wshadow warning: "extractor pattern binds a single value to a Product2 of type (Int, Int)" + } +} diff --git a/tests/pos/t6994-maybe.scala b/tests/pos/t6994-maybe.scala new file mode 100644 index 000000000000..fe9ba9565b0f --- /dev/null +++ b/tests/pos/t6994-maybe.scala @@ -0,0 +1,10 @@ +//> using options -Yexplicit-nulls +import language.experimental.magic +object Test { + object NF { + def unapply(t: Throwable): Throwable? = null + } + val x = (try { None } catch { case NF(ex) => None }) getOrElse 0 + // Was emitting a spurious warning post typer: + // "This catches all Throwables. If this is really intended, use `case ex6 : Throwable` to clear this warning." +} diff --git a/tests/pos/t796-maybe.scala b/tests/pos/t796-maybe.scala new file mode 100644 index 000000000000..8f3876ddc314 --- /dev/null +++ b/tests/pos/t796-maybe.scala @@ -0,0 +1,28 @@ +//> using options -Yexplicit-nulls +/** I know what I am doing is wrong -- since I am about to look into + * this bug, I add a test in pending/pos... however, I am afraid that + * once this bug is fixed, this test case might go into test/pos + * there it adds to the huge number of tiny little test cases. + * + * Ideally, an option in the bugtracking system would automatically + * handle "pos" bugs. + */ +import language.experimental.magic +object Test extends App { + + object Twice { + def apply(x: Int) = x * 2 + def unapply(x: Int): Tuple1[Int]? = + if (x % 2 == 0) Tuple1(x / 2) + else null + } + + def test(x: Int) = x match { + case Twice(y) => "x is two times " + y + case _ => "x is odd" + } + + Console.println(test(3)) + Console.println(test(4)) + +} diff --git a/tests/pos/t8045-maybe.scala b/tests/pos/t8045-maybe.scala new file mode 100644 index 000000000000..2bee57b44a3c --- /dev/null +++ b/tests/pos/t8045-maybe.scala @@ -0,0 +1,19 @@ +//> using options -Yexplicit-nulls +import language.experimental.magic +object Test extends App { + case class Number(i: Int) + + object UnliftNumber { + def unapply(t: Any): Number? = t match { + case i: Int => Number(i) + case _ => null + } + } + + def eval(expr: Any): Option[Number] = expr match { + case UnliftNumber(n) => Some(n) + case _ => None + } + + println(eval(1)) +} diff --git a/tests/pos/t8128-maybe.scala b/tests/pos/t8128-maybe.scala new file mode 100644 index 000000000000..420aade7a992 --- /dev/null +++ b/tests/pos/t8128-maybe.scala @@ -0,0 +1,18 @@ +//> using options -Yexplicit-nulls +import language.experimental.magic +import scala.magic.* +object G { + def unapply(m: Any): Maybe[?, 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/pos/unapplyComplex-maybe.scala b/tests/pos/unapplyComplex-maybe.scala new file mode 100644 index 000000000000..a4950f43a913 --- /dev/null +++ b/tests/pos/unapplyComplex-maybe.scala @@ -0,0 +1,41 @@ +//> using options -Yexplicit-nulls +import language.experimental.magic +trait Complex extends Product2[Double, Double] { + def canEqual(other: Any) = other.isInstanceOf[Complex] +} + +class ComplexRect(val _1: Double, val _2: Double) extends Complex { + override def toString = "ComplexRect("+_1+","+_2+")" +} + +class ComplexPolar(val _1: Double, val _2: Double) extends Complex { + override def toString = "ComplexPolar("+_1+","+_2+")" +} + +object ComplexRect { + def unapply(z:Complex): Complex? = { + if (z.isInstanceOf[ComplexRect]) z else z match { + case ComplexPolar(mod, arg) => + new ComplexRect(mod*math.cos(arg), mod*math.sin(arg)) +} } } + +object ComplexPolar { + def unapply(z:Complex): Complex? = { + if (z.isInstanceOf[ComplexPolar]) z else z match { + case ComplexRect(re,im) => + new ComplexPolar(math.sqrt(re*re + im*im), math.atan(re/im)) +} } } + +object Test { + def main(args:Array[String]) = { + new ComplexRect(1,1) match { + case ComplexPolar(mod,arg) => // z @ ??? + Console.println("mod"+mod+"arg"+arg) + } + val Komplex = ComplexRect + new ComplexPolar(math.sqrt(2),math.Pi / 4.0) match { + case Komplex(re,im) => // z @ ??? + Console.println("re"+re+" im"+im) + } + } +} diff --git a/tests/pos/unapplyVal-maybe.scala b/tests/pos/unapplyVal-maybe.scala new file mode 100644 index 000000000000..d31b2c17656e --- /dev/null +++ b/tests/pos/unapplyVal-maybe.scala @@ -0,0 +1,39 @@ +//> using options -Yexplicit-nulls +package test // bug #1215 + +import language.experimental.magic +class Async { + def unapply(scrut: Any): Any? = null +} + +class Buffer { + val Put = new Async + //case class Put(x: Int) + + def joinPat(x: Any): Unit = { + x match { + case Put => + case Put(y) => + println("returning " + y) + } + } +} + + +object unapplyJoins extends App { // bug #1257 + + class Sync { + def apply(): Int = 42 + def unapply(scrut: Any): Boolean = false + } + + class Buffer { + object Get extends Sync + + val jp: PartialFunction[Any, Any] = { + case Get() => + } + } + + println((new Buffer).jp.isDefinedAt(42)) +} diff --git a/tests/run/LazyLists-maybe.scala b/tests/run/LazyLists-maybe.scala new file mode 100644 index 000000000000..711599697ee5 --- /dev/null +++ b/tests/run/LazyLists-maybe.scala @@ -0,0 +1,112 @@ +//> using options -Yexplicit-nulls +import language.experimental.magic +package xcollections: + import annotation.unchecked.uncheckedVariance + import compiletime.uninitialized + + abstract class LazyList[+T]: + + private var myHead: T = uninitialized + private var myTail: LazyList[T] = uninitialized + private var myForced: LazyList[T] | Null = null + + protected def force(): LazyList[T] + + protected def set(hd: T @uncheckedVariance, tl: LazyList[T] @uncheckedVariance): LazyList[T] = + assert(myForced == null, "implementation error: attempting to re-define existing LazyList") + myHead = hd + myTail = tl + this + + def forced(): LazyList[T] = + var myForced = this.myForced + if myForced == null then + myForced = force() + this.myForced = myForced + assert(myForced.myForced != null, "implementation error: LazyList was not forced") + myForced + else myForced + + def isEmpty: Boolean = forced() eq LazyList.empty + + def head: T = + val e = forced() + require(!e.isEmpty, "head on empty LazyList") + e.myHead + + def tail: LazyList[T] = + val e = forced() + require(!e.isEmpty, "tail on empty LazyList") + e.myTail + + def fromIterable[T](xs: Iterable[T]) = xs match + case xs: LazyList[T] @unchecked => xs + case _ => LazyList.fromIterator(xs.iterator) + + object LazyList: + + val empty: LazyList[Nothing] = new: + protected def force(): LazyList[Nothing] = this + + object `#::`: + def unapply[T](xs: LazyList[T]): (T, LazyList[T])? = + if xs.isEmpty then null + else (xs.head, xs.tail) + + def fromIterator[T](it: Iterator[T]): LazyList[T] = new: + protected def force() = + if it.hasNext then set(it.next, fromIterator (it)) + else empty + + extension [T, U >: T](xs: LazyList[T]) + def #::(x: U): LazyList[U] = new: + protected def force(): LazyList[U] = + set(x, xs) + + def ++(ys: LazyList[U]): LazyList[U] = new: + protected def force() = + if xs.isEmpty then ys.forced() + else set(xs.head, xs.tail ++ ys) + + extension [T, U](xs: LazyList[T]) + def map(f: T => U): LazyList[U] = new: + protected def force() = + if xs.isEmpty then empty + else set(f(xs.head), xs.tail.map(f)) + + def flatMap(f: T => LazyList[U]): LazyList[U] = new: + protected def force(): LazyList[U] = + if xs.isEmpty then empty + else f(xs.head) ++ xs.tail.flatMap(f) + + def foldLeft(z: U)(f: (U, T) => U): U = + if xs.isEmpty then z + else xs.tail.foldLeft(f(z, xs.head))(f) + + extension [T](xs: LazyList[T]) + def filter(p: T => Boolean): LazyList[T] = new: + protected def force(): LazyList[T] = + if xs.isEmpty then empty + else if p(xs.head) then set(xs.head, xs.tail.filter(p)) + else xs.tail.filter(p) + + def take(n: Int): LazyList[T] = + if n <= 0 then empty + else new: + protected def force(): LazyList[T] = + if xs.isEmpty then xs + else set(xs.head, xs.tail.take(n - 1)) + + def drop(n: Int): LazyList[T] = + if n <= 0 then xs + else new: + protected def force(): LazyList[T] = + def advance(xs: LazyList[T], n: Int): LazyList[T] = + if n <= 0 || xs.isEmpty then xs + else advance(xs.tail, n - 1) + advance(xs, n) + + end LazyList +end xcollections + +@main def Test() = () \ No newline at end of file diff --git a/tests/run/Typeable-maybe.check b/tests/run/Typeable-maybe.check new file mode 100644 index 000000000000..671b06e6ab94 --- /dev/null +++ b/tests/run/Typeable-maybe.check @@ -0,0 +1,8 @@ +1 is a Int +List(1, 2, 3) is a List[Int] +List(1, 2, 3) is not a Int +1 is not a List[Int] +a is not a Int +List() is a List[Int] +List(1, a) is not a List[Int] +List(1, 2) is a List[Int] diff --git a/tests/run/Typeable-maybe.scala b/tests/run/Typeable-maybe.scala new file mode 100644 index 000000000000..395f6b4557ea --- /dev/null +++ b/tests/run/Typeable-maybe.scala @@ -0,0 +1,62 @@ +//> using options -Yexplicit-nulls +/** A test that shows how to use shapeless.Typeable in extractors without the + * TypeLevel Scala 4 extensions. + * In essence you have to write + * + * case Typeable.instanceOf[T](x) => + * + * instead of + * + * case Typeable[T](x) + * + * The first idiom would be nice to have but it requires more backtracking + * in Typer that we allow now. Essentially, given + * + * case C[T](x) + * + * it's unclear whether this should expand to `C[T].unapply(x)`, (as it does now) + * or to `C.unapply[T](x)` (which is what TypeLevel Scala 4 did, I believe) + */ +import language.experimental.magic +import scala.magic.* +trait Typeable[T]: + def cast(x: Any): Option[T] + def describe: String + override def toString = s"Typeable[$describe]" + +object Typeable: + def apply[T: Typeable]: Typeable[T] = summon + + class instanceOf[T: Typeable]: + def unapply(x: Any): T? = Typeable[T].cast(x) match + case Some(t) => Ok(t) + case None => null + + given int: Typeable[Int]: + def cast(x: Any): Option[Int] = x match + case x: Int => Some(x) + case _ => None + def describe = "Int" + + given list: [T: Typeable] => Typeable[List[T]]: + def cast(x: Any): Option[List[T]] = x match + case x: List[_] if x.forall(Typeable[T].cast(_).isDefined) => Some(x.asInstanceOf[List[T]]) + case _ => None + def describe = s"List[${Typeable[T].describe}]" +end Typeable + +def testInstance[T: Typeable](x: Any): Unit = + val isa = x match + case Typeable.instanceOf[T](x) => "is a" + case _ => "is not a" + println(s"$x $isa ${Typeable[T].describe}") + +@main def Test() = + testInstance[Int](1) + testInstance[List[Int]](List(1, 2, 3)) + testInstance[Int](List(1, 2, 3)) + testInstance[List[Int]](1) + testInstance[Int]("a") + testInstance[List[Int]](Nil) + testInstance[List[Int]](1 :: "a" :: Nil) + testInstance[List[Int]](1 :: 2 :: Nil) diff --git a/tests/run/fully-abstract-interface-maybe.check b/tests/run/fully-abstract-interface-maybe.check new file mode 100644 index 000000000000..181c6eb0d1ec --- /dev/null +++ b/tests/run/fully-abstract-interface-maybe.check @@ -0,0 +1,29 @@ +CaseClassImplementation +underlying rep: class CaseClassImplementation$Const +1 +test1 OK +1 = 1 +test2 OK +1 = 1 + +underlying rep: class CaseClassImplementation$App +7 +test3 OK +AppliedOp(PlusOp, Const(1), App(MultOp,Const(2),Const(3))) = 7 +test4 OK +AppliedOp(PlusOp, Const(1), App(MultOp,Const(2),Const(3))) = 7 + +ListImplementation +underlying rep: class scala.collection.immutable.$colon$colon +1 +test1 OK +1 = 1 +test2 OK +1 = 1 + +underlying rep: class scala.collection.immutable.$colon$colon +7 +test3 OK +AppliedOp(List(+), List(1), List(List(*), List(2), List(3))) = 7 +test4 OK +AppliedOp(List(+), List(1), List(List(*), List(2), List(3))) = 7 diff --git a/tests/run/fully-abstract-interface-maybe.scala b/tests/run/fully-abstract-interface-maybe.scala new file mode 100644 index 000000000000..79f6bc8fa4c8 --- /dev/null +++ b/tests/run/fully-abstract-interface-maybe.scala @@ -0,0 +1,331 @@ +//> using options -Yexplicit-nulls +import language.experimental.magic +import scala.reflect.ClassTag + +object Test { + def main(args: Array[String]): Unit = { + println("CaseClassImplementation") + testInterface(CaseClassImplementation) + + println() + + println("ListImplementation") + testInterface(ListImplementation) + } + + def testInterface(arithmetic: Arithmetic): Unit = { + import arithmetic.* + val const1 = Constant(1) + println("underlying rep: " + const1.getClass) + println(const1.eval) + + const1 match { + case AppliedOp(_, _, _) => + println("test1 fail") + case c @ Constant(n) => + println("test1 OK") + println(s"$n = ${c.eval}") + } + + const1 match { + case _: AppliedOp => + println("test2 fail") + case c: Constant => + println("test2 OK") + println(s"${c.num} = ${c.eval}") + } + println() + + // 1 + (2 * 3) + val applied = AppliedOp(Op.Puls(), Constant(1), AppliedOp(Op.Mult(), Constant(2), Constant(3))) + + println("underlying rep: " + applied.getClass) + println(applied.eval) + + applied match { + case c @ Constant(n) => + println("test3 fail") + case a @ AppliedOp(op, x, y) => + println("test3 OK") + println(s"AppliedOp($op, $x, $y) = ${a.eval}") + } + + applied match { + case c: Constant => + println("test4 fail") + case a: AppliedOp => + println("test4 OK") + println(s"AppliedOp(${a.op}, ${a.lhs}, ${a.rhs}) = ${a.eval}") + } + + } +} + +abstract class Arithmetic { + + // === Numbers ========================================== + // Represents: + // trait Number + // case class Constant(n: Int) extends Number + // case class AppliedOp(op: Op, lhs: Number, rhs: Number) extends Number + + type Number + implicit def numberClassTag: ClassTag[Number] + + trait AbstractNumber { + def thisNumber: Number + def eval: Int = thisNumber match { + case Constant(n) => n + case AppliedOp(op, x, y) => op(x, y) + } + } + implicit def NumberDeco(t: Number): AbstractNumber + + // --- Constant ---------------------------------------- + + type Constant <: Number + implicit def constantClassTag: ClassTag[Constant] + + val Constant: ConstantExtractor + abstract class ConstantExtractor { + def apply(x: Int): Constant + def unapply(x: Constant): Int? + } + trait AbstractConstant { + def num: Int + } + implicit def ConstantDeco(t: Constant): AbstractConstant + + // --- AppliedOp ---------------------------------------- + + type AppliedOp <: Number + implicit def appliedOpClassTag: ClassTag[AppliedOp] + + trait AbstractAppliedOp { + def op: Op + def lhs: Number + def rhs: Number + } + implicit def AppliedOpDeco(t: AppliedOp): AbstractAppliedOp + + val AppliedOp: AppliedOpExtractor + abstract class AppliedOpExtractor { + def apply(op: Op, x: Number, y: Number): AppliedOp + def unapply(x: AppliedOp): (Op, Number, Number)? + } + + // === Operations ======================================= + // Represents: + // trait Op + // case object Puls extends Op + // case object Mult extends Op + + type Op + implicit def opClassTag: ClassTag[Op] + + trait AbstractOp { + def thisOp: Op + def apply(x: Number, y: Number): Int = thisOp match { + case Op.Puls() => x.eval + y.eval + case Op.Mult() => x.eval * y.eval + } + } + implicit def OpDeco(t: Op): AbstractOp + + val Op: OpModule + abstract class OpModule { + val Puls: PulsExtractor + abstract class PulsExtractor { + def apply(): Op + def unapply(x: Op): Boolean + } + + val Mult: MultExtractor + abstract class MultExtractor { + def apply(): Op + def unapply(x: Op): Boolean + } + } +} + +object CaseClassImplementation extends Arithmetic { + + // === Numbers ========================================== + // Represented as case classes + + sealed trait Num + final case class Const(n: Int) extends Num + final case class App(op: Op, x: Num, y: Num) extends Num + + type Number = Num + + def numberClassTag: ClassTag[Number] = implicitly + + def NumberDeco(t: Number): AbstractNumber = new AbstractNumber { + def thisNumber: Number = t + } + + // --- Constant ---------------------------------------- + + type Constant = Const + def constantClassTag: ClassTag[Constant] = implicitly + + def ConstantDeco(const: Constant): AbstractConstant = new AbstractConstant { + def num: Int = const.n + } + + object Constant extends ConstantExtractor { + def apply(x: Int): Constant = Const(x) + def unapply(x: Constant): Int? = x.n + } + + // --- AppliedOp ---------------------------------------- + + def AppliedOpDeco(t: AppliedOp): AbstractAppliedOp = new AbstractAppliedOp { + def op: Op = t.op + def lhs: Number = t.x + def rhs: Number = t.y + } + + type AppliedOp = App + def appliedOpClassTag: ClassTag[AppliedOp] = implicitly + + object AppliedOp extends AppliedOpExtractor { + def apply(op: Op, x: Number, y: Number): AppliedOp = App(op, x, y) + def unapply(app: AppliedOp): (Op, Number, Number)? = (app.op, app.x, app.y) + } + + // === Operations ======================================= + // Represented as case classes + + sealed trait Operation + case object PlusOp extends Operation + case object MultOp extends Operation + + type Op = Operation + def opClassTag: ClassTag[Op] = implicitly + + def OpDeco(t: Op): AbstractOp = new AbstractOp { + def thisOp: Op = t + } + + object Op extends OpModule { + object Puls extends PulsExtractor { + def apply(): Op = PlusOp + def unapply(x: Op): Boolean = x == PlusOp + } + object Mult extends MultExtractor { + def apply(): Op = MultOp + def unapply(x: Op): Boolean = x == MultOp + } + } +} + +object ListImplementation extends Arithmetic { + // Logically represented as: + // type Number <: List[Any] + // type Constant <: Number // List(n: Int) + // type AppliedOp <: Number // List(op: Op, lhs: Number, rhs: Number) + // + // type Op <: List[Any] // List(id: "+" | "*") + + // === Numbers ========================================== + + type Number = List[Any] + + def numberClassTag: ClassTag[Number] = new ClassTag[Number] { + def runtimeClass: Class[_] = classOf[List[_]] + override def unapply(x: Any): Option[List[Any]] = x match { + case ls: List[Any] if ls.length == 3 || (ls.length == 1 && ls(0).isInstanceOf[Int]) => + // Test that it is one of: + // type Constant <: Number // List(n: Int) + // type AppliedOp <: Number // List(op: Op, lhs: Number, rhs: Number) + Some(ls) + case _ => None + } + } + + def NumberDeco(t: Number): AbstractNumber = new AbstractNumber { + def thisNumber: Number = t + } + + // --- Constant ---------------------------------------- + + type Constant = List[Any] // List(n: Int) + def constantClassTag: ClassTag[Constant] = new ClassTag[Constant] { + def runtimeClass: Class[_] = classOf[List[_]] + override def unapply(x: Any): Option[List[Any]] = x match { + case ls: List[Any] if ls.length == 1 && ls(0).isInstanceOf[Int] => + // Test that it is: + // type Constant <: Number // List(n: Int) + Some(ls) + case _ => None + } + } + + def ConstantDeco(const: Constant): AbstractConstant = new AbstractConstant { + def num: Int = const(0).asInstanceOf[Int] + } + + object Constant extends ConstantExtractor { + def apply(x: Int): Constant = List(x) + def unapply(x: Constant): Int? = ConstantDeco(x).num + } + + // --- AppliedOp ---------------------------------------- + + def AppliedOpDeco(t: AppliedOp): AbstractAppliedOp = new AbstractAppliedOp { + def op: Op = t(0).asInstanceOf[Op] + def lhs: Number = t(1).asInstanceOf[Number] + def rhs: Number = t(2).asInstanceOf[Number] + } + + type AppliedOp = List[Any] // List(op: Op, lhs: Number, rhs: Number) + def appliedOpClassTag: ClassTag[AppliedOp] = new ClassTag[AppliedOp] { + def runtimeClass: Class[_] = classOf[List[_]] + override def unapply(x: Any): Option[List[Any]] = x match { + case ls: List[Any] if ls.length == 3 => + // Test that it is: + // type AppliedOp <: Number // List(op: Op, lhs: Number, rhs: Number) + Some(ls) + case _ => None + } + } + + object AppliedOp extends AppliedOpExtractor { + def apply(op: Op, x: Number, y: Number): AppliedOp = List(op, x, y) + def unapply(app: AppliedOp): (Op, Number, Number)? = { + val app2 = AppliedOpDeco(app) + (app2.op, app2.lhs, app2.rhs) + } + } + + // === Operations ======================================= + + type Op = List[Any] + def opClassTag: ClassTag[Op] = new ClassTag[Constant] { + def runtimeClass: Class[_] = classOf[List[_]] + override def unapply(x: Any): Option[List[Any]] = x match { + case op @ (("+" | "*") :: Nil) => + // Test that it is: + // type Op <: List[Any] // List(id: "+" | "*") + Some(op) + case _ => None + } + } + + def OpDeco(t: Op): AbstractOp = new AbstractOp { + def thisOp: Op = t + } + + object Op extends OpModule { + object Puls extends PulsExtractor { + def apply(): Op = List("+") + def unapply(x: Op): Boolean = x(0) == "+" + } + object Mult extends MultExtractor { + def apply(): Op = List("*") + def unapply(x: Op): Boolean = x(0) == "*" + } + } +} \ No newline at end of file diff --git a/tests/run/fully-abstract-nat-1-maybe.check b/tests/run/fully-abstract-nat-1-maybe.check new file mode 100644 index 000000000000..37bcfe2c2448 --- /dev/null +++ b/tests/run/fully-abstract-nat-1-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-1-maybe.scala b/tests/run/fully-abstract-nat-1-maybe.scala new file mode 100644 index 000000000000..f9e6f0c13a1c --- /dev/null +++ b/tests/run/fully-abstract-nat-1-maybe.scala @@ -0,0 +1,144 @@ +//> using options -Yexplicit-nulls + +import language.experimental.magic +object Test { + def main(args: Array[String]): Unit = { + println("CaseNums") + test(CaseNums) + println() + println("IntNums") + test(IntNums) + } + + def test(numbers: Numbers) = { + import numbers.* + + val zero: Nat = Zero() + val one: Nat = Succ(zero) + val two: Nat = Succ(one) + val three: Nat = Succ(two) + + zero match { + case Succ(p) => println("error") + case Zero() => println("ok") + } + + one match { + case Zero() => println("error") + case Succ(p) => println("ok") + } + + zero match { + case s: Succ => println("ok - unchecked error") + case z: Zero => println("ok - unchecked no error") + } + + def divOpt(a: Nat, b: Nat): Option[(Nat, Nat)] = b match { + case s @ Succ(_) => + // s is of type Nat though we know it is a Succ + Some(safeDiv(a, s.asInstanceOf[Succ])) + 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? + } + + implicit 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 + } + } + + 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)) (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) nat - 1 else null + } + + def SuccDeco(succ: Succ): SuccAPI = new SuccAPI { + def pred: Int = succ - 1 + } + + def safeDiv(a: Nat, b: Succ): (Nat, Nat) = (a / b, a % b) +} diff --git a/tests/run/fully-abstract-nat-2-maybe.check b/tests/run/fully-abstract-nat-2-maybe.check new file mode 100644 index 000000000000..d381a1625cc5 --- /dev/null +++ b/tests/run/fully-abstract-nat-2-maybe.check @@ -0,0 +1,13 @@ +CaseNums +ok +ok +ok +None +Some((SuccClass(ZeroObj),SuccClass(ZeroObj))) + +IntNums +error +error +error +/ by zero +Some((1,1)) diff --git a/tests/run/fully-abstract-nat-2-maybe.scala b/tests/run/fully-abstract-nat-2-maybe.scala new file mode 100644 index 000000000000..82fcb170acba --- /dev/null +++ b/tests/run/fully-abstract-nat-2-maybe.scala @@ -0,0 +1,155 @@ +//> using options -Yexplicit-nulls + +import language.experimental.magic +import scala.reflect.ClassTag + +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("error") // Will happen with IntNums + case z: Zero => println("ok") + } + + def divOpt(a: Nat, b: Nat): Option[(Nat, Nat)] = b match { + case s @ Succ(_) => Some(safeDiv(a, s)) + case _ => None + } + + try println(divOpt(one, zero)) + catch { case ex: java.lang.ArithmeticException => println(ex.getMessage) } + println(divOpt(three, two)) + } +} + +trait Numbers { + + type Nat + type Zero <: Nat + type Succ <: Nat + + implicit def natTag: ClassTag[Nat] + implicit def zeroTag: ClassTag[Zero] + implicit def succTag: ClassTag[Succ] + + val Zero: ZeroExtractor + trait ZeroExtractor { + def apply(): Zero + def unapply(zero: Zero): Boolean + } + + val Succ: SuccExtractor + trait SuccExtractor { + def apply(nat: Nat): Succ + def unapply(succ: Succ): Nat? + } + + implicit def SuccDeco(succ: Succ): SuccAPI + trait SuccAPI { + def pred: Nat + } + + def safeDiv(a: Nat, b: Succ): (Nat, Nat) +} + + +object CaseNums extends Numbers { + + trait NatClass + object ZeroObj extends NatClass { override def toString: String = "ZeroObj" } + case class SuccClass(pred: NatClass) extends NatClass + + type Nat = NatClass + type Zero = ZeroObj.type + type Succ = SuccClass + + def natTag: ClassTag[Nat] = implicitly[ClassTag[NatClass]] + def zeroTag: ClassTag[Zero] = implicitly[ClassTag[ZeroObj.type]] + def succTag: ClassTag[Succ] = implicitly[ClassTag[SuccClass]] + + object Zero extends ZeroExtractor { + def apply(): Zero = ZeroObj + def unapply(zero: Zero): Boolean = true + } + + object Succ extends SuccExtractor { + def apply(nat: Nat): Succ = SuccClass(nat) + def unapply(succ: Succ): Nat? = succ.pred + } + + 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)) (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 + + // BROKEN + // All class tags are identical: Nat, Zero and Succ cannot be distinguished + def natTag: ClassTag[Int] = ClassTag.Int + def zeroTag: ClassTag[Int] = ClassTag.Int // will also match any Int that is non zero + def succTag: ClassTag[Int] = ClassTag.Int // will also match 0 + + object Zero extends ZeroExtractor { + def apply(): Int = 0 + def unapply(zero: Zero): Boolean = true + } + + object Succ extends SuccExtractor { + def apply(nat: Nat): Int = nat + 1 + def unapply(succ: Succ): Int? = succ - 1 + } + + def SuccDeco(succ: Succ): SuccAPI = new SuccAPI { + def pred: Int = succ - 1 + } + + def safeDiv(a: Nat, b: Succ): (Nat, Nat) = (a / b, a % b) +} diff --git a/tests/run/fully-abstract-nat-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..d328eb883a11 --- /dev/null +++ b/tests/run/fully-abstract-nat-3-maybe.scala @@ -0,0 +1,159 @@ +//> using options -Yexplicit-nulls + +import language.experimental.magic +object Test { + def main(args: Array[String]): Unit = { + println("CaseNums") + test(CaseNums) + println() + println("IntNums") + test(IntNums) + } + + def test(numbers: Numbers) = { + import numbers.* + + val zero: Nat = Zero() + val one: Nat = Succ(zero) + val two: Nat = Succ(one) + val three: Nat = Succ(two) + + zero match { + case Succ(p) => println("error") + case Zero() => println("ok") + } + + one match { + case Zero() => println("error") + case Succ(p) => println("ok") + } + + zero match { + case s: Succ => println("ok - unchecked error") + case z: Zero => println("ok - unchecked no error") + } + + def divOpt(a: Nat, b: Nat): Option[(Nat, Nat)] = b match { + case SuccRefine(s @ Succ(_)) => Some(safeDiv(a, s)) + case _ => None + } + + println(divOpt(one, zero)) + println(divOpt(three, two)) + } +} + +trait Numbers { + + type Nat + type Zero <: Nat + type Succ <: Nat + + val Zero: ZeroExtractor + trait ZeroExtractor { + def apply(): Zero + def unapply(nat: Nat): Boolean + } + + val Succ: SuccExtractor + trait SuccExtractor { + def apply(nat: Nat): Succ + def unapply(nat: Nat): Nat? + } + + val SuccRefine: SuccRefineExtractor + trait SuccRefineExtractor { + def unapply(nat: Nat): Succ? + } + + implicit 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)) (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) nat - 1 else null + } + + + object SuccRefine extends SuccRefineExtractor { + def unapply(nat: Nat): Succ? = + if (nat > 0) nat else null + } + + def SuccDeco(succ: Succ): SuccAPI = new SuccAPI { + def pred: Int = succ - 1 + } + + def safeDiv(a: Nat, b: Succ): (Nat, Nat) = (a / b, a % b) +} diff --git a/tests/run/fully-abstract-nat-maybe.check b/tests/run/fully-abstract-nat-maybe.check new file mode 100644 index 000000000000..bb36ec67992e --- /dev/null +++ b/tests/run/fully-abstract-nat-maybe.check @@ -0,0 +1,35 @@ +CaseClassImplementation +underlying rep: Z +test1 OK +test2 OK + +underlying rep: S(S(S(Z))) +test3 OK +Succ(S(S(Z))) = 3 +test4 OK +Succ(S(S(Z))) = 3 + +IntImplementation +underlying rep: 0 +test1 OK +test2 OK + +underlying rep: 3 +test3 OK +Succ(2) = 3 +test4 OK +Succ(2) = 3 + +UnboundedIntImplementation +underlying rep: 0 +test1 OK +test2 OK + +underlying rep: 3 +test3 OK +Succ(2) = 3 +test4 OK +Succ(2) = 3 + +test OK +Succ(1267650600228229401496703205374) = 1267650600228229401496703205376 diff --git a/tests/run/fully-abstract-nat-maybe.scala b/tests/run/fully-abstract-nat-maybe.scala new file mode 100644 index 000000000000..692fd573a4ba --- /dev/null +++ b/tests/run/fully-abstract-nat-maybe.scala @@ -0,0 +1,289 @@ +//> using options -Yexplicit-nulls +import language.experimental.magic +import scala.magic.* +import scala.reflect.ClassTag + +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.* + 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.* + val zero = Zero() + println("underlying rep: " + zero) + + zero match { + case Succ(_) => println("test1 fail") + case Zero() => println("test1 OK") + } + + zero match { + case _: Succ => println("test2 fail") + case _: Zero => println("test2 OK") + } + + println() + + val three = Succ(Succ(zero)).succ + + println("underlying rep: " + three) + + three match { + case Zero() => println("test3 fail") + case s @ Succ(pred) => + println("test3 OK") + println(s"Succ($pred) = ${s.value}") + } + + three match { + case _: Zero => println("test4 fail") + case s: Succ => + println("test4 OK") + println(s"Succ(${s.pred}) = ${s.value}") + } + } + +} + +abstract class Numbers { + + // === Nat ========================================== + // Represents: + // trait Nat + // case object Zero extends Nat + // case class Succ(pred: Nat) extends Nat + + type Nat + implicit def natClassTag: ClassTag[Nat] + + trait AbstractNat { + def value: Int + def succ: Succ + } + implicit def NatDeco(nat: Nat): AbstractNat + + // --- Zero ---------------------------------------- + + type Zero <: Nat + + implicit def zeroClassTag: ClassTag[Zero] + + val Zero: ZeroExtractor + abstract class ZeroExtractor { + def apply(): Zero + def unapply(zero: Zero): Boolean + } + + // --- Succ ---------------------------------------- + + type Succ <: Nat + + implicit 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 + } + implicit def SuccDeco(succ: Succ): AbstractSucc + +} + +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 + + implicit 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) + + implicit 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 + } + } + + implicit 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) 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) 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 + } + + implicit 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) n.intValue() + else n + } + } + +} diff --git a/tests/run/i13968-maybe.scala b/tests/run/i13968-maybe.scala new file mode 100644 index 000000000000..e753e2683f5e --- /dev/null +++ b/tests/run/i13968-maybe.scala @@ -0,0 +1,28 @@ +//> using options -Yexplicit-nulls +import language.experimental.magic +object Bar { + def unapply(x: Any): (Int *: Int *: EmptyTuple)? = 1 *: 2 *: Tuple() +} + +object Bar23 { + def unapply(x: Any): Option[ + Int *: Int *: Int *: Int *: Int *: Int *: Int *: Int *: Int *: Int *: + Int *: Int *: Int *: Int *: Int *: Int *: Int *: Int *: Int *: Int *: + Int *: Int *: Int *: EmptyTuple + ] = Some( + 1 *: 2 *: 3 *: 4 *: 5 *: 6 *: 7 *: 8 *: 9 *: 10 *: + 11 *: 12 *: 13 *: 14 *: 15 *: 16 *: 17 *: 18 *: 19 *: 20 *: + 21 *: 22 *: 23 *: Tuple() + ) +} + +@main def Test() = + "" match + case Bar((a, b)) => assert(a == 1 && b == 2, (a, b)) + + "" match + case Bar23(( + u1, u2, u3, u4, u5, u6, u7, u8, u9, u10, + u11, u12, u13, u14, u15, u16, u17, u18, u19, u20, + u21, u22, u23 + )) => assert(u1 == 1 && u23 == 23, (u1, u23)) diff --git a/tests/run/i1748-maybe.check b/tests/run/i1748-maybe.check new file mode 100644 index 000000000000..888299747af9 --- /dev/null +++ b/tests/run/i1748-maybe.check @@ -0,0 +1,2 @@ +class + extends diff --git a/tests/run/i1748-maybe.scala b/tests/run/i1748-maybe.scala new file mode 100644 index 000000000000..7bb0d7199347 --- /dev/null +++ b/tests/run/i1748-maybe.scala @@ -0,0 +1,16 @@ +//> using options -Yexplicit-nulls +import language.experimental.magic +object Test { + implicit class Foo(sc: StringContext) { + object q { + def unapply(arg: Any): (Any, Any)? = + (sc.parts(0), sc.parts(1)) + } + } + + def main(args: Array[String]): Unit = { + val q"class $name extends $parent" = new Object + println(name) + println(parent) + } +} \ No newline at end of file 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..b0650eb3dca3 --- /dev/null +++ b/tests/run/i1773-maybe.scala @@ -0,0 +1,16 @@ +//> using options -Yexplicit-nulls +import language.experimental.magic +object Test { + implicit class Foo(sc: StringContext) { + object q { + def unapply(arg: Any): (Any, Any)? = + (sc.parts(0), sc.parts(1)) + } + } + + def main(args: Array[String]): Unit = { + val q"class ${name: String} extends ${parent: String}" = new Object + println(name) + println(parent) + } +} diff --git a/tests/run/i1779-maybe.check b/tests/run/i1779-maybe.check new file mode 100644 index 000000000000..4ef6e900e49d --- /dev/null +++ b/tests/run/i1779-maybe.check @@ -0,0 +1 @@ + extends diff --git a/tests/run/i1779-maybe.scala b/tests/run/i1779-maybe.scala new file mode 100644 index 000000000000..7bfb18e97d29 --- /dev/null +++ b/tests/run/i1779-maybe.scala @@ -0,0 +1,15 @@ +//> using options -Yexplicit-nulls +import language.experimental.magic +object Test { + implicit class Foo(sc: StringContext) { + object q { + def unapply(arg: Any): (Any, Any)? = + (sc.parts(0), sc.parts(1)) + } + } + + def main(args: Array[String]): Unit = { + val q"class $_ extends $_parent" = new Object + println(_parent) + } +} diff --git a/tests/run/i4177-maybe.scala b/tests/run/i4177-maybe.scala new file mode 100644 index 000000000000..c9eeab18f0a9 --- /dev/null +++ b/tests/run/i4177-maybe.scala @@ -0,0 +1,20 @@ +//> using options -Yexplicit-nulls +import language.experimental.magic +object Test { + private[this] var count = 0 + + def test(x: Int) = { count += 1; true } + + object Foo { + def unapply(x: Int): Int? = { count += 1; x } + } + + def main(args: Array[String]): Unit = { + val res = List(1, 2).collect { case x if test(x) => x } + assert(count == 2) + + count = 0 + val res2 = List(1, 2).collect { case Foo(x) => x } + assert(count == 2) + } +} diff --git a/tests/run/i8530-b-maybe.scala b/tests/run/i8530-b-maybe.scala new file mode 100644 index 000000000000..94a3fb4fdf94 --- /dev/null +++ b/tests/run/i8530-b-maybe.scala @@ -0,0 +1,23 @@ +//> using options -Yexplicit-nulls +import language.experimental.magic +import scala.magic.Ok +import scala.compiletime.erasedValue + +class MyRegex[Pattern <: String & Singleton/*Literal constant*/]: + inline def unapplySeq(s: CharSequence): List[String?]? = + inline erasedValue[Pattern] match + case "foo" => if s == "foo" then Nil else null + case _ => valueOf[Pattern].r.unapplySeq(s) match + case Some(xs) => Ok(xs) + case None => null + +@main def Test: Unit = + val myRegexp1 = new MyRegex["foo"] + val myRegexp2 = new MyRegex["f(o+)"] + "foo" match + case myRegexp1() => // Match ok + case myRegexp2(x) => ??? + "foooo" match + case myRegexp1() => ??? + case myRegexp2(x) => + assert(x == "oooo") \ No newline at end of file diff --git a/tests/run/i8530-maybe.check b/tests/run/i8530-maybe.check new file mode 100644 index 000000000000..34749b91c683 --- /dev/null +++ b/tests/run/i8530-maybe.check @@ -0,0 +1,7 @@ +MyBoooleanUnapply +2 +3 +(4,5) +5 +6 +7 diff --git a/tests/run/i8530-maybe.scala b/tests/run/i8530-maybe.scala new file mode 100644 index 000000000000..3299e36efcf4 --- /dev/null +++ b/tests/run/i8530-maybe.scala @@ -0,0 +1,48 @@ +//> using options -Yexplicit-nulls +import language.experimental.magic +import scala.magic.* +object MyBoooleanUnapply: + inline def unapply(x: Int): Boolean = true + +object MyOptionUnapply: + inline def unapply(x: Int): Long? = x.toLong + +object MyPolyUnapply: + inline def unapply[T](x: T): T? = Ok(x) + +object MySeqUnapply: + inline def unapplySeq(x: Int): Seq[Int] = Seq(x, x + 1) + +object MyWhiteboxUnapply: + transparent inline def unapply(x: Int): Any? = Ok(x) + +object MyWhiteboxUnapply1: + transparent inline def unapply(using DummyImplicit)(x: Int)(using DummyImplicit): Any? = Ok(x) + +object MyWhiteboxUnapply2: + transparent inline def unapply(using DummyImplicit)(using DummyImplicit)(x: Int)(using DummyImplicit)(using DummyImplicit): Any? = Ok(x) + + +@main def Test = + 1 match + case MyBoooleanUnapply() => println("MyBoooleanUnapply") + + 2 match + case MyOptionUnapply(y) => println(y) + + 3 match + case MyPolyUnapply(a) => println(a) + + 4 match + case MySeqUnapply(a, b) => println((a, b)) + + 5 match + case MyWhiteboxUnapply(x) => println(x: Int) + + 6 match + case MyWhiteboxUnapply1(x) => println(x: Int) + + 7 match + case MyWhiteboxUnapply2(x) => println(x: Int) + +end Test diff --git a/tests/run/i8577a-maybe.scala b/tests/run/i8577a-maybe.scala new file mode 100644 index 000000000000..90baf5490853 --- /dev/null +++ b/tests/run/i8577a-maybe.scala @@ -0,0 +1,15 @@ +//> using options -Yexplicit-nulls +import language.experimental.magic +object Macro: + opaque type StrCtx = StringContext + def apply(ctx: StringContext): StrCtx = ctx + def unapply(ctx: StrCtx): StringContext? = ctx + +extension (ctx: StringContext) def mac: Macro.StrCtx = Macro(ctx) +extension (inline ctx: Macro.StrCtx) inline def unapplySeq(inline input: Int): Seq[Int]? = + Seq(input) + +@main def Test: Unit = + val mac"$x" = 1 + val y: Int = x + assert(x == 1) diff --git a/tests/run/i8577b-maybe.scala b/tests/run/i8577b-maybe.scala new file mode 100644 index 000000000000..a08b7e942e8d --- /dev/null +++ b/tests/run/i8577b-maybe.scala @@ -0,0 +1,15 @@ +//> using options -Yexplicit-nulls +import language.experimental.magic +object Macro: + opaque type StrCtx = StringContext + def apply(ctx: StringContext): StrCtx = ctx + def unapply(ctx: StrCtx): StringContext? = ctx + +extension (ctx: StringContext) def mac: Macro.StrCtx = Macro(ctx) +extension (inline ctx: Macro.StrCtx) inline def unapplySeq[U](inline input: U): Seq[U]? = + Seq(input) + +@main def Test: Unit = + val mac"$x" = 1 + val y: Int = x + assert(x == 1) diff --git a/tests/run/i8577c-maybe.scala b/tests/run/i8577c-maybe.scala new file mode 100644 index 000000000000..eb3faddef1c3 --- /dev/null +++ b/tests/run/i8577c-maybe.scala @@ -0,0 +1,15 @@ +//> using options -Yexplicit-nulls +import language.experimental.magic +object Macro: + opaque type StrCtx = StringContext + def apply(ctx: StringContext): StrCtx = ctx + def unapply(ctx: StrCtx): StringContext? = ctx + +extension (ctx: StringContext) def mac: Macro.StrCtx = Macro(ctx) +extension [T] (inline ctx: Macro.StrCtx) inline def unapplySeq(inline input: T): Seq[T]? = + Seq(input) + +@main def Test: Unit = + val mac"$x" = 1 + val y: Int = x + assert(x == 1) diff --git a/tests/run/i8577d-maybe.scala b/tests/run/i8577d-maybe.scala new file mode 100644 index 000000000000..c69c63b4c6ec --- /dev/null +++ b/tests/run/i8577d-maybe.scala @@ -0,0 +1,15 @@ +//> using options -Yexplicit-nulls +import language.experimental.magic +object Macro: + opaque type StrCtx = StringContext + def apply(ctx: StringContext): StrCtx = ctx + def unapply(ctx: StrCtx): StringContext? = ctx + +extension (ctx: StringContext) def mac: Macro.StrCtx = Macro(ctx) +extension [T] (inline ctx: Macro.StrCtx) inline def unapplySeq[U](inline input: T): Seq[T]? = + Seq(input) + +@main def Test: Unit = + val mac"$x" = 1 + val y: Int = x + assert(x == 1) diff --git a/tests/run/i8577e-maybe.scala b/tests/run/i8577e-maybe.scala new file mode 100644 index 000000000000..ffc5bd240824 --- /dev/null +++ b/tests/run/i8577e-maybe.scala @@ -0,0 +1,19 @@ +//> using options -Yexplicit-nulls +import language.experimental.magic +object Macro: + opaque type StrCtx = StringContext + def apply(ctx: StringContext): StrCtx = ctx + def unapply(ctx: StrCtx): StringContext? = ctx + +extension (ctx: StringContext) def mac: Macro.StrCtx = Macro(ctx) +extension [T] (inline ctx: Macro.StrCtx) inline def unapplySeq[U](inline input: (T, U)): Seq[(T, U)]? = + Seq(input) + +@main def Test: Unit = + val mac"$x" = (1, 2) + val x2: (Int, Int) = x + assert(x == (1, 2)) + + val mac"$y" = (1, "a") + val y2: (Int, String) = y + assert(y == (1, "a")) diff --git a/tests/run/i8577f-maybe.scala b/tests/run/i8577f-maybe.scala new file mode 100644 index 000000000000..42d8d7263c3f --- /dev/null +++ b/tests/run/i8577f-maybe.scala @@ -0,0 +1,15 @@ +//> using options -Yexplicit-nulls +import language.experimental.magic +object Macro: + opaque type StrCtx = StringContext + def apply(ctx: StringContext): StrCtx = ctx + def unapply(ctx: StrCtx): StringContext? = ctx + +@main def Test: Unit = + extension (ctx: StringContext) def mac: Macro.StrCtx = Macro(ctx) + extension (inline ctx: Macro.StrCtx) inline def unapplySeq[U](inline input: U): Seq[U]? = + Seq(input) + + val mac"$x" = 1 + val y: Int = x + assert(x == 1) diff --git a/tests/run/i8577g-maybe.scala b/tests/run/i8577g-maybe.scala new file mode 100644 index 000000000000..a56cd914878c --- /dev/null +++ b/tests/run/i8577g-maybe.scala @@ -0,0 +1,15 @@ +//> using options -Yexplicit-nulls +import language.experimental.magic +object Macro: + opaque type StrCtx = StringContext + def apply(ctx: StringContext): StrCtx = ctx + def unapply(ctx: StrCtx): StringContext? = ctx + +extension (ctx: StringContext) def mac: Macro.StrCtx = Macro(ctx) +extension [T] (inline ctx: Macro.StrCtx) inline def unapplySeq[U](inline input: T | U): Seq[T | U]? = + Seq(input) + +@main def Test: Unit = + val mac"$x" = 1 + val y: Int = x + assert(x == 1) diff --git a/tests/run/i8577h-maybe.scala b/tests/run/i8577h-maybe.scala new file mode 100644 index 000000000000..30aeee95eac0 --- /dev/null +++ b/tests/run/i8577h-maybe.scala @@ -0,0 +1,15 @@ +//> using options -Yexplicit-nulls +import language.experimental.magic +object Macro: + opaque type StrCtx = StringContext + def apply(ctx: StringContext): StrCtx = ctx + def unapply(ctx: StrCtx): StringContext? = ctx + +extension (ctx: StringContext) def mac: Macro.StrCtx = Macro(ctx) +extension [T] (inline ctx: Macro.StrCtx) inline def unapplySeq[U](inline input: U | T): Seq[T | U]? = + Seq(input) + +@main def Test: Unit = + val mac"$x" = 1 + val y: Int = x + assert(x == 1) diff --git a/tests/run/i8577i-maybe.scala b/tests/run/i8577i-maybe.scala new file mode 100644 index 000000000000..b38c8d862fc0 --- /dev/null +++ b/tests/run/i8577i-maybe.scala @@ -0,0 +1,16 @@ +//> using options -Yexplicit-nulls +import language.experimental.magic +object Macro: + opaque type StrCtx = StringContext + def apply(ctx: StringContext): StrCtx = ctx + def unapply(ctx: StrCtx): StringContext? = ctx + +extension (ctx: StringContext) def mac: Macro.StrCtx = Macro(ctx) +extension (inline ctx: Macro.StrCtx) transparent inline def unapplySeq(inline input: String): Seq[Any]? = + Seq(123) + +@main def Test: Unit = + "abc" match + case mac"$x" => + val y: Int = x + assert(x == 123) diff --git a/tests/run/maybe-numeric-widening.check b/tests/run/maybe-numeric-widening.check new file mode 100644 index 000000000000..d3c284e753f4 --- /dev/null +++ b/tests/run/maybe-numeric-widening.check @@ -0,0 +1 @@ +got: 3 diff --git a/tests/run/maybe-numeric-widening.scala b/tests/run/maybe-numeric-widening.scala new file mode 100644 index 000000000000..c611c84b310d --- /dev/null +++ b/tests/run/maybe-numeric-widening.scala @@ -0,0 +1,12 @@ +//> using options -Yexplicit-nulls +import language.experimental.magic +import scala.magic.* + +object WidenBug: + def f(x: Int): Long? = x // no numeric widening inserted + +object Test: + def main(args: Array[String]): Unit = + WidenBug.f(3) match + case Ok(v) => println("got: " + (v: Long)) + case null => println("null") diff --git a/tests/run/maybe-widening.check b/tests/run/maybe-widening.check new file mode 100644 index 000000000000..1e3e8f58856e --- /dev/null +++ b/tests/run/maybe-widening.check @@ -0,0 +1 @@ +matched: 2 diff --git a/tests/run/maybe-widening.scala b/tests/run/maybe-widening.scala new file mode 100644 index 000000000000..10fbbaec2a03 --- /dev/null +++ b/tests/run/maybe-widening.scala @@ -0,0 +1,19 @@ +//> using options -Yexplicit-nulls +import language.experimental.magic + +trait Peano: + type Nat + val Succ: SuccExtractor + trait SuccExtractor: + def unapply(nat: Nat): Nat? + +object IntNums extends Peano: + type Nat = Int + object Succ extends SuccExtractor: + def unapply(nat: Nat) = nat - 1 + +object Test: + def main(args: Array[String]): Unit = + (3: IntNums.Nat) match + case IntNums.Succ(v) => println("matched: " + v) + case _ => println("no match") diff --git a/tests/run/maybe.scala b/tests/run/maybe.scala index 084f5ebad610..4395e6da051c 100644 --- a/tests/run/maybe.scala +++ b/tests/run/maybe.scala @@ -2,6 +2,7 @@ import language.experimental.magic import scala.magic.* + class C: def toOptionAny[T](x: Any): Option[Any] = x match case Ok(y) => Some(y) @@ -29,8 +30,8 @@ object WithTail: object Poly: def unapply[T](x: T): T? = x match - case Pos(y) => y - case WithTail(s) => s + case Pos(y: T @unchecked) => Ok(y) + case WithTail(s: T @unchecked) => Ok(s) case _ => null def f[T](x: T) = diff --git a/tests/run/named-patterns-maybe.check b/tests/run/named-patterns-maybe.check new file mode 100644 index 000000000000..9ccc08d67069 --- /dev/null +++ b/tests/run/named-patterns-maybe.check @@ -0,0 +1,20 @@ +name Bob, age 22 +name Bob +age 22 +age 22, name Bob +Bob, 22 +name Bob, age 22 +name Bob +age 22 +age 22, name Bob +Bob, 22 +1003 Lausanne, Rue de la Gare 44 +1003 Lausanne +Rue de la Gare in Lausanne +1003 Lausanne, Rue de la Gare 44 +1003 Lausanne, Rue de la Gare 44 +Bob, aged 22, in 1003 Lausanne, Rue de la Gare 44 +Bob in 1003 Lausanne +aged 22 in Rue de la Gare in Lausanne +Bob, aged 22 in 1003 Lausanne, Rue de la Gare 44 +Bob, aged 22 in 1003 Lausanne, Rue de la Gare 44 diff --git a/tests/run/named-patterns-maybe.scala b/tests/run/named-patterns-maybe.scala new file mode 100644 index 000000000000..01495966ebf7 --- /dev/null +++ b/tests/run/named-patterns-maybe.scala @@ -0,0 +1,75 @@ +//> using options -Yexplicit-nulls + +import language.experimental.magic +object Test1: + class Person(val name: String, val age: Int) + + object Person: + def unapply(p: Person): (name: String, age: Int) = (p.name, p.age) + + class Person2(val name: String, val age: Int) + object Person2: + def unapply(p: Person2): (name: String, age: Int)? = (p.name, p.age) + + case class Address(city: String, zip: Int, street: String, number: Int) + + @main def Test = + val bob = Person("Bob", 22) + bob match + case Person(name = n, age = a) => println(s"name $n, age $a") + bob match + case Person(name = n) => println(s"name $n") + bob match + case Person(age = a) => println(s"age $a") + bob match + case Person(age = a, name = n) => println(s"age $a, name $n") + bob match + case Person(age, name) => println(s"$age, $name") + + val bob2 = Person2("Bob", 22) + bob2 match + case Person2(name = n, age = a) => println(s"name $n, age $a") + bob2 match + case Person2(name = n) => println(s"name $n") + bob2 match + case Person2(age = a) => println(s"age $a") + bob2 match + case Person2(age = a, name = n) => println(s"age $a, name $n") + bob2 match + case Person2(age, name) => println(s"$age, $name") + + val addr = Address("Lausanne", 1003, "Rue de la Gare", 44) + addr match + case Address(city = c, zip = z, street = s, number = n) => + println(s"$z $c, $s $n") + addr match + case Address(zip = z, city = c) => + println(s"$z $c") + addr match + case Address(city = c, street = s) => + println(s"$s in $c") + addr match + case Address(number = n, street = s, zip = z, city = c) => + println(s"$z $c, $s $n") + addr match + case Address(c, z, s, number) => + println(s"$z $c, $s $number") + + type Person3 = (p: Person2, addr: Address) + + val p3 = (p = bob2, addr = addr) + p3 match + case (addr = Address(city = c, zip = z, street = s, number = n), p = Person2(name = nn, age = a)) => + println(s"$nn, aged $a, in $z $c, $s $n") + p3 match + case (p = Person2(name = nn), addr = Address(zip = z, city = c)) => + println(s"$nn in $z $c") + p3 match + case (p = Person2(age = a), addr = Address(city = c, street = s)) => + println(s"aged $a in $s in $c") + p3 match + case (Person2(age = a, name = nn), Address(number = n, street = s, zip = z, city = c)) => + println(s"$nn, aged $a in $z $c, $s $n") + p3 match + case (Person2(nn, a), Address(c, z, s, number)) => + println(s"$nn, aged $a in $z $c, $s $number") diff --git a/tests/run/orelse.scala b/tests/run/orelse.scala index 55743dff2fd4..af2a2fb47464 100644 --- a/tests/run/orelse.scala +++ b/tests/run/orelse.scala @@ -31,8 +31,8 @@ object WithTail: object Poly: def unapply[T](x: T): T ? String = x match - case Pos(y) => y - case WithTail(s) => s + 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) = @@ -42,7 +42,7 @@ def f[T, E](x: T, e: E) = val x4 = toEither(Err("bad")) val y1 = toEitherIntStr(Ok(1)) - val y3 = toEitherIntStr(22) + val y3 = toEitherIntStr(Ok(22)) val y4 = toEitherIntStr(Err("bad")) val z1 = toEither(Ok(x)) diff --git a/tests/run/patmat-maybe.check b/tests/run/patmat-maybe.check new file mode 100644 index 000000000000..c3b06e0f9d8a --- /dev/null +++ b/tests/run/patmat-maybe.check @@ -0,0 +1,4 @@ +Bob is 22 years old and lives in Paris +Hello Peter +Bob is 22 years old and lives in Paris +Hello PersonExtractor(Peter) diff --git a/tests/run/patmat-maybe.scala b/tests/run/patmat-maybe.scala new file mode 100644 index 000000000000..a74414453e46 --- /dev/null +++ b/tests/run/patmat-maybe.scala @@ -0,0 +1,47 @@ +//> using options -Yexplicit-nulls +import language.experimental.magic +object Test1: + class User(val name: String, val age: Int, val city: String) + object User: + def unapply(user: User) = UserExtractor(user.name, user.age, user.city) + + case class UserExtractor(name: String, age: Int, city: String) + + class Person(val name: String) + object Person: + def unapply(person: Person) = PersonExtractor(person.name) + + case class PersonExtractor(name: String) + + def test = + val user = User("Bob", 22, "Paris") + (user: Any) match + case User(name, age, city) => println(s"$name is $age years old and lives in $city") + val p = Person("Peter") + (p: Any) match + case Person(n) => println(s"Hello $n") + +object Test2: + class User(val name: String, val age: Int, val city: String) + object User: + def unapply(user: User): UserExtractor? = UserExtractor(user.name, user.age, user.city) + + case class UserExtractor(name: String, age: Int, city: String) + + class Person(val name: String) + object Person: + def unapply(person: Person): Some[PersonExtractor] = Some(PersonExtractor(person.name)) + + case class PersonExtractor(name: String) + + def test = + val user = User("Bob", 22, "Paris") + (user: Any) match + case User(name, age, city) => println(s"$name is $age years old and lives in $city") + val p = Person("Peter") + (p: Any) match + case Person(n) => println(s"Hello $n") + +@main def Test = + Test1.test + Test2.test diff --git a/tests/run/patmat-option-named-maybe.scala b/tests/run/patmat-option-named-maybe.scala new file mode 100644 index 000000000000..f4c76d2c579f --- /dev/null +++ b/tests/run/patmat-option-named-maybe.scala @@ -0,0 +1,23 @@ +//> using options -Yexplicit-nulls +import language.experimental.magic +case class HasSingleField(f: HasSingleField) + +object Test { + + def main(args: Array[String]) = { + val s: Object = HasSingleField(null.asInstanceOf[HasSingleField]) + s match { + case Matcher(self) => + assert(self ne null) + } + } +} + +object Matcher { + def unapply(x: Object): HasSingleField? = { + if (x.isInstanceOf[HasSingleField]) + x.asInstanceOf[HasSingleField] + else + null + } +} diff --git a/tests/run/patmat-spec-maybe.scala b/tests/run/patmat-spec-maybe.scala new file mode 100644 index 000000000000..68e753acda13 --- /dev/null +++ b/tests/run/patmat-spec-maybe.scala @@ -0,0 +1,63 @@ +//> using options -Yexplicit-nulls +// To be kept in sync with docs/docs/reference/pattern-matching.md +import language.experimental.magic +object Test { + def main(args: Array[String]): Unit = { + object Even { + def unapply(s: String): Boolean = s.size % 2 == 0 + } + + "even" match { + case s @ Even() => println(s"$s has an even number of characters") + case s => println(s"$s has an odd number of characters") + } + // even has an even number of characters + + class FirstChars(s: String) extends Product { + def _1 = s.charAt(0) + def _2 = s.charAt(1) + + // Not used by pattern matching: Product is only used as a marker trait. + def canEqual(that: Any): Boolean = ??? + def productArity: Int = ??? + def productElement(n: Int): Any = ??? + } + + object FirstChars { + def unapply(s: String): FirstChars = new FirstChars(s) + } + + "Hi!" match { + case FirstChars(char1, char2) => + println(s"First: $char1; Second: $char2") + } + // First: H; Second: i + + object CharList { + def unapplySeq(s: String): Seq[Char]? = s.toList + } + + "example" match { + case CharList(c1, c2, c3, c4, _, _, _) => + println(s"$c1,$c2,$c3,$c4") + case _ => + println("Expected *exactly* 7 characters!") + } + // e,x,a,m + + class Nat(val x: Int) { + def get: Int = x + def isEmpty = x < 0 + } + + object Nat { + def unapply(x: Int): Nat = new Nat(x) + } + + 5 match { + case Nat(n) => println(s"$n is a natural number") + case _ => () + } + // 5 is a natural number + } +} diff --git a/tests/run/patmatch-classtag-maybe.scala b/tests/run/patmatch-classtag-maybe.scala new file mode 100644 index 000000000000..b6bbfbd54b87 --- /dev/null +++ b/tests/run/patmatch-classtag-maybe.scala @@ -0,0 +1,47 @@ +//> using options -Yexplicit-nulls +import language.experimental.magic +import reflect.ClassTag +trait API { + type CaseDef + + implicit val tagForCaseDef: ClassTag[CaseDef] + + trait CaseDefCompanion { + def apply(x: String): CaseDef + def unapply(x: CaseDef): String? + } + lazy val CaseDef: CaseDefCompanion +} + +object dotc { + case class CaseDef(str: String) +} + +object Impl extends API { + type CaseDef = dotc.CaseDef + + val tagForCaseDef: ClassTag[dotc.CaseDef] = implicitly + + object CaseDef extends CaseDefCompanion { + def apply(str: String): CaseDef = dotc.CaseDef(str) + def unapply(x: CaseDef): String? = x.str + } +} + +object Test extends App { + val api: API = Impl + import api.* + + val x: Any = CaseDef("123") + + x match { + case cdef: CaseDef => + val x: CaseDef = cdef + println(cdef) + } + x match { + case cdef @ CaseDef(s) => + val x: CaseDef = cdef + println(s) + } +} diff --git a/tests/run/reducable-maybe.scala b/tests/run/reducable-maybe.scala new file mode 100644 index 000000000000..ac3f823bb6b0 --- /dev/null +++ b/tests/run/reducable-maybe.scala @@ -0,0 +1,64 @@ +//> using options -Yexplicit-nulls +import language.experimental.magic +object Test extends App { + object Cons { + var count = 0 + + def unapply[T](xs: List[T]): (T, List[T])? = { + count += 1 + xs match { + case x :: xs1 => (x, xs1) + case _ => null + } + } + } + + object Guard { + var count = 0 + + def apply(): Boolean = { + count += 1 + false + } + } + + def reset(): Unit = { + Cons.count = 0 + Guard.count = 0 + } + + val xs = List(1, 2, 3) + test1(xs) + reset() + test2(xs) + + def test1(xs: List[Int]): Unit = { + val res = xs match { + case Cons(0, Nil) => 1 + case Cons(_, Nil) => 2 + case Cons(0, _) => 3 + case Cons(1, ys) => 4 + } + + assert(res == 4, res) + assert(Cons.count == 1, Cons.count) + } + + // #1313 + def test2(xs: List[Int]): Unit = { + val res = xs match { + case Cons(0, Nil) if Guard() => 1 + case Cons(0, Nil) => 2 + case Cons(_, Nil) if Guard() => 3 + case Cons(_, Nil) => 4 + case Cons(0, _) if Guard() => 5 + case Cons(0, _) => 6 + case Cons(1, ys) if Guard() => 7 + case Cons(1, ys) => 8 + } + + assert(res == 8, res) + assert(Cons.count == 1, Cons.count) + assert(Guard.count == 1, Guard.count) + } +} 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..50dffb123bc6 --- /dev/null +++ b/tests/run/string-extractor-maybe.scala @@ -0,0 +1,65 @@ +//> using options -Yexplicit-nulls +import language.experimental.magic +final class StringExtract(val s: String) extends AnyVal { + def length = s.length + def lengthCompare(n: Int) = s.length compare n + def apply(idx: Int): Char = s charAt idx + def head: Char = s charAt 0 + def tail: String = s drop 1 + def drop(n: Int): Seq[Char] = toSeq.drop(n) + def toSeq: Seq[Char] = s.toSeq + + override def toString = s +} + +final class ThreeStringExtract(val s: String) extends AnyVal { + def get: (List[Int], Double, Seq[Char]) = ((s.length :: Nil, s.length.toDouble, toSeq)) + def length = s.length + def lengthCompare(n: Int) = s.length compare n + def apply(idx: Int): Char = s charAt idx + def head: Char = s charAt 0 + def tail: String = s drop 1 + def drop(n: Int): Seq[Char] = toSeq.drop(n) + def toSeq: Seq[Char] = s.toSeq + + override def toString = s +} + + +object Bippy { + def unapplySeq(x: Any): StringExtract? = + if ((x == null) || (x == "")) 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/t1048-maybe.check b/tests/run/t1048-maybe.check new file mode 100644 index 000000000000..f1e5eeed2d93 --- /dev/null +++ b/tests/run/t1048-maybe.check @@ -0,0 +1,2 @@ +3 +2 diff --git a/tests/run/t1048-maybe.scala b/tests/run/t1048-maybe.scala new file mode 100644 index 000000000000..ba8fe9922108 --- /dev/null +++ b/tests/run/t1048-maybe.scala @@ -0,0 +1,23 @@ +//> using options -Yexplicit-nulls +import language.experimental.magic +final case class W[A](v: A) + +object E { + def unapply(w: W[Any]): Any? = null +} + +object Bug { + def bug[A](e: Either[W[_], A]) = e match { + case Left(E(x)) => 1 + case Right(x) => 2 + case _ => 3 + } +} + +object Test { + def main(args: Array[String]): Unit = { + println(Bug.bug(Left(W(5)))) + println(Bug.bug(Right(5))) + } +} + diff --git a/tests/run/t1220-maybe.scala b/tests/run/t1220-maybe.scala new file mode 100644 index 000000000000..ca84cbf97bc6 --- /dev/null +++ b/tests/run/t1220-maybe.scala @@ -0,0 +1,17 @@ +//> using options -Yexplicit-nulls +import language.experimental.magic +object Test extends App { + + class QSRichIterable[A](self: Iterable[A]) { + def filterMap[R](f: PartialFunction[A,R]) = + self filter (f.isDefinedAt) map f + } + + object Un { + def unapply(i: Int): Int? = i + } + + val richIter = new QSRichIterable(List(0, 1, 2, 3, 4)) + + assert((richIter filterMap {case Un(3) => 7}) == List(7)) +} diff --git a/tests/run/t4415-maybe.scala b/tests/run/t4415-maybe.scala new file mode 100644 index 000000000000..3c00ac7abb1f --- /dev/null +++ b/tests/run/t4415-maybe.scala @@ -0,0 +1,88 @@ +//> using options -Yexplicit-nulls +/** + * Demonstration of issue with Extractors. If lines 15/16 are not present, get at runtime: + * + * Exception in thread "main" java.lang.VerifyError: (class: ExtractorIssue$$, method: convert signature: (LTopProperty;)LMyProp;) Accessing value from uninitialized register 5 + * at ExtractorIssue.main(ExtractorIssue.scala) + * at com.intellij.rt.execution.application.AppMain.main(AppMain.java:115)] + * + * If lines 15/16 are present, the compiler crashes: + * + * fatal error (server aborted): not enough arguments for method body%3: (val p: MyProp[java.lang.String])MyProp[_33]. + * Unspecified value parameter p. + */ +import language.experimental.magic +object Test { + + def main(args: Array[String]): Unit = { + convert(new SubclassProperty) + } + + def convert(prop: TopProperty): MyProp[_] = { + prop match { + + /////////////////////////////////////////////////////////////////////////////////////////////////////////////// + //case SubclassSecondMatch(p) => p // if these lines are present, the compiler crashes. If commented, unsafe byte + //case SecondMatch(p) => p // byte code is generated, which causes a java.lang.VerifyError at runtime + /////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + case SubclassMatch(p) => p + case StandardMatch(p) => p + } + } +} + +class TopProperty + +class StandardProperty extends TopProperty +class SubclassProperty extends StandardProperty + +class SecondProperty extends TopProperty +class SubclassSecondProperty extends StandardProperty + +trait MyProp[T] +case class MyPropImpl[T]() extends MyProp[T] + +object SubclassMatch { + + def unapply(prop: SubclassProperty) : MyProp[String]? = { + new MyPropImpl + } + + def apply(prop: MyProp[String]) : SubclassProperty = { + new SubclassProperty() + } +} + +object StandardMatch { + + def unapply(prop: StandardProperty) : MyProp[String]? = { + new MyPropImpl + } + + def apply(prop: MyProp[String]) : StandardProperty = { + new StandardProperty() + } +} + +object SubclassSecondMatch { + + def unapply(prop: SubclassSecondProperty) : MyProp[BigInt]? = { + new MyPropImpl + } + + def apply(prop: MyProp[String]) : SubclassSecondProperty = { + new SubclassSecondProperty() + } +} + +object SecondMatch { + + def unapply(prop: SecondProperty) : MyProp[BigInt]? = { + new MyPropImpl + } + + def apply(prop: MyProp[String]) : SecondProperty = { + new SecondProperty() + } +} 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..92f521d249b2 --- /dev/null +++ b/tests/run/t6111-maybe.scala @@ -0,0 +1,31 @@ +//> using options -Yexplicit-nulls +// SI-6675 DEPRECATED AUTO-TUPLING BECAUSE BAD IDEA -- MEAMAXIMACULPA +// TODO: remove this test case in 2.12, when the deprecation will go into effect and this will no longer compile +// slightly overkill, but a good test case for implicit resolution in extractor calls, +// along with the real fix: an extractor pattern with 1 sub-pattern should type check for all extractors +// that return Option[T], whatever T (even if it's a tuple) +import language.experimental.magic +import scala.magic.* +object Foo { + def unapply[S, T](scrutinee: S)(implicit evidence: FooHasType[S, T]): T? = scrutinee match { + case i: Int => Ok((i, i).asInstanceOf[T]) + } +} + +class FooHasType[S, T] +object FooHasType { + implicit object int extends 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/t7214-maybe.scala b/tests/run/t7214-maybe.scala new file mode 100644 index 000000000000..1d457c2f7e1a --- /dev/null +++ b/tests/run/t7214-maybe.scala @@ -0,0 +1,61 @@ +//> using options -Yexplicit-nulls +// scalajs: --skip + +// pattern matcher crashes here trying to synthesize an uneeded outer test. +// no-symbol does not have an owner +// at scala.reflect.internal.SymbolTable.abort(SymbolTable.scala:49) +// at scala.tools.nsc.Global.abort(Global.scala:253) +// at scala.reflect.internal.Symbols$NoSymbol.owner(Symbols.scala:3248) +// at scala.reflect.internal.Symbols$Symbol.effectiveOwner(Symbols.scala:678) +// at scala.reflect.internal.Symbols$Symbol.isDefinedInPackage(Symbols.scala:664) +// at scala.reflect.internal.TreeGen.mkAttributedSelect(TreeGen.scala:188) +// at scala.reflect.internal.TreeGen.mkAttributedRef(TreeGen.scala:124) +// at scala.tools.nsc.ast.TreeDSL$CODE$.REF(TreeDSL.scala:308) +// at scala.tools.nsc.typechecker.PatternMatching$TreeMakers$TypeTestTreeMaker$treeCondStrategy$.outerTest(PatternMatching.scala:1209) +import language.experimental.magic +class Crash { + type Alias = C#T + + val c = new C + val t = new c.T + + // Crash via a Typed Pattern... + (t: Any) match { + case e: Alias => + } + + // ... or via a Typed Extractor Pattern. + object Extractor { + def unapply(a: Alias): Any? = null + } + (t: Any) match { + case Extractor(_) => + case _ => + } + + // checking that correct outer tests are applied when + // aliases for path dependent types are involved. + val c2 = new C + type CdotT = c.T + type C2dotT = c2.T + + val outerField = t.getClass.getDeclaredFields.find(_.getName contains ("outer")).get + outerField.setAccessible(true) + + (t: Any) match { + case _: C2dotT => + println(s"!!! wrong match. t.outer=${outerField.get(t)} / c2 = $c2") // this matches on 2.10.0 + case _: CdotT => + case _ => + println(s"!!! wrong match. t.outer=${outerField.get(t)} / c = $c") + } +} + +class C { + class T +} + +object Test extends App { + new Crash +} + 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..002bb1f1b5a8 --- /dev/null +++ b/tests/run/tryPatternMatch-maybe.scala @@ -0,0 +1,143 @@ +//> using options -Yexplicit-nulls +// scalajs: --compliant-semantics + +import language.experimental.magic +import java.io.IOException +import java.util.concurrent.TimeoutException + +object IAE { + def unapply(e: Exception): String? = + if (e.isInstanceOf[IllegalArgumentException] && e.getMessage != null) 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/tuple-patterns-maybe.check b/tests/run/tuple-patterns-maybe.check new file mode 100644 index 000000000000..4aacb5183367 --- /dev/null +++ b/tests/run/tuple-patterns-maybe.check @@ -0,0 +1,9 @@ +2 +2 +3 +1 +10 +23 +1 +10 +23 diff --git a/tests/run/tuple-patterns-maybe.scala b/tests/run/tuple-patterns-maybe.scala new file mode 100644 index 000000000000..01485ac1d481 --- /dev/null +++ b/tests/run/tuple-patterns-maybe.scala @@ -0,0 +1,42 @@ +//> using options -Yexplicit-nulls +import language.experimental.magic +object Test extends App { + (1, 2) match { + case (1, x) => println(x) + } + val x: Any = (1, 2) + x match { + case (1, x) => println(x) + } + + //final class tuple2[+A, +B](_1: A, _2: B) + + final class TupleXXL1 private (es: Array[Object]) { + override def toString = elems.mkString("(", ",", ")") + def elems: Array[Object] = es + } + object TupleXXL1 { + def apply(elems: Array[Object]) = new TupleXXL1(elems.clone) + def apply(elems: Any*) = new TupleXXL1(elems.asInstanceOf[Seq[Object]].toArray) + def unapplySeq(x: TupleXXL1): Seq[Any]? = x.elems.toSeq + } + + val x3 = TupleXXL1(1, 2, 3) + x3 match { + case TupleXXL1(x1, x2, x3) => println(x3) + } + + val x23 = (1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23) + x23 match { + case (x1, x2, x3, x4, x5, x6, x7, x8, x9, x10, x11, x12, x13, x14, x15, x16, x17, x18, x19, x20, x21, x22, x23) => + println(x1) + println(x10) + println(x23) + } + (x23: Any) match { + case (x1, x2, x3, x4, x5, x6, x7, x8, x9, x10, x11, x12, x13, x14, x15, x16, x17, x18, x19, x20, x21, x22, x23) => + println(x1) + println(x10) + println(x23) + } +} diff --git a/tests/run/type-test-binding-maybe.check b/tests/run/type-test-binding-maybe.check new file mode 100644 index 000000000000..14e859cfe54b --- /dev/null +++ b/tests/run/type-test-binding-maybe.check @@ -0,0 +1,2 @@ +ok +9 diff --git a/tests/run/type-test-binding-maybe.scala b/tests/run/type-test-binding-maybe.scala new file mode 100644 index 000000000000..81643cde82b4 --- /dev/null +++ b/tests/run/type-test-binding-maybe.scala @@ -0,0 +1,36 @@ +//> using options -Yexplicit-nulls +import language.experimental.magic +import scala.reflect.TypeTest + +sealed trait Foo { + + type X + type Y <: X + + def x: X + + def f(y: Y) = println("ok") + + given TypeTest[X, Y] = new TypeTest { + def unapply(x: X): Option[x.type & Y] = + Some(x.asInstanceOf[x.type & Y]) + } + + object Z { + def unapply(arg: Y): Int? = 9 + } +} + +object Test { + def main(args: Array[String]): Unit = { + test(new Foo { type X = Int; type Y = Int; def x: X = 1 }) + } + + def test(foo: Foo): Unit = { + foo.x match { + case x @ foo.Z(i) => // `x` is refined to type `foo.Y` + foo.f(x) + println(i) + } + } +} 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..69e35ae5cd3f --- /dev/null +++ b/tests/run/type-test-nat-maybe.scala @@ -0,0 +1,132 @@ +//> using options -Yexplicit-nulls +import language.experimental.magic +import scala.reflect.TypeTest + +object Test { + def main(args: Array[String]): Unit = { + app(ClassNums) + app(IntNums) + } + + def app(peano: Peano): Unit = { + import peano.* + def divOpt(m: Nat, n: Nat): Option[(Nat, Nat)] = { + n match { + case Zero => None + case s @ Succ(_) => Some(safeDiv(m, s)) + } + } + val two = Succ(Succ(Zero)) + val five = Succ(Succ(Succ(two))) + println(divOpt(five, two)) + println(divOpt(two, five)) + println(divOpt(two, Zero)) + } +} + +trait Peano { + type Nat + + type Zero <: Nat + given TypeTest[Nat, Zero] = typeTestOfZero + + type Succ <: Nat + given TypeTest[Nat, Succ] = typeTestOfSucc + + def safeDiv(m: Nat, n: Succ): (Nat, Nat) + + protected def typeTestOfZero: TypeTest[Nat, Zero] + protected def typeTestOfSucc: TypeTest[Nat, Succ] + + implicit 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) 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..03107f40dce8 --- /dev/null +++ b/tests/run/unapply-maybe.scala @@ -0,0 +1,124 @@ +//> using options -Yexplicit-nulls +import language.experimental.magic +object Test { + def main(args: Array[String]): Unit = { + Foo.run() + Mas.run() + LisSeqArr.run() + StreamFoo.run() + Test1256.run() + } +} + +// this class is used for representation +class Bar { + var size: Int = 50 + var name: String = "medium" +} + +// test basic unapply for 0, 1 and 2 args and with precise type test +object Fii { + def unapply(x: Any): Boolean = x.isInstanceOf[Bar] +} +object Faa { + def unapply(x: Any): String? = if(x.isInstanceOf[Bar]) 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)(implicit 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") + implicit val bc: Int = 3 + assert(7 == (4 match { + case VarFoo(x) => x + })) + } +} + +// same, but now object is not top-level +object Mas { + object Gaz { + def unapply(x: Any): Product2[Int, String]? = x match { + case y: Baz => (y.size, y.name) + case _ => null + } + } + class Baz { + var size: Int = 60 + var name: String = "too large" + } + def run(): Unit = { + val b = new Baz + assert((60,"too large") == (b match { + case Gaz(s:Int, n:String) => (s,n) + })) + } +} + +object LisSeqArr { + def run(): Unit = { + assert((1,2) == ((List(1,2,3): Any) match { case List(x,y,_*) => (x,y)})) + assert((1,2) == ((List(1,2,3): Any) match { case Seq(x,y,_*) => (x,y)})) + } +} + +object StreamFoo { + def sum(lazyList: LazyList[Int]): Int = + lazyList match { + case ll if ll.isEmpty => 0 + case LazyList.cons(hd, tl) => hd + sum(tl) + } + def run(): Unit = { + val str: LazyList[Int] = List(1,2,3).to(LazyList) + assert(6 == sum(str)) + } +} + +object Test1256 { + class Sync { + def unapply(scrut: Any): Boolean = false + } + + class Buffer { + val Get = new Sync + val jp: PartialFunction[Any, Any] = { + case Get() => + } + } + + def run(): Unit = { + assert(!(new Buffer).jp.isDefinedAt(42)) + } +} diff --git a/tests/run/unapply-tparam-maybe.scala b/tests/run/unapply-tparam-maybe.scala new file mode 100644 index 000000000000..cea24b7e6b82 --- /dev/null +++ b/tests/run/unapply-tparam-maybe.scala @@ -0,0 +1,38 @@ +//> using options -Yexplicit-nulls +import language.experimental.magic +class Foo[T] { + def unapply(x: Int): Int? = 4 +} + +object Foo { + def unapply[T](x: T): Int? = 5 +} + +object Bar { + def unapply[T](x: T): Int? = 5 +} + +class Baz[T] { + def unapply(x: Int): Int? = 4 +} + +object Baz + + +object Test extends App { + 1 match { + case Foo(x) => assert(x == 5) + } + + 1 match { + case Foo[Int](x) => assert(x == 5) // Type params on object takes precedence + } + + 1 match { + case Bar[Int](x) => assert(x == 5) + } + + 1 match { + case Baz[Int](x) => assert(x == 4) // Otherwise type params are for the class + } +} \ No newline at end of file diff --git a/tests/run/unchecked-patterns-maybe.scala b/tests/run/unchecked-patterns-maybe.scala new file mode 100644 index 000000000000..adb96bed96ac --- /dev/null +++ b/tests/run/unchecked-patterns-maybe.scala @@ -0,0 +1,13 @@ +//> using options -Yexplicit-nulls +import language.experimental.magic +object Test extends App { + val x: Int = 2: @unchecked + val (y1: Some[Int]) = Some(1): Option[Int] @unchecked + + val a :: as = List(1, 2, 3): @unchecked + val lst @ b :: bs = List(1, 2, 3): @unchecked + val (1, c) = (1, 2): @unchecked + + object Positive { def unapply(i: Int): Int? = if i > 0 then i else null } + val Positive(p) = 5: @unchecked +} \ No newline at end of file diff --git a/tests/run/virtpatmat_stringinterp-maybe.check b/tests/run/virtpatmat_stringinterp-maybe.check new file mode 100644 index 000000000000..7927f4f2d95a --- /dev/null +++ b/tests/run/virtpatmat_stringinterp-maybe.check @@ -0,0 +1 @@ +Node(1) diff --git a/tests/run/virtpatmat_stringinterp-maybe.scala b/tests/run/virtpatmat_stringinterp-maybe.scala new file mode 100644 index 000000000000..0106050d5621 --- /dev/null +++ b/tests/run/virtpatmat_stringinterp-maybe.scala @@ -0,0 +1,18 @@ +//> using options -Yexplicit-nulls + +import language.experimental.magic +import scala.language.implicitConversions + +object Test extends App { + case class Node(x: Int) + + implicit def sc2xml(sc: StringContext): XMLContext = new XMLContext(sc) + class XMLContext(sc: StringContext) { + object xml { + def unapplySeq(xml: Node): Seq[Node]? = List(Node(1)) + } + } + + val x: Node = Node(0) + x match { case xml"""""" => println(a) } +} 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..147a457e9cc8 --- /dev/null +++ b/tests/run/virtpatmat_unapply-maybe.scala @@ -0,0 +1,34 @@ +//> using options -Yexplicit-nulls +import language.experimental.magic +class IntList(val hd: Int, val tl: IntList) +object NilIL extends IntList(0, null.asInstanceOf[IntList]) +object IntList { + def unapply(il: IntList): (Int, IntList)? = if(il eq NilIL) null else (il.hd, il.tl) + def apply(x: Int, xs: IntList) = new IntList(x, xs) +} + +object Test extends App { + IntList(1, IntList(2, NilIL)) match { + case IntList(a1, IntList(a2, IntList(a3, y))) => println(a1 + a2 + a3) + case IntList(x, y) => println(x) + } + + IntList(1, IntList(2, IntList(3, NilIL))) match { + case IntList(a1, IntList(a2, IntList(a3, y))) => println(a1 + a2 + a3) + case IntList(x, y) => println(x) + } +} + +// ((x1: IntList) => IntList.unapply(x1).flatMap(((x4: (Int, IntList)) => IntList.unapply(x4._2).flatMap(((x5: (Int, IntList)) => IntList.unapply(x5._2).flatMap(((x6: (Int, IntList)) => implicitly[Predef.MatchingStrategy[Option]].success(Predef.println(x4._1.+(x5._1).+(x6._1))))))))).orElse(IntList.unapply(x1).flatMap(((x7: (Int, IntList)) => implicitly[scala.Predef.MatchingStrategy[Option]].success(Predef.println(x7._1))))).orElse(implicitly[scala.Predef.MatchingStrategy[Option]].fail))(IntList.apply(1, IntList.apply(2, IntList.apply(3, null)))) + +/* + ((x1: IntList) => + IntList.this.unapply(x1).flatMap[Int](((x4: (Int, IntList)) => + IntList.this.unapply(x4._2).flatMap[Int](((x5: (Int, IntList)) => + IntList.this.unapply(x5._2).flatMap[Int](((x6: (Int, IntList)) => + Predef.this.implicitly[scala.Predef.MatchingStrategy[Option]](scala.this.Predef.OptionMatching).success[Int](x6._1))))))).orElse[Int]( + IntList.this.unapply(x1).flatMap[Int](((x7: (Int, IntList)) => + Predef.this.implicitly[scala.Predef.MatchingStrategy[Option]](scala.this.Predef.OptionMatching).success[Int](x7._1)))).orElse[Int]( + Predef.this.implicitly[scala.Predef.MatchingStrategy[Option]](scala.this.Predef.OptionMatching).fail) + ).apply(IntList.apply(1, null)) +*/ diff --git a/tests/warn/i12253-maybe.check b/tests/warn/i12253-maybe.check new file mode 100644 index 000000000000..8d9f140292e1 --- /dev/null +++ b/tests/warn/i12253-maybe.check @@ -0,0 +1,13 @@ +-- [E092] Pattern Match Unchecked Warning: tests/warn/i12253-maybe.scala:15:10 ----------------------------------------- +15 | case extractors.InlinedLambda(_, Select(_, name)) => Expr(name) // warn // warn + | ^ + |the type test for extractors.q2.reflect.Term cannot be checked at runtime because it refers to an abstract type member or type parameter + | + | longer explanation available when compiling with `-explain` +-- [E092] Pattern Match Unchecked Warning: tests/warn/i12253-maybe.scala:15:38 ----------------------------------------- +15 | case extractors.InlinedLambda(_, Select(_, name)) => Expr(name) // warn // warn + | ^ + |the type test for q1.reflect.Select cannot be checked at runtime because it refers to an abstract type member or type parameter + | + | longer explanation available when compiling with `-explain` +there was 1 deprecation warning; re-run with -deprecation for details diff --git a/tests/warn/i12253-maybe.scala b/tests/warn/i12253-maybe.scala new file mode 100644 index 000000000000..6ad4954a70ec --- /dev/null +++ b/tests/warn/i12253-maybe.scala @@ -0,0 +1,33 @@ +//> using options -Yexplicit-nulls + + +import language.experimental.magic +import scala.quoted.{given, *} +import deriving.*, compiletime.* + +object MacroUtils: + transparent inline def extractNameFromSelector[To, T](inline code: To => T) = ${extractNameFromSelectorImpl('code)} + + def extractNameFromSelectorImpl[To: Type, T: Type](code: Expr[To => T])(using q1: Quotes): Expr[String] = + import quotes.reflect.* + val extractors = new Extractors + code.asTerm match + case extractors.InlinedLambda(_, Select(_, name)) => Expr(name) // warn // warn + case t => report.throwError(s"Illegal argument to extractor: ${code.show}, in tasty: $t") + + class Extractors(using val q2: Quotes): + //attempt to strip away consecutive inlines in AST and extract only final lambda + import quotes.reflect.* + + object InlinedLambda: + def unapply(arg: Term): (List[ValDef], Term)? = + arg match + case Inlined(_, _, Lambda(vals, term)) => (vals, term) + case Inlined(_, _, nested) => InlinedLambda.unapply(nested) + case t => null + end InlinedLambda + + end Extractors +end MacroUtils + +// nopos-warn deprecation \ No newline at end of file diff --git a/tests/warn/maybe-typetest.check b/tests/warn/maybe-typetest.check index 0855780529a9..341dfaa39b26 100644 --- a/tests/warn/maybe-typetest.check +++ b/tests/warn/maybe-typetest.check @@ -12,6 +12,12 @@ | the type test for String? cannot be checked at runtime because its type arguments can't be determined from T | | longer explanation available when compiling with `-explain` +-- [E092] Pattern Match Unchecked Warning: tests/warn/maybe-typetest.scala:10:9 ---------------------------------------- +10 | case y: String? => println(y) // warn typetest? + | ^ + |the type test for String? cannot be checked at runtime because its type arguments can't be determined from T ? Nothing + | + | longer explanation available when compiling with `-explain` -- [E092] Pattern Match Unchecked Warning: tests/warn/maybe-typetest.scala:13:9 ---------------------------------------- 13 | case y: Option[String] => println(y)// warn typetest | ^ diff --git a/tests/warn/maybe-typetest.scala b/tests/warn/maybe-typetest.scala index 88e4b5087b37..5a8013a6b121 100644 --- a/tests/warn/maybe-typetest.scala +++ b/tests/warn/maybe-typetest.scala @@ -7,7 +7,7 @@ def Test[T](x: T) = case y: String? => println(y) // warn typetest case _ => Ok(x) match - case y: String? => println(y) // TODO why not warn typetest? + case y: String? => println(y) // warn typetest? case _ => // warn unreachable x match case y: Option[String] => println(y)// warn typetest diff --git a/tests/warn/strict-pattern-bindings-3.2-maybe.scala b/tests/warn/strict-pattern-bindings-3.2-maybe.scala new file mode 100644 index 000000000000..b5c36f0364a5 --- /dev/null +++ b/tests/warn/strict-pattern-bindings-3.2-maybe.scala @@ -0,0 +1,39 @@ +//> using options -Yexplicit-nulls + +// These tests should fail under -Werror with source version source version 3.2 or later +import language.experimental.magic +import language.`3.2` + +object Test: + // from filtering-fors.scala + val xs: List[AnyRef] = ??? + + for ((x: String) <- xs) do () // warn + for (y@ (x: String) <- xs) do () // warn + for ((x, y) <- xs) do () // warn + + for ((x: String) <- xs if x.isEmpty) do () // warn + for ((x: String) <- xs; y = x) do () // warn + for ((x: String) <- xs; (y, z) <- xs) do () // warn // warn + for (case (x: String) <- xs; (y, z) <- xs) do () // warn + for ((x: String) <- xs; case (y, z) <- xs) do () // warn + + val pairs: List[AnyRef] = List((1, 2), "hello", (3, 4)) + for ((x, y) <- pairs) yield (y, x) // warn + + // from unchecked-patterns.scala + val y :: ys = List(1, 2, 3) // warn + val (1, c) = (1, 2) // warn + val 1 *: cs = 1 *: Tuple() // warn + + val (_: Int | _: AnyRef) = ??? : AnyRef // warn + + val 1 = 2 // warn + + 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 // warn + val Some(s1) = Option(1) // warn From f3f7d25c3feb6e798a0137ea74c9f6c3e24f4069 Mon Sep 17 00:00:00 2001 From: odersky Date: Wed, 19 Aug 2026 19:17:44 +0200 Subject: [PATCH 15/28] Make Err constructor inline Needed to avoid erasure difference between bootstrapped and non-bootstrapped compilers. --- library/src/scala/magic/Err.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/library/src/scala/magic/Err.scala b/library/src/scala/magic/Err.scala index 8dc164204a8e..00510c48f943 100644 --- a/library/src/scala/magic/Err.scala +++ b/library/src/scala/magic/Err.scala @@ -6,7 +6,7 @@ import annotation.experimental @experimental object Err: - def apply[E](e: E): Maybe[Nothing, E] = + inline def apply[E](e: E): Maybe[Nothing, E] = (if e == () then null else new runtime.Fail(e)) .asInstanceOf[Maybe[Nothing, E]] From 4d32f70af60902e379910bbf40293cc9b7a70cfd Mon Sep 17 00:00:00 2001 From: odersky Date: Thu, 20 Aug 2026 11:43:25 +0200 Subject: [PATCH 16/28] Make T? a subtype of T | Null if T is not nullable So for T not nullable, we now have T? =:= T | Null. --- .../src/dotty/tools/dotc/core/TypeComparer.scala | 8 ++++++++ tests/pos/maybe-subtyping.scala | 14 ++++++++++++++ 2 files changed, 22 insertions(+) create mode 100644 tests/pos/maybe-subtyping.scala diff --git a/compiler/src/dotty/tools/dotc/core/TypeComparer.scala b/compiler/src/dotty/tools/dotc/core/TypeComparer.scala index 297525d7e225..8d48feb5cf3a 100644 --- a/compiler/src/dotty/tools/dotc/core/TypeComparer.scala +++ b/compiler/src/dotty/tools/dotc/core/TypeComparer.scala @@ -854,6 +854,14 @@ class TypeComparer(@constructorOnly initctx: Context) extends ConstraintHandling return recur(tp1, OrType(tp21, tp221, tp2.isSoft)) && recur(tp1, OrType(tp21, tp222, tp2.isSoft)) case _ => } + tp2 match + case OrNull(tp2a) => + tp1w match + case MagicMaybeType(tp1a, errArg, _) => + if errArg.isRef(defn.UnitClass) && tp1a.isNotNull then + return recur(tp1a, tp2a) + case _ => + case _ => either(recur(tp1, tp21), recur(tp1, tp22)) || fourthTry case tp2: MatchType => val reduced = tp2.reduced diff --git a/tests/pos/maybe-subtyping.scala b/tests/pos/maybe-subtyping.scala new file mode 100644 index 000000000000..f3a813873627 --- /dev/null +++ b/tests/pos/maybe-subtyping.scala @@ -0,0 +1,14 @@ +//> using options -Yexplicit-nulls +import language.experimental.magic + +var x: String? = "hi" +var y: String | Null = "ho" + +def Test = + x = y + y = x + + val z1 = if ??? then x else y + val _: String? = z1 + val z2 = if ??? then y else x + val _: String? = z2 From dca441479d1838fe42bf0b7ce33fa0b01129fca2 Mon Sep 17 00:00:00 2001 From: odersky Date: Thu, 20 Aug 2026 17:13:57 +0200 Subject: [PATCH 17/28] Optimize redundant dual tests in pattern matcher --- .../tools/dotc/transform/PatternMatcher.scala | 102 +++++++++++++++--- tests/pos/maybe-subtyping.scala | 5 + tests/run/maybe.scala | 2 +- tests/run/orelse.check | 33 ++++++ tests/run/orelse.scala | 21 ++++ 5 files changed, 149 insertions(+), 14 deletions(-) create mode 100644 tests/run/orelse.check diff --git a/compiler/src/dotty/tools/dotc/transform/PatternMatcher.scala b/compiler/src/dotty/tools/dotc/transform/PatternMatcher.scala index 5e2c2716c436..ff1d810fb286 100644 --- a/compiler/src/dotty/tools/dotc/transform/PatternMatcher.scala +++ b/compiler/src/dotty/tools/dotc/transform/PatternMatcher.scala @@ -202,7 +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 IsFailTest extends Test // scrutinee.isInstanceOf[Fail] + 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) @@ -437,18 +438,18 @@ object PatternMatcher { else unapp match case Apply(fn, arg :: Nil) if fn.symbol == defn.Magic_OkUnapply => - unappResultPlan(unapp, args, arg.symbol, unappType, wasUnaryNamedTupleSelectArgForNamedTuple) + unappResultPlan(unapp, args, arg.symbol, unappType, wasUnaryNamedTupleSelectArgForNamedTuple, IsOkTest) case Apply(fn, arg :: Nil) if fn.symbol == defn.Magic_ErrUnapply => - unappResultPlan(unapp, args, arg.symbol, unappType, wasUnaryNamedTupleSelectArgForNamedTuple, isErrMatch = true) + unappResultPlan(unapp, args, arg.symbol, unappType, wasUnaryNamedTupleSelectArgForNamedTuple, IsErrTest) case _ => letAbstract(unapp): unappResult => - unappResultPlan(unapp, args, unappResult, unappType, wasUnaryNamedTupleSelectArgForNamedTuple) + unappResultPlan(unapp, args, unappResult, unappType, wasUnaryNamedTupleSelectArgForNamedTuple, NonEmptyTest) } def unappResultPlan( unapp: Tree, args: List[Tree], unappResult: Symbol, unappType: Type, wasUnaryNamedTupleSelectArgForNamedTuple: Boolean, - isErrMatch: Boolean = false): Plan = { + nonEmptyTest: Test): Plan = { val isUnapplySeq = unapp.symbol.name == nme.unapplySeq if isProductMatch(unappType, args.length) && !isUnapplySeq then val selectors = productSelectors(unappType).take(args.length) @@ -467,7 +468,7 @@ object PatternMatcher { else { assert(isGetMatch(unappType)) val argsPlan = { - val get = getOfGetMatch(ref(unappResult), isErrMatch) + val get = getOfGetMatch(ref(unappResult), nonEmptyTest == IsErrTest) if (isUnapplySeq) letAbstract(get) { getResult => if unapplySeqTypeElemTp(get.tpe).exists then @@ -500,9 +501,7 @@ object PatternMatcher { matchArgsPlan(selectors, args, onSuccess) } } - TestPlan( - if isErrMatch then IsFailTest else NonEmptyTest, - unappResult, unapp.span, argsPlan) + TestPlan(nonEmptyTest, unappResult, unapp.span, argsPlan) } } @@ -849,15 +848,85 @@ object PatternMatcher { Inliner(plan) } + /** Drop tests that are oposites of previously established tests. + * + * When we have the following shape: + * + * if testA then plan1 + * if testB then plan2 + * nextPlan? + * + * where testA is a dual of testB and plan1 is test-free, transform it to + * + * if testA then plan1 + * plan2 + * nextPlan? + * + * "Dual" means: + * - one of the tests is an IsOKTest, the other is an + * IsErrTest or an "== null" test, + * - the two tests have the same scrutinee. + */ + def dropOpposites(plan: Plan): Plan = { + + object Dropper extends PlanTransform { + + def isTestFree(plan: Plan): Boolean = plan match + case _: TestPlan => false + case _: ReturnPlan => true + case _: ResultPlan => true + case LetPlan(_, expr) => isTestFree(expr) + case LabeledPlan(_, expr) => isTestFree(expr) + case SeqPlan(hd, tl) => isTestFree(hd) && isTestFree(tl) + + def isDual(test1: Test, test2: Test) = test1 match + case IsOkTest => + test2 match + case IsErrTest => true + case EqualTest(tree) => tree.tpe.isRef(defn.NullClass) + case _ => false + case _ => + false + + override def apply(plan: SeqPlan): Plan = { + if Feature.magicEnabled then + plan.head = apply(plan.head) + plan.tail = apply(plan.tail) + plan.head match + case TestPlan(test1, scrut1, _, follow1) => + def tryDropTest(plan: Plan) = plan match + case TestPlan(test2, scrut2, _, follow2) => + (scrut1, scrut2) match + case (_: Ident, _: Ident) + if scrut1.symbol == scrut2.symbol + && (isDual(test1, test2) || isDual(test2, test1)) + && isTestFree(follow1) => + follow2 + case _ => plan + case _ => + plan + + plan.tail match + case tail @ SeqPlan(tailHead, tailTail) => + tail.head = tryDropTest(tailHead) + case tail => + plan.tail = tryDropTest(tail) + case _ => + plan + } + } + Dropper(plan) + } + // ----- Generating trees from plans --------------- /** The condition a test plan rewrites to */ private def emitCondition(plan: TestPlan): Tree = val scrutinee = plan.scrutinee (plan.test: @unchecked) match - case NonEmptyTest => + case NonEmptyTest | IsOkTest => scrutinee.tpe.widenDealias match - case AppliedType(tycon, _ :: errArg :: Nil) if tycon.isRef(defn.MagicMaybeClass) => + case MagicMaybeType(_, errArg, _) => val test = scrutinee.nullTest(cond = false) if errArg.isRef(defn.UnitClass) then test @@ -867,7 +936,13 @@ object PatternMatcher { scrutinee .select(nme.isEmpty, _.info.isParameterless) .select(nme.UNARY_!, _.info.isParameterless)) - case IsFailTest => + case IsOkTest => + val MagicMaybeType(_, errArg, _) = scrutinee.tpe.widenDealias.runtimeChecked + val notNull = scrutinee.nullTest(cond = false) + if errArg.isRef(defn.UnitClass) + then notNull + else notNull.and(scrutinee.isInstance(defn.MagicFailClass.typeRef).not) + case IsErrTest => val MagicMaybeType(_, _, nullable) = scrutinee.tpe.widen.runtimeChecked val typeTest = scrutinee.isInstance(defn.MagicFailClass.typeRef) if nullable then scrutinee.nullTest(cond = true).or(typeTest) @@ -1199,7 +1274,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/pos/maybe-subtyping.scala b/tests/pos/maybe-subtyping.scala index f3a813873627..15440006d575 100644 --- a/tests/pos/maybe-subtyping.scala +++ b/tests/pos/maybe-subtyping.scala @@ -12,3 +12,8 @@ def Test = val _: String? = z1 val z2 = if ??? then y else x val _: String? = z2 + + def foo[T](x: T, y: T): T = x + + val z3 = foo(x, y) + val _: String? = z3 diff --git a/tests/run/maybe.scala b/tests/run/maybe.scala index 4395e6da051c..854c532e0af5 100644 --- a/tests/run/maybe.scala +++ b/tests/run/maybe.scala @@ -9,8 +9,8 @@ class C: case null => None def toOptionStr(x: String?): Option[String] = x match - case Ok(y) => Some(y) case null => None + case Ok(y) => Some(y) def toOption[T](x: T?): Option[T] = x match case Ok(y) => Some(y) 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 index af2a2fb47464..40d6d6ecb727 100644 --- a/tests/run/orelse.scala +++ b/tests/run/orelse.scala @@ -13,8 +13,13 @@ def toEitherIntStr(x: Int ? String): Either[String, Int] = x match 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 @@ -47,15 +52,27 @@ def f[T, E](x: T, e: E) = 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") @@ -91,12 +108,16 @@ def polyTest2[T](x: T) = x match @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("") From 2a69fb96162a8752f8a825efd9b06019be98d8ae Mon Sep 17 00:00:00 2001 From: odersky Date: Thu, 20 Aug 2026 18:08:43 +0200 Subject: [PATCH 18/28] Disable orelse test under scalajs --- tests/run/orelse.scala | 3 +++ 1 file changed, 3 insertions(+) diff --git a/tests/run/orelse.scala b/tests/run/orelse.scala index 40d6d6ecb727..497130b0afe9 100644 --- a/tests/run/orelse.scala +++ b/tests/run/orelse.scala @@ -1,3 +1,6 @@ +// scalajs: --skip +// scalajs needs to be diasabled since `null` gives an Err(undefined) instead of an Err(()) +// TODO: Figure out how to fix this under scalajs //> using options -Yexplicit-nulls import language.experimental.magic import scala.magic.* From fdb1883f588ba13234fa1c6cbfa6d27a0f98a517 Mon Sep 17 00:00:00 2001 From: odersky Date: Thu, 20 Aug 2026 18:50:11 +0200 Subject: [PATCH 19/28] Move Maybe class to magic.compiletime package We don't need it directly, since source programs use `?` instead. Also: Fix printing of maybe type trees. --- compiler/src/dotty/tools/dotc/core/Definitions.scala | 8 ++++---- .../src/dotty/tools/dotc/printing/RefinedPrinter.scala | 6 ++++++ library/src/scala/magic/Err.scala | 6 ++++++ library/src/scala/magic/Ok.scala | 1 + library/src/scala/magic/{ => compiletime}/Maybe.scala | 2 +- .../{compiletime.scala => compiletime/package.scala} | 5 +++-- tests/pos/t8128-maybe.scala | 1 + .../stdlibExperimentalDefinitions.scala | 4 +++- 8 files changed, 25 insertions(+), 8 deletions(-) rename library/src/scala/magic/{ => compiletime}/Maybe.scala (88%) rename library/src/scala/magic/{compiletime.scala => compiletime/package.scala} (84%) diff --git a/compiler/src/dotty/tools/dotc/core/Definitions.scala b/compiler/src/dotty/tools/dotc/core/Definitions.scala index 57e4f57c0468..307ff6b81df2 100644 --- a/compiler/src/dotty/tools/dotc/core/Definitions.scala +++ b/compiler/src/dotty/tools/dotc/core/Definitions.scala @@ -483,7 +483,7 @@ class Definitions { // Magic stuff @tu lazy val MagicPackageClass: ClassSymbol = requiredPackage("scala.magic").moduleClass.asClass - @tu lazy val MagicMaybeClass: ClassSymbol = requiredClass("scala.magic.Maybe") + @tu lazy val MagicMaybeClass: ClassSymbol = requiredClass("scala.magic.compiletime.Maybe") @tu lazy val MagicValidClass: ClassSymbol = requiredClass("scala.magic.runtime.Valid") @tu lazy val MagicFailClass: ClassSymbol = requiredClass("scala.magic.runtime.Fail") @@ -493,9 +493,9 @@ class Definitions { @tu lazy val MagicErrModule: Symbol = requiredModule("scala.magic.Err") @tu lazy val Magic_ErrUnapply: Symbol = MagicErrModule.requiredMethod(nme.unapply) - @tu lazy val MagicCompiletimeModule: Symbol = requiredModule("scala.magic.compiletime") - @tu lazy val Magic_spec: Symbol = MagicCompiletimeModule.requiredMethod("$spec") - @tu lazy val Magic_wrappedType: Symbol = MagicCompiletimeModule.requiredMethod("$wrappedType") + @tu lazy val MagicCompiletimePackage: Symbol = requiredPackage("scala.magic.compiletime") + @tu lazy val Magic_spec: Symbol = MagicCompiletimePackage.requiredMethod("$spec") + @tu lazy val Magic_wrappedType: Symbol = MagicCompiletimePackage.requiredMethod("$wrappedType") // More synthetic symbols @tu lazy val andType: TypeSymbol = enterBinaryAlias(tpnme.AND, AndType(_, _)) diff --git a/compiler/src/dotty/tools/dotc/printing/RefinedPrinter.scala b/compiler/src/dotty/tools/dotc/printing/RefinedPrinter.scala index e7fe09ba5911..e3c85eecbc94 100644 --- a/compiler/src/dotty/tools/dotc/printing/RefinedPrinter.scala +++ b/compiler/src/dotty/tools/dotc/printing/RefinedPrinter.scala @@ -644,6 +644,12 @@ class RefinedPrinter(_ctx: Context) extends PlainPrinter(_ctx) { && !printDebug && tree.typeOpt.exists then toText(tree.typeOpt) + else if tpt.symbol == defn.MagicMaybeClass && args.length == 2 then + if unsplice(args(1)).symbol == defn.UnitClass then + toTextLocal(args(0)) ~ "?" + else + changePrec(InfixPrec): + toText(args(0)) ~ " ? " ~ toText(args(1)) else args match case arg :: _ if arg.isTerm => toTextLocal(tpt) ~ "(" ~ Text(args.map(argText), ", ") ~ ")" diff --git a/library/src/scala/magic/Err.scala b/library/src/scala/magic/Err.scala index 00510c48f943..1abc7be15628 100644 --- a/library/src/scala/magic/Err.scala +++ b/library/src/scala/magic/Err.scala @@ -2,10 +2,16 @@ package scala.magic import language.experimental.magic import scala.magic.runtime +import scala.magic.compiletime.Maybe import annotation.experimental @experimental object Err: + + /** `inline` neded since nonbootrapped 3.9.0 compiler + * uses a different erasure for Maybe than boostrapped + * 3.10.0 compiler. + */ inline def apply[E](e: E): Maybe[Nothing, E] = (if e == () then null else new runtime.Fail(e)) .asInstanceOf[Maybe[Nothing, E]] diff --git a/library/src/scala/magic/Ok.scala b/library/src/scala/magic/Ok.scala index 1fdceb886bb2..c3fc24cc7dc3 100644 --- a/library/src/scala/magic/Ok.scala +++ b/library/src/scala/magic/Ok.scala @@ -1,6 +1,7 @@ package scala.magic import scala.magic.runtime.Valid +import scala.magic.compiletime.Maybe import annotation.experimental @experimental diff --git a/library/src/scala/magic/Maybe.scala b/library/src/scala/magic/compiletime/Maybe.scala similarity index 88% rename from library/src/scala/magic/Maybe.scala rename to library/src/scala/magic/compiletime/Maybe.scala index 77f946d0588c..6e3216a25e3f 100644 --- a/library/src/scala/magic/Maybe.scala +++ b/library/src/scala/magic/compiletime/Maybe.scala @@ -1,4 +1,4 @@ -package scala.magic +package scala.magic.compiletime import scala.magic.runtime.Valid import annotation.experimental diff --git a/library/src/scala/magic/compiletime.scala b/library/src/scala/magic/compiletime/package.scala similarity index 84% rename from library/src/scala/magic/compiletime.scala rename to library/src/scala/magic/compiletime/package.scala index 8b065772732d..cd3fe8f5c7f6 100644 --- a/library/src/scala/magic/compiletime.scala +++ b/library/src/scala/magic/compiletime/package.scala @@ -2,14 +2,15 @@ package scala.magic import annotation.experimental -@experimental -object compiletime { +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/tests/pos/t8128-maybe.scala b/tests/pos/t8128-maybe.scala index 420aade7a992..34e4d66e5b72 100644 --- a/tests/pos/t8128-maybe.scala +++ b/tests/pos/t8128-maybe.scala @@ -1,6 +1,7 @@ //> using options -Yexplicit-nulls import language.experimental.magic import scala.magic.* +import compiletime.Maybe object G { def unapply(m: Any): Maybe[?, Unit] = Ok("") } diff --git a/tests/run-tasty-inspector/stdlibExperimentalDefinitions.scala b/tests/run-tasty-inspector/stdlibExperimentalDefinitions.scala index b8b11fe9beb1..e69e50f7a60b 100644 --- a/tests/run-tasty-inspector/stdlibExperimentalDefinitions.scala +++ b/tests/run-tasty-inspector/stdlibExperimentalDefinitions.scala @@ -102,11 +102,13 @@ val experimentalDefinitionInLibrary = Set( "scala.specialize.Specialized$", // New feature: magic - "scala.magic.Maybe", "scala.magic.Ok", "scala.magic.Ok$", "scala.magic.compiletime", "scala.magic.compiletime$", + "scala.magic.compiletime.Maybe", + "scala.magic.compiletime.package$.$spec", + "scala.magic.compiletime.package$.$wrappedType", "scala.magic.runtime.Valid", "scala.magic.Err", "scala.magic.Err$", From 40fd4f146c5216cd6299b1335cbb7c894ae240ca Mon Sep 17 00:00:00 2001 From: odersky Date: Fri, 21 Aug 2026 10:36:53 +0200 Subject: [PATCH 20/28] Allow widening also for binary orelse types --- .../dotty/tools/dotc/core/TypeComparer.scala | 6 +++--- .../src/dotty/tools/dotc/core/Types.scala | 19 ++++++++++++------- .../tools/dotc/transform/PatternMatcher.scala | 9 ++++----- tests/run/orelse.scala | 2 +- 4 files changed, 20 insertions(+), 16 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/core/TypeComparer.scala b/compiler/src/dotty/tools/dotc/core/TypeComparer.scala index 8d48feb5cf3a..5a50c1a05d4a 100644 --- a/compiler/src/dotty/tools/dotc/core/TypeComparer.scala +++ b/compiler/src/dotty/tools/dotc/core/TypeComparer.scala @@ -858,7 +858,7 @@ class TypeComparer(@constructorOnly initctx: Context) extends ConstraintHandling case OrNull(tp2a) => tp1w match case MagicMaybeType(tp1a, errArg, _) => - if errArg.isRef(defn.UnitClass) && tp1a.isNotNull then + if errArg.isRef(defn.UnitClass) && tp1a.isNotNullNorMaybe then return recur(tp1a, tp2a) case _ => case _ => @@ -1518,8 +1518,8 @@ class TypeComparer(@constructorOnly initctx: Context) extends ConstraintHandling /** T <: T? if T is not null */ def byMaybeWidening: Boolean = tp2 match - case MagicMaybeType(res2, err2, _) if tp1.isNotNull => - recur(tp1, res2) && isSubType(err2, defn.UnitType) + case MagicMaybeType(res2, err2, _) if tp1.isNotNullNorMaybe => + recur(tp1, res2) case _ => false tycon2 match { diff --git a/compiler/src/dotty/tools/dotc/core/Types.scala b/compiler/src/dotty/tools/dotc/core/Types.scala index d7c9443ad10e..3b8f885c2276 100644 --- a/compiler/src/dotty/tools/dotc/core/Types.scala +++ b/compiler/src/dotty/tools/dotc/core/Types.scala @@ -381,17 +381,22 @@ object Types extends TypeUtils { } /** Is this type guaranteed not to have `null` as a value? */ - final def isNotNull(using Context): Boolean = this match { + final def isNotNull(norMaybe: Boolean)(using Context): Boolean = this match case tp: ConstantType => tp.value.value != null case tp: FlexibleType => false case tp: ClassInfo => !tp.cls.isNullableClass && !tp.isNothingType - case MagicMaybeType(_, _, nullable) => !nullable - case tp: TypeBounds => tp.hi.isNotNull - case tp: TypeProxy => tp.underlying.isNotNull - case AndType(tp1, tp2) => tp1.isNotNull || tp2.isNotNull - case OrType(tp1, tp2) => tp1.isNotNull && tp2.isNotNull + case MagicMaybeType(_, _, nullable) => !norMaybe && !nullable + case tp: TypeBounds => tp.hi.isNotNull(norMaybe) + case tp: TypeProxy => tp.underlying.isNotNull(norMaybe) + case AndType(tp1, tp2) => tp1.isNotNull(norMaybe) || tp2.isNotNull(norMaybe) + case OrType(tp1, tp2) => tp1.isNotNull(norMaybe) && tp2.isNotNull(norMaybe) case _ => false - } + + /** Is this type guaranteed not to have `null` as a value? */ + final def isNotNull(using Context): Boolean = isNotNull(norMaybe = false) + + /** Is this type guaranteed not to have `null` or `Fail(...)` as a value? */ + final def isNotNullNorMaybe(using Context): Boolean = isNotNull(norMaybe = true) /** Is `null` a value of this type? */ def admitsNull(using Context): Boolean = diff --git a/compiler/src/dotty/tools/dotc/transform/PatternMatcher.scala b/compiler/src/dotty/tools/dotc/transform/PatternMatcher.scala index ff1d810fb286..bb88f348d185 100644 --- a/compiler/src/dotty/tools/dotc/transform/PatternMatcher.scala +++ b/compiler/src/dotty/tools/dotc/transform/PatternMatcher.scala @@ -390,11 +390,10 @@ object PatternMatcher { val MagicMaybeType(_, errArg, nullable) = gm.tpe.widen.runtimeChecked val select = gm.asInstance(defn.MagicFailClass.typeRef.appliedTo(defn.AnyType)) .select(nme.elem) - if nullable - then If( - gm.nullTest(cond = true), - unitLiteral.asInstance(errArg), - select) + if nullable then + If(gm.nullTest(cond = true), + unitLiteral.asInstance(errArg), + select) else select else val validTpe = defn.MagicValidClass.typeRef diff --git a/tests/run/orelse.scala b/tests/run/orelse.scala index 497130b0afe9..b5abf5957ffd 100644 --- a/tests/run/orelse.scala +++ b/tests/run/orelse.scala @@ -50,7 +50,7 @@ def f[T, E](x: T, e: E) = val x4 = toEither(Err("bad")) val y1 = toEitherIntStr(Ok(1)) - val y3 = toEitherIntStr(Ok(22)) + val y3 = toEitherIntStr(22) val y4 = toEitherIntStr(Err("bad")) val z1 = toEither(Ok(x)) From e8f4685bc4b82febbfb6883174b6937147b4e133 Mon Sep 17 00:00:00 2001 From: odersky Date: Fri, 21 Aug 2026 12:53:38 +0200 Subject: [PATCH 21/28] Direct style core infrastructure Also: drop overfitted condition by Claude in space engine. --- .../tools/dotc/transform/patmat/Space.scala | 5 +-- .../scala/magic/package.scala | 34 +++++++++++++++++ library/src/scala/magic/package.scala | 4 -- .../parse-dates.scala} | 37 ++++--------------- 4 files changed, 44 insertions(+), 36 deletions(-) create mode 100644 library/src-bootstrapped/scala/magic/package.scala delete mode 100644 library/src/scala/magic/package.scala rename tests/{run/errorhandling/maybe.scala => run-bootstrapped/parse-dates.scala} (62%) diff --git a/compiler/src/dotty/tools/dotc/transform/patmat/Space.scala b/compiler/src/dotty/tools/dotc/transform/patmat/Space.scala index 574f11c30ca3..587428ed9951 100644 --- a/compiler/src/dotty/tools/dotc/transform/patmat/Space.scala +++ b/compiler/src/dotty/tools/dotc/transform/patmat/Space.scala @@ -414,11 +414,10 @@ object SpaceEngine { 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 + // into a space of its own for `null` (see `maybeParts`), `Err...)` has to cover // it explicitly. pat.tpe.widen.dealias match - case MagicMaybeType(_, _, /*nullable=*/true) - if fun.symbol == defn.Magic_ErrUnapply && pats.forall(isWildcardArg) => + case MagicMaybeType(_, _, /*nullable=*/true) if fun.symbol == defn.Magic_ErrUnapply => Or(prod :: nullSpace :: Nil) case _ => prod diff --git a/library/src-bootstrapped/scala/magic/package.scala b/library/src-bootstrapped/scala/magic/package.scala new file mode 100644 index 000000000000..c8dc4994c3fa --- /dev/null +++ b/library/src-bootstrapped/scala/magic/package.scala @@ -0,0 +1,34 @@ +//> using options -Yexplicit-nulls +package scala +import language.experimental.magic +import scala.util.boundary, boundary.{Label, break} + +package object magic { + + type CanErr[E] = Label[Nothing ? E] + + inline def maybe[T, E](inline body: CanErr[E] ?=> T): T ? E = + boundary(Ok(body)) + + extension [T, E](x: T ? E) + transparent inline def ? (using CanErr[E]): T = x match + case Ok(y) => y + case Err(e) => break(Err(e)) + + def withErr[E1](e: E1): T ? E1 = x match + case Ok(y) => Ok(y) + case Err(_) => Err(e) + + def mapErr[E1](f: E => E1): T ? E1 = x match + case Ok(y) => Ok(y) + case Err(e) => Err(f(e)) + + inline def provided(cond: Boolean)(using CanErr[Unit]): Unit = + if !cond then boundary.break(Err(())) + + inline def provided[E](cond: Boolean, inline e: E)(using CanErr[E]): Unit = + if !cond then boundary.break(Err(e)) + + +} + diff --git a/library/src/scala/magic/package.scala b/library/src/scala/magic/package.scala deleted file mode 100644 index 78e4369b04e2..000000000000 --- a/library/src/scala/magic/package.scala +++ /dev/null @@ -1,4 +0,0 @@ -package scala - -package object magic - diff --git a/tests/run/errorhandling/maybe.scala b/tests/run-bootstrapped/parse-dates.scala similarity index 62% rename from tests/run/errorhandling/maybe.scala rename to tests/run-bootstrapped/parse-dates.scala index 94dc19e4b8e2..daba1e6009f2 100644 --- a/tests/run/errorhandling/maybe.scala +++ b/tests/run-bootstrapped/parse-dates.scala @@ -1,20 +1,10 @@ //> using options -Yexplicit-nulls -package scala.util -import boundary.{Label, break} import language.experimental.magic +import scala.magic.* -infix type `??`[+R, +E] = Result[R, E] - -inline def maybe[R, E](inline body: Label[Err[E]] ?=> R): R ?? E = - boundary(Ok(body)) - -implicit def toResult[R, E](x: R): R ?? E = Ok(x) - -def NULL: Err[Unit] = Err(()) - -extension (str: String) def parseInt: Int ?? Unit = +extension (str: String) def parseInt: Int ? Unit = try str.toInt - catch case ex: NumberFormatException => Err(null) + catch case ex: NumberFormatException => null case class Date(day: Int, month: Int, year: Int) @@ -24,14 +14,9 @@ def parseDate(str: String) = maybe: Date(d.parseInt?, m.parseInt?, y.parseInt?) case _ => - NULL + null -extension [R, D](x: R ?? D) - def withErr[E](msg: E): R ?? E = x match - case Ok(y) => Ok(y) - case Err(_) => Err(msg) - -def parseDate2(str: String): Date ?? String = +def parseDate2(str: String): Date ? String = str.split("/") match case Array(d, m, y) => maybe: @@ -42,13 +27,7 @@ def parseDate2(str: String): Date ?? String = case _ => Err("Date not in format day/month/year") -def provided(cond: Boolean)(using Label[Err[Unit]]): Unit = - if !cond then boundary.break(NULL) - -inline def provided[E](cond: Boolean, inline err: E)(using Label[Err[E]]): Unit = - if !cond then boundary.break(Err(err)) - -def parseDate3(str: String): Date ?? String = +def parseDate3(str: String): Date ? String = str.split("/") match case Array(d, m, y) => maybe: @@ -61,7 +40,7 @@ def parseDate3(str: String): Date ?? String = case _ => Err("Date not in format day/month/year") -def parseDate4(str: String): Date ?? Unit = +def parseDate4(str: String): Date? = str.split("/") match case Array(d, m, y) => maybe: @@ -72,4 +51,4 @@ def parseDate4(str: String): Date ?? Unit = provided(1 <= month && month <= 12) Date(day, month, year) case _ => - NULL + null From 72c89c04dfdc73b1c2d9319d2b2c2ae3c53ecf04 Mon Sep 17 00:00:00 2001 From: odersky Date: Fri, 21 Aug 2026 13:08:14 +0200 Subject: [PATCH 22/28] Drop mostly redundant tests To reduce added linecount we drop a bunch of tests that are straightforward Option -> ? substitutions. --- ...it-global-scala2-library-tasty.excludelist | 1 - .../test/dotc/pos-test-pickling.excludelist | 1 - .../pos/unapply-implicit-arg-pos-maybe.scala | 16 - .../unapplySeq-implicit-arg-pos-maybe.scala | 16 - .../warn/unapply-implicit-arg-maybe.check | 8 - .../warn/unapply-implicit-arg-maybe.scala | 16 - .../warn/unapply-implicit-arg3-maybe.check | 14 - .../warn/unapply-implicit-arg3-maybe.scala | 16 - tests/neg/i23156-maybe.scala | 8 - tests/neg/i3989c-maybe.scala | 18 - tests/neg/infix-maybe.check | 30 -- tests/neg/infix-maybe.scala | 71 ---- ...e-pattern-binding-messages-old-maybe.check | 48 --- ...e-pattern-binding-messages-old-maybe.scala | 18 - tests/neg/tryPatternMatchEq-maybe.scala | 36 -- tests/patmat/3543-maybe.scala | 54 --- tests/patmat/dotty-maybe.scala | 32 -- tests/patmat/i2502-maybe.check | 8 - tests/patmat/i2502-maybe.scala | 19 - tests/patmat/i2502b-maybe.check | 8 - tests/patmat/i2502b-maybe.scala | 19 - tests/patmat/patmat-extractor-maybe.check | 12 - tests/patmat/patmat-extractor-maybe.scala | 19 - tests/patmat/t8511-maybe.check | 8 - tests/patmat/t8511-maybe.scala | 27 -- tests/pos/byname-implicits-8-maybe.scala | 34 -- tests/pos/i1318-maybe.scala | 40 --- tests/pos/i15188-maybe.scala | 11 - tests/pos/i15188b-maybe.scala | 10 - tests/pos/i18601b-maybe.scala | 27 -- tests/pos/i2104-maybe.scala | 22 -- tests/pos/i2104b-maybe.scala | 18 - tests/pos/i23022-maybe.scala | 14 - tests/pos/i23459-maybe.scala | 24 -- tests/pos/i6621-maybe.scala | 11 - tests/pos/i8997-maybe.scala | 9 - tests/pos/inline-i1773-maybe.scala | 16 - ...pattern-bindings-3.0-migration-maybe.scala | 39 --- .../strict-pattern-bindings-3.1-maybe.scala | 39 --- tests/pos/t1048-maybe.scala | 16 - tests/pos/t3136-maybe.scala | 21 -- tests/pos/t5041-maybe.scala | 11 - tests/pos/t796-maybe.scala | 28 -- tests/pos/t8045-maybe.scala | 19 - tests/pos/unapplyComplex-maybe.scala | 41 --- tests/pos/unapplyVal-maybe.scala | 39 --- .../run/fully-abstract-interface-maybe.check | 29 -- .../run/fully-abstract-interface-maybe.scala | 331 ------------------ tests/run/fully-abstract-nat-1-maybe.check | 13 - tests/run/fully-abstract-nat-1-maybe.scala | 144 -------- tests/run/fully-abstract-nat-2-maybe.check | 13 - tests/run/fully-abstract-nat-2-maybe.scala | 155 -------- tests/run/i1748-maybe.check | 2 - tests/run/i1748-maybe.scala | 16 - tests/run/i1779-maybe.check | 1 - tests/run/i1779-maybe.scala | 15 - tests/run/i8577b-maybe.scala | 15 - tests/run/i8577c-maybe.scala | 15 - tests/run/i8577d-maybe.scala | 15 - tests/run/i8577e-maybe.scala | 19 - tests/run/i8577f-maybe.scala | 15 - tests/run/i8577g-maybe.scala | 15 - tests/run/i8577h-maybe.scala | 15 - tests/run/patmat-maybe.check | 4 - tests/run/patmat-maybe.scala | 47 --- tests/run/patmat-option-named-maybe.scala | 23 -- tests/run/patmat-spec-maybe.scala | 63 ---- tests/run/patmatch-classtag-maybe.scala | 47 --- tests/run/reducable-maybe.scala | 64 ---- tests/run/t1048-maybe.check | 2 - tests/run/t1048-maybe.scala | 23 -- tests/run/t1220-maybe.scala | 17 - tests/run/t4415-maybe.scala | 88 ----- tests/run/t7214-maybe.scala | 61 ---- tests/run/tuple-patterns-maybe.check | 9 - tests/run/tuple-patterns-maybe.scala | 42 --- tests/run/type-test-binding-maybe.check | 2 - tests/run/type-test-binding-maybe.scala | 36 -- tests/run/unchecked-patterns-maybe.scala | 13 - tests/run/virtpatmat_stringinterp-maybe.check | 1 - tests/run/virtpatmat_stringinterp-maybe.scala | 18 - 81 files changed, 2400 deletions(-) delete mode 100644 tests/init-global/pos/unapply-implicit-arg-pos-maybe.scala delete mode 100644 tests/init-global/pos/unapplySeq-implicit-arg-pos-maybe.scala delete mode 100644 tests/init-global/warn/unapply-implicit-arg-maybe.check delete mode 100644 tests/init-global/warn/unapply-implicit-arg-maybe.scala delete mode 100644 tests/init-global/warn/unapply-implicit-arg3-maybe.check delete mode 100644 tests/init-global/warn/unapply-implicit-arg3-maybe.scala delete mode 100644 tests/neg/i23156-maybe.scala delete mode 100644 tests/neg/i3989c-maybe.scala delete mode 100644 tests/neg/infix-maybe.check delete mode 100644 tests/neg/infix-maybe.scala delete mode 100644 tests/neg/refutable-pattern-binding-messages-old-maybe.check delete mode 100644 tests/neg/refutable-pattern-binding-messages-old-maybe.scala delete mode 100644 tests/neg/tryPatternMatchEq-maybe.scala delete mode 100644 tests/patmat/3543-maybe.scala delete mode 100644 tests/patmat/dotty-maybe.scala delete mode 100644 tests/patmat/i2502-maybe.check delete mode 100644 tests/patmat/i2502-maybe.scala delete mode 100644 tests/patmat/i2502b-maybe.check delete mode 100644 tests/patmat/i2502b-maybe.scala delete mode 100644 tests/patmat/patmat-extractor-maybe.check delete mode 100644 tests/patmat/patmat-extractor-maybe.scala delete mode 100644 tests/patmat/t8511-maybe.check delete mode 100644 tests/patmat/t8511-maybe.scala delete mode 100644 tests/pos/byname-implicits-8-maybe.scala delete mode 100644 tests/pos/i1318-maybe.scala delete mode 100644 tests/pos/i15188-maybe.scala delete mode 100644 tests/pos/i15188b-maybe.scala delete mode 100644 tests/pos/i18601b-maybe.scala delete mode 100644 tests/pos/i2104-maybe.scala delete mode 100644 tests/pos/i2104b-maybe.scala delete mode 100644 tests/pos/i23022-maybe.scala delete mode 100644 tests/pos/i23459-maybe.scala delete mode 100644 tests/pos/i6621-maybe.scala delete mode 100644 tests/pos/i8997-maybe.scala delete mode 100644 tests/pos/inline-i1773-maybe.scala delete mode 100644 tests/pos/strict-pattern-bindings-3.0-migration-maybe.scala delete mode 100644 tests/pos/strict-pattern-bindings-3.1-maybe.scala delete mode 100644 tests/pos/t1048-maybe.scala delete mode 100644 tests/pos/t3136-maybe.scala delete mode 100644 tests/pos/t5041-maybe.scala delete mode 100644 tests/pos/t796-maybe.scala delete mode 100644 tests/pos/t8045-maybe.scala delete mode 100644 tests/pos/unapplyComplex-maybe.scala delete mode 100644 tests/pos/unapplyVal-maybe.scala delete mode 100644 tests/run/fully-abstract-interface-maybe.check delete mode 100644 tests/run/fully-abstract-interface-maybe.scala delete mode 100644 tests/run/fully-abstract-nat-1-maybe.check delete mode 100644 tests/run/fully-abstract-nat-1-maybe.scala delete mode 100644 tests/run/fully-abstract-nat-2-maybe.check delete mode 100644 tests/run/fully-abstract-nat-2-maybe.scala delete mode 100644 tests/run/i1748-maybe.check delete mode 100644 tests/run/i1748-maybe.scala delete mode 100644 tests/run/i1779-maybe.check delete mode 100644 tests/run/i1779-maybe.scala delete mode 100644 tests/run/i8577b-maybe.scala delete mode 100644 tests/run/i8577c-maybe.scala delete mode 100644 tests/run/i8577d-maybe.scala delete mode 100644 tests/run/i8577e-maybe.scala delete mode 100644 tests/run/i8577f-maybe.scala delete mode 100644 tests/run/i8577g-maybe.scala delete mode 100644 tests/run/i8577h-maybe.scala delete mode 100644 tests/run/patmat-maybe.check delete mode 100644 tests/run/patmat-maybe.scala delete mode 100644 tests/run/patmat-option-named-maybe.scala delete mode 100644 tests/run/patmat-spec-maybe.scala delete mode 100644 tests/run/patmatch-classtag-maybe.scala delete mode 100644 tests/run/reducable-maybe.scala delete mode 100644 tests/run/t1048-maybe.check delete mode 100644 tests/run/t1048-maybe.scala delete mode 100644 tests/run/t1220-maybe.scala delete mode 100644 tests/run/t4415-maybe.scala delete mode 100644 tests/run/t7214-maybe.scala delete mode 100644 tests/run/tuple-patterns-maybe.check delete mode 100644 tests/run/tuple-patterns-maybe.scala delete mode 100644 tests/run/type-test-binding-maybe.check delete mode 100644 tests/run/type-test-binding-maybe.scala delete mode 100644 tests/run/unchecked-patterns-maybe.scala delete mode 100644 tests/run/virtpatmat_stringinterp-maybe.check delete mode 100644 tests/run/virtpatmat_stringinterp-maybe.scala diff --git a/compiler/test/dotc/pos-init-global-scala2-library-tasty.excludelist b/compiler/test/dotc/pos-init-global-scala2-library-tasty.excludelist index 3351cfa9f818..e95a5b8b8c8f 100644 --- a/compiler/test/dotc/pos-init-global-scala2-library-tasty.excludelist +++ b/compiler/test/dotc/pos-init-global-scala2-library-tasty.excludelist @@ -2,7 +2,6 @@ patmat.scala patmat-interpolator.scala unapplySeq-implicit-arg-pos.scala -unapplySeq-implicit-arg-pos-maybe.scala global-cycle11.scala ## warns! and fails under -Werror, noticed at #25571 diff --git a/compiler/test/dotc/pos-test-pickling.excludelist b/compiler/test/dotc/pos-test-pickling.excludelist index e8429ae78df2..709e233b54b3 100644 --- a/compiler/test/dotc/pos-test-pickling.excludelist +++ b/compiler/test/dotc/pos-test-pickling.excludelist @@ -29,7 +29,6 @@ i9804.scala i13433.scala i16649-irrefutable.scala strict-pattern-bindings-3.0-migration.scala -strict-pattern-bindings-3.0-migration-maybe.scala i17186b.scala i11982a.scala i17255 diff --git a/tests/init-global/pos/unapply-implicit-arg-pos-maybe.scala b/tests/init-global/pos/unapply-implicit-arg-pos-maybe.scala deleted file mode 100644 index 4af2d9bb8333..000000000000 --- a/tests/init-global/pos/unapply-implicit-arg-pos-maybe.scala +++ /dev/null @@ -1,16 +0,0 @@ -//> using options -Yexplicit-nulls -import language.experimental.magic -object Bar { - class Foo { - def m1(i: Int) = i + i1 - def m2(i: Int) = i + 2 - } - def unapply(using f1: Foo)(using f2: Foo)(i: Int)(using f3: Foo): Int? = - if i == 0 then f1.m1(i1) + f3.m1(i1) else f2.m2(i) + f3.m2(i) - - given Foo = new Foo - val i1: Int = 0 - val i2: Int = i1 match - case Bar(i) => i - case _ => 0 -} \ No newline at end of file diff --git a/tests/init-global/pos/unapplySeq-implicit-arg-pos-maybe.scala b/tests/init-global/pos/unapplySeq-implicit-arg-pos-maybe.scala deleted file mode 100644 index 7a740d0ca712..000000000000 --- a/tests/init-global/pos/unapplySeq-implicit-arg-pos-maybe.scala +++ /dev/null @@ -1,16 +0,0 @@ -//> using options -Yexplicit-nulls -import language.experimental.magic -object Bar { - class Foo { - def m1(seq: Seq[Int]) = 0 +: seq - def m2(seq: Seq[Int]) = i1 +: seq - } - def unapplySeq(using f1: Foo)(using f2: Foo)(seqi: Seq[Int])(using f3: Foo): Seq[Int]? = - if seqi(0) == 0 then f1.m1(seqi) else f2.m2(seqi) - - given Foo = new Foo - val i1: Int = 0 - val i2: Int = Seq(i1) match - case Bar(i) => i - case _ => 0 -} diff --git a/tests/init-global/warn/unapply-implicit-arg-maybe.check b/tests/init-global/warn/unapply-implicit-arg-maybe.check deleted file mode 100644 index 246e394a252c..000000000000 --- a/tests/init-global/warn/unapply-implicit-arg-maybe.check +++ /dev/null @@ -1,8 +0,0 @@ --- Warning: tests/init-global/warn/unapply-implicit-arg-maybe.scala:13:16 ---------------------------------------------- -13 | val i2: Int = i2 match // warn - | ^^ - | Access uninitialized field value i2. Calling trace: - | ├── object Bar { [ unapply-implicit-arg-maybe.scala:3 ] - | │ ^ - | └── val i2: Int = i2 match // warn [ unapply-implicit-arg-maybe.scala:13 ] - | ^^ diff --git a/tests/init-global/warn/unapply-implicit-arg-maybe.scala b/tests/init-global/warn/unapply-implicit-arg-maybe.scala deleted file mode 100644 index 3f223239c479..000000000000 --- a/tests/init-global/warn/unapply-implicit-arg-maybe.scala +++ /dev/null @@ -1,16 +0,0 @@ -//> using options -Yexplicit-nulls -import language.experimental.magic -object Bar { - class Foo { - def m1(i: Int) = i+1 - def m2(i: Int) = i+2 - } - def unapply(using f1: Foo)(i: Int): Int? = - if i == 0 then f1.m1(i) else f1.m2(i) - - given Foo = new Foo - val i1: Int = 0 - val i2: Int = i2 match // warn - case Bar(i) => i - case _ => 0 -} \ No newline at end of file diff --git a/tests/init-global/warn/unapply-implicit-arg3-maybe.check b/tests/init-global/warn/unapply-implicit-arg3-maybe.check deleted file mode 100644 index da0664b4f7d3..000000000000 --- a/tests/init-global/warn/unapply-implicit-arg3-maybe.check +++ /dev/null @@ -1,14 +0,0 @@ --- Warning: tests/init-global/warn/unapply-implicit-arg3-maybe.scala:6:25 ---------------------------------------------- -6 | def m2(i: Int) = i + i2 // warn - | ^^ - | Access uninitialized field value i2. Calling trace: - | ├── object Bar { [ unapply-implicit-arg3-maybe.scala:3 ] - | │ ^ - | ├── case Bar(i) => i [ unapply-implicit-arg3-maybe.scala:14 ] - | │ ^^^^^^ - | ├── def unapply(using f1: Foo)(i: Int): Int? = [ unapply-implicit-arg3-maybe.scala:8 ] - | │ ^ - | ├── if i == 0 then f1.m1(i) else f1.m2(i) [ unapply-implicit-arg3-maybe.scala:9 ] - | │ ^^^^^^^^ - | └── def m2(i: Int) = i + i2 // warn [ unapply-implicit-arg3-maybe.scala:6 ] - | ^^ diff --git a/tests/init-global/warn/unapply-implicit-arg3-maybe.scala b/tests/init-global/warn/unapply-implicit-arg3-maybe.scala deleted file mode 100644 index 7acc075c36b8..000000000000 --- a/tests/init-global/warn/unapply-implicit-arg3-maybe.scala +++ /dev/null @@ -1,16 +0,0 @@ -//> using options -Yexplicit-nulls -import language.experimental.magic -object Bar { - class Foo { - def m1(i: Int) = i + i1 - def m2(i: Int) = i + i2 // warn - } - def unapply(using f1: Foo)(i: Int): Int? = - if i == 0 then f1.m1(i) else f1.m2(i) - - given Foo = new Foo - val i1: Int = 0 - val i2: Int = i1 match - case Bar(i) => i - case _ => 0 -} diff --git a/tests/neg/i23156-maybe.scala b/tests/neg/i23156-maybe.scala deleted file mode 100644 index c71c39ea2a95..000000000000 --- a/tests/neg/i23156-maybe.scala +++ /dev/null @@ -1,8 +0,0 @@ -//> using options -Yexplicit-nulls -import language.experimental.magic -object Unpack { - (1, 2) match { - case Unpack(first, _) => first - } - def unapply(e: (Int, Int)): T? = ??? // error -} \ No newline at end of file diff --git a/tests/neg/i3989c-maybe.scala b/tests/neg/i3989c-maybe.scala deleted file mode 100644 index f2455c430652..000000000000 --- a/tests/neg/i3989c-maybe.scala +++ /dev/null @@ -1,18 +0,0 @@ -//> using options -Yexplicit-nulls -import language.experimental.magic -import scala.magic.* -import scala.Option -object Test extends App { - trait A[+X] - class B[+X](val x: X) extends A[X] - object B { - def unapply[X](b: B[X]): X? = Ok(b.x) - } - - class C[+X](x: Any) extends B[Any](x) with A[X] - def f(a: A[Int]): Int = a match { - case B(i) => i // error - case _ => 0 - } - f(new C[Int]("foo")) -} diff --git a/tests/neg/infix-maybe.check b/tests/neg/infix-maybe.check deleted file mode 100644 index 303d5ef2b12e..000000000000 --- a/tests/neg/infix-maybe.check +++ /dev/null @@ -1,30 +0,0 @@ --- Error: tests/neg/infix-maybe.scala:27:4 ----------------------------------------------------------------------------- -27 | c mop 2 // error: should not be used as infix operator - | ^^^ - | Alphanumeric method mop is not declared infix; it should not be used as infix operator. - | Instead, use method syntax .mop(...) or backticked identifier `mop`. - | The latter can be rewritten automatically under -rewrite -source 3.4-migration. --- Error: tests/neg/infix-maybe.scala:28:4 ----------------------------------------------------------------------------- -28 | c meth 2 // error: should not be used as infix operator - | ^^^^ - | Alphanumeric method meth is not declared infix; it should not be used as infix operator. - | Instead, use method syntax .meth(...) or backticked identifier `meth`. - | The latter can be rewritten automatically under -rewrite -source 3.4-migration. --- Error: tests/neg/infix-maybe.scala:46:14 ---------------------------------------------------------------------------- -46 | val x1: Int Map String = ??? // error - | ^^^ - | Alphanumeric type Map is not declared infix; it should not be used as infix operator. - | Instead, use prefix syntax Map[...] or backticked identifier `Map`. - | The latter can be rewritten automatically under -rewrite -source 3.4-migration. --- Error: tests/neg/infix-maybe.scala:48:14 ---------------------------------------------------------------------------- -48 | val x3: Int AndC String = ??? // error - | ^^^^ - | Alphanumeric type AndC is not declared infix; it should not be used as infix operator. - | Instead, use prefix syntax AndC[...] or backticked identifier `AndC`. - | The latter can be rewritten automatically under -rewrite -source 3.4-migration. --- Error: tests/neg/infix-maybe.scala:62:8 ----------------------------------------------------------------------------- -62 | val _ Pair _ = p // error - | ^^^^ - | Alphanumeric extractor Pair is not declared infix; it should not be used as infix operator. - | Instead, use prefix syntax Pair(...) or backticked identifier `Pair`. - | The latter can be rewritten automatically under -rewrite -source 3.4-migration. diff --git a/tests/neg/infix-maybe.scala b/tests/neg/infix-maybe.scala deleted file mode 100644 index cc800dc83370..000000000000 --- a/tests/neg/infix-maybe.scala +++ /dev/null @@ -1,71 +0,0 @@ -//> using options -source future -deprecation -Yexplicit-nulls - -// Compile with -strict -Xfatal-warnings -deprecation -import language.experimental.magic -class C: - infix def op(x: Int): Int = ??? - def meth(x: Int): Int = ??? - def matching(x: Int => Int) = ??? - def +(x: Int): Int = ??? - -object C: - given AnyRef: - extension (x: C) - infix def iop (y: Int) = ??? - def mop (y: Int) = ??? - def ++ (y: Int) = ??? - -val c = C() -def test() = { - c op 2 - c iop 2 - c.meth(2) - c ++ 2 - - c.op(2) - c.iop(2) - c mop 2 // error: should not be used as infix operator - c meth 2 // error: should not be used as infix operator - c `meth` 2 // OK, sincd `meth` is backquoted - c + 3 // OK, since `+` is symbolic - 1 to 2 // OK, since `to` is defined by Scala-2 - c meth { // OK, since `meth` is followed by `{...}` - 3 - } - c matching { // OK, since `meth` is followed by `{...}` - case x => x - } - - infix class Or[X, Y] - class AndC[X, Y] - infix type And[X, Y] = AndC[X, Y] - infix type &&[X, Y] = AndC[X, Y] - - class Map[X, Y] - - val x1: Int Map String = ??? // error - val x2: Int Or String = ??? // OK since Or is declared `infix` - val x3: Int AndC String = ??? // error - val x4: Int `AndC` String = ??? // OK - val x5: Int And String = ??? // OK - val x6: Int && String = ??? - - case class Pair[T](x: T, y: T) - infix case class Q[T](x: T, y: T) - - object PP { - infix def unapply[T](x: Pair[T]): (T, T)? = (x.x, x.y) - } - - val p = Pair(1, 2) - val Pair(_, _) = p - val _ Pair _ = p // error - val _ `Pair` _ = p // OK - val (_ PP _) = p: @unchecked // OK - - val q = Q(1, 2) - val Q(_, _) = q - val _ Q _ = q // OK - - -} \ No newline at end of file diff --git a/tests/neg/refutable-pattern-binding-messages-old-maybe.check b/tests/neg/refutable-pattern-binding-messages-old-maybe.check deleted file mode 100644 index 85c687a43a1e..000000000000 --- a/tests/neg/refutable-pattern-binding-messages-old-maybe.check +++ /dev/null @@ -1,48 +0,0 @@ --- Error: tests/neg/refutable-pattern-binding-messages-old-maybe.scala:7:14 -------------------------------------------- -7 | for Positive(i) <- List(1, 2, 3) do () // error: refutable extractor - | ^^^^^^^^^^^ - | pattern binding uses refutable extractor `Test.Positive` - | - | If this usage is intentional, this can be communicated by adding the `case` keyword before the full pattern, - | which will result in a filtering for expression (using `withFilter`). - | This patch can be rewritten automatically under -rewrite -source 3.2-migration. --- Error: tests/neg/refutable-pattern-binding-messages-old-maybe.scala:12:11 ------------------------------------------- -12 | for ((x: String) <- xs) do () // error: pattern type more specialized - | ^^^^^^ - | pattern's type String is more specialized than the right hand side expression's type AnyRef - | - | If the narrowing is intentional, this can be communicated by adding the `case` keyword before the full pattern, - | which will result in a filtering for expression (using `withFilter`). - | This patch can be rewritten automatically under -rewrite -source 3.2-migration. --- Error: tests/neg/refutable-pattern-binding-messages-old-maybe.scala:16:13 ------------------------------------------- -16 | for none @ None <- ys do () // error: pattern type does not match - | ^^^^ - | pattern's type None.type does not match the right hand side expression's type (x$1 : Option[?]) - | - | If the narrowing is intentional, this can be communicated by adding the `case` keyword before the full pattern, - | which will result in a filtering for expression (using `withFilter`). - | This patch can be rewritten automatically under -rewrite -source 3.2-migration. --- Warning: tests/neg/refutable-pattern-binding-messages-old-maybe.scala:6:14 ------------------------------------------ -6 | val Positive(p) = 5 // warn: refutable extractor - | ^^^^^^^^^^^^^^^ - | pattern binding uses refutable extractor `Test.Positive` - | - | If this usage is intentional, this can be communicated by adding `: @unchecked` after the expression, - | which may result in a MatchError at runtime. - | This patch can be rewritten automatically under -rewrite -source 3.2-migration. --- Warning: tests/neg/refutable-pattern-binding-messages-old-maybe.scala:11:20 ----------------------------------------- -11 | val i :: is = List(1, 2, 3) // warn: 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 `: @unchecked` after the expression, - | which may result in a MatchError at runtime. - | This patch can be rewritten automatically under -rewrite -source 3.2-migration. --- Warning: tests/neg/refutable-pattern-binding-messages-old-maybe.scala:17:10 ----------------------------------------- -17 | val 1 = 2 // warn: 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 `: @unchecked` after the expression, - | which may result in a MatchError at runtime. - | This patch can be rewritten automatically under -rewrite -source 3.2-migration. diff --git a/tests/neg/refutable-pattern-binding-messages-old-maybe.scala b/tests/neg/refutable-pattern-binding-messages-old-maybe.scala deleted file mode 100644 index 3691515be5e5..000000000000 --- a/tests/neg/refutable-pattern-binding-messages-old-maybe.scala +++ /dev/null @@ -1,18 +0,0 @@ -//> using options -source 3.7 -Yexplicit-nulls -import language.experimental.magic -object Test { - // refutable extractor - object Positive { def unapply(i: Int): Int? = if i > 0 then i else null } - val Positive(p) = 5 // warn: 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) // warn: 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 // warn: pattern type does not match -} diff --git a/tests/neg/tryPatternMatchEq-maybe.scala b/tests/neg/tryPatternMatchEq-maybe.scala deleted file mode 100644 index 651636373e00..000000000000 --- a/tests/neg/tryPatternMatchEq-maybe.scala +++ /dev/null @@ -1,36 +0,0 @@ -//> using options -Yexplicit-nulls -import language.experimental.magic -import java.io.IOException -import java.lang.NullPointerException -import java.lang.IllegalArgumentException - -object IAE { - def unapply(e: Exception): String? = - if (e.isInstanceOf[IllegalArgumentException]) 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 `a` => // error: cannot compare - case EX => - case IAE(msg) => - case e: IllegalArgumentException => - } - } -} diff --git a/tests/patmat/3543-maybe.scala b/tests/patmat/3543-maybe.scala deleted file mode 100644 index f74355f08c77..000000000000 --- a/tests/patmat/3543-maybe.scala +++ /dev/null @@ -1,54 +0,0 @@ -//> using options -Yexplicit-nulls -import language.experimental.magic -class Test { - class Foo { - def unapply(x: String): String? = ??? - } - - def test(xs: List[String]): Unit = { - val Yes = new Foo - val No = new Foo - - xs match { - case Yes(x) :: ls => println("Yes") - case No(y) :: ls => println("No") - case _ => - } - } -} - -class Test2 { - class Foo(x: Boolean) { - def unapply(y: String): Boolean = x - } - - def test(xs: List[String]): Unit = { - val Yes = new Foo(true) - val No = new Foo(false) - - xs match { - case No() :: ls => println("No") - case Yes() :: ls => println("Yes") - case _ => - } - } -} - -class Test3 { - import scala.util.matching.Regex - - def main(args: Array[String]): Unit = { - foo("c" :: Nil, false) - } - - def foo(remaining: List[String], inCodeBlock: Boolean): Unit = { - remaining match { - case CodeBlockEndRegex(before) :: ls => - case SymbolTagRegex(name) :: ls if !inCodeBlock => println("OK") - case _ => - } - } - - val CodeBlockEndRegex = new Regex("(b)") - val SymbolTagRegex = new Regex("(c)") -} diff --git a/tests/patmat/dotty-maybe.scala b/tests/patmat/dotty-maybe.scala deleted file mode 100644 index 73889619bf67..000000000000 --- a/tests/patmat/dotty-maybe.scala +++ /dev/null @@ -1,32 +0,0 @@ -//> using options -Yexplicit-nulls -import language.experimental.magic -object IntEqualityTestTreeMaker { - def unapply(xs: Int): Int? = ??? -} - -class Test { - def isBelow(n: Int, s: String): Boolean = false - - def foo(xs: List[(Int, String)]): Unit = xs.filter(isBelow.tupled) match { - case Nil => - case matches => - } - - def linkCompanions(xs: List[(Int, Int)]): Unit = { - xs.groupBy(_._1).foreach { - case (_, List(x1, x2)) => - case _ => () - } - } - - def bar(xs: List[(Int, String)]): Unit = xs match { - case (x, s) :: Nil => - case Nil => - case _ => - } - - def patmat(alts: List[List[Int]]): Unit = alts.forall { - case List(IntEqualityTestTreeMaker(_)) => false - case _ => true - } -} \ No newline at end of file diff --git a/tests/patmat/i2502-maybe.check b/tests/patmat/i2502-maybe.check deleted file mode 100644 index c94f58825422..000000000000 --- a/tests/patmat/i2502-maybe.check +++ /dev/null @@ -1,8 +0,0 @@ --- [E029] Pattern Match Exhaustivity Warning: tests/patmat/i2502-maybe.scala:7:35 -7 | def classOrArrayType: String = this match { - | ^^^^ - | match may not be exhaustive. - | - | It would fail on pattern case: _: BTypes.this.ClassBType - | - | longer explanation available when compiling with `-explain` diff --git a/tests/patmat/i2502-maybe.scala b/tests/patmat/i2502-maybe.scala deleted file mode 100644 index ac660be002b7..000000000000 --- a/tests/patmat/i2502-maybe.scala +++ /dev/null @@ -1,19 +0,0 @@ -//> using options -Yexplicit-nulls -import language.experimental.magic -abstract class BTypes { - trait BType - - sealed trait RefBType extends BType { - def classOrArrayType: String = this match { - case ClassBType(internalName) => internalName - case a: ArrayBType => "" - } - } - - final class ClassBType(val internalName: String) extends RefBType - class ArrayBType extends RefBType - - object ClassBType { - def unapply(x: ClassBType): String? = null - } -} \ No newline at end of file diff --git a/tests/patmat/i2502b-maybe.check b/tests/patmat/i2502b-maybe.check deleted file mode 100644 index 859da05bb41e..000000000000 --- a/tests/patmat/i2502b-maybe.check +++ /dev/null @@ -1,8 +0,0 @@ --- [E029] Pattern Match Exhaustivity Warning: tests/patmat/i2502b-maybe.scala:7:35 -7 | def classOrArrayType: String = this match { - | ^^^^ - | match may not be exhaustive. - | - | It would fail on pattern case: _: BTypes.this.ClassBType - | - | longer explanation available when compiling with `-explain` diff --git a/tests/patmat/i2502b-maybe.scala b/tests/patmat/i2502b-maybe.scala deleted file mode 100644 index 327fe7dcad26..000000000000 --- a/tests/patmat/i2502b-maybe.scala +++ /dev/null @@ -1,19 +0,0 @@ -//> using options -Yexplicit-nulls -import language.experimental.magic -abstract class BTypes { - trait BType - - sealed trait RefBType extends BType { - def classOrArrayType: String = this match { - case ClassBType(internalName) => internalName - case a: ArrayBType => "" - } - } - - final case class ClassBType(val internalName: String) extends RefBType - class ArrayBType extends RefBType - - object ClassBType { - def unapply(x: RefBType): String? = null - } -} diff --git a/tests/patmat/patmat-extractor-maybe.check b/tests/patmat/patmat-extractor-maybe.check deleted file mode 100644 index 38f48e78403b..000000000000 --- a/tests/patmat/patmat-extractor-maybe.check +++ /dev/null @@ -1,12 +0,0 @@ --- [E029] Pattern Match Exhaustivity Warning: tests/patmat/patmat-extractor-maybe.scala:15:30 -15 | def foo(x: Node): Boolean = x match { // unexhaustive - | ^ - | match may not be exhaustive. - | - | It would fail on pattern case: NodeA(_), NodeB(_), NodeC(_) - | - | longer explanation available when compiling with `-explain` --- [E030] Match case Unreachable Warning: tests/patmat/patmat-extractor-maybe.scala:17:13 -17 | case Node(NodeA(4), NodeB(false)) => true // unreachable code - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | Unreachable case diff --git a/tests/patmat/patmat-extractor-maybe.scala b/tests/patmat/patmat-extractor-maybe.scala deleted file mode 100644 index 613cbbf256df..000000000000 --- a/tests/patmat/patmat-extractor-maybe.scala +++ /dev/null @@ -1,19 +0,0 @@ -//> using options -Yexplicit-nulls -import language.experimental.magic -sealed trait Node -case class NodeA(i: Int) extends Node -case class NodeB(b: Boolean) extends Node -case class NodeC(s: String) extends Node - -object Node { - def unapply(node: Node): (Node, Node)? = ??? -} - -// currently scalac can't do anything with following -// it's possible to do better in our case -object Test { - def foo(x: Node): Boolean = x match { // unexhaustive - case Node(NodeA(_), NodeB(_)) => true - case Node(NodeA(4), NodeB(false)) => true // unreachable code - } -} \ No newline at end of file diff --git a/tests/patmat/t8511-maybe.check b/tests/patmat/t8511-maybe.check deleted file mode 100644 index b094816491ec..000000000000 --- a/tests/patmat/t8511-maybe.check +++ /dev/null @@ -1,8 +0,0 @@ --- [E029] Pattern Match Exhaustivity Warning: tests/patmat/t8511-maybe.scala:20:42 -20 | private def logic(head: Expr): String = head match { - | ^^^^ - | match may not be exhaustive. - | - | It would fail on pattern case: Bar(_), Baz(), EatsExhaustiveWarning(_) - | - | longer explanation available when compiling with `-explain` diff --git a/tests/patmat/t8511-maybe.scala b/tests/patmat/t8511-maybe.scala deleted file mode 100644 index 2a4ff200e4b3..000000000000 --- a/tests/patmat/t8511-maybe.scala +++ /dev/null @@ -1,27 +0,0 @@ -//> using options -Yexplicit-nulls -import language.experimental.magic -sealed trait Expr -final case class Foo(other: Option[String]) extends Expr -final case class Bar(someConstant: String) extends Expr -final case class Baz() extends Expr -final case class EatsExhaustiveWarning(other: Reference) extends Expr - -sealed trait Reference { - val value: String -} - -object Reference { - def unapply(reference: Reference): (String)? = { - reference.value - } -} - -object EntryPoint { - private def logic(head: Expr): String = head match { - case Foo(_) => - ??? - // Commenting this line only causes the exhaustive search warning to be emitted - case EatsExhaustiveWarning(Reference(text)) => - ??? - } -} \ No newline at end of file diff --git a/tests/pos/byname-implicits-8-maybe.scala b/tests/pos/byname-implicits-8-maybe.scala deleted file mode 100644 index d05a89a7f9d8..000000000000 --- a/tests/pos/byname-implicits-8-maybe.scala +++ /dev/null @@ -1,34 +0,0 @@ -//> using options -Yexplicit-nulls -// shapeless's Lazy implemented in terms of byname implicits -import language.experimental.magic -import scala.magic.* -trait Lazy[T] { - lazy val value: T -} - -object Lazy { - implicit def apply[T](implicit t: => T): Lazy[T] = - new Lazy[T] { - lazy val value = t - } - - def unapply[T](lt: Lazy[T]): T? = Ok(lt.value) -} - -trait Foo { - type Out - def out: Out -} - -object Foo { - type Aux[Out0] = Foo { type Out = Out0 } - - implicit val fooInt: Aux[Int] = new Foo { type Out = Int ; def out = 23 } -} - -object Test { - def bar[T](t: T)(implicit foo: Lazy[Foo.Aux[T]]): T = foo.value.out - - val i = bar(13) - i: Int -} diff --git a/tests/pos/i1318-maybe.scala b/tests/pos/i1318-maybe.scala deleted file mode 100644 index 5a4c6ecd5edd..000000000000 --- a/tests/pos/i1318-maybe.scala +++ /dev/null @@ -1,40 +0,0 @@ -//> using options -Yexplicit-nulls -import language.experimental.magic -object Foo { - class S(i: Int) - case class T(i: Int) extends S(i) - - object T { - def unapply(s: S): (Int, Int)? = (5, 6) - // def unapply(o: Object): (Int, Int, Int)? = (5, 6, 7) - } - - val s = new S(5) - - s match { - // case T(x, y, z) => println(x + y + z) - case T(x, y) => println(x + y) - case T(x) => println(x) - case _ => println("not match") - } -} - -object Bar { - case class T(i: Int) - class S(i: Int) extends T(i) - - object T { - def unapply(s: S): (Int, Int)? = (5, 6) - // def unapply(o: Object): (Int, Int, Int)? = (5, 6, 7) - } - - val s = new S(5) - - s match { - // case T(x, y, z) => println(x + y + z) - case T(x, y) => println(x + y) - case T(x) => println(x) - case _ => println("not match") - } -} - diff --git a/tests/pos/i15188-maybe.scala b/tests/pos/i15188-maybe.scala deleted file mode 100644 index f5b5140e9875..000000000000 --- a/tests/pos/i15188-maybe.scala +++ /dev/null @@ -1,11 +0,0 @@ -//> using options -Yexplicit-nulls -import language.experimental.magic -object O - -extension [T] (ctx: O.type) inline def unapplySeq(input: T): Seq[T]? = Seq(input) - -@main -def Main = { - val O(x) = 3 - println(s"x: $x") -} diff --git a/tests/pos/i15188b-maybe.scala b/tests/pos/i15188b-maybe.scala deleted file mode 100644 index 0189dab64640..000000000000 --- a/tests/pos/i15188b-maybe.scala +++ /dev/null @@ -1,10 +0,0 @@ -//> using options -Yexplicit-nulls -import language.experimental.magic -class C - -extension (ctx: C) inline def unapply(input: String): String? = "hi" - -@main def run = { - val O = new C - val O(x) = "3" -} diff --git a/tests/pos/i18601b-maybe.scala b/tests/pos/i18601b-maybe.scala deleted file mode 100644 index 08d4069be7f9..000000000000 --- a/tests/pos/i18601b-maybe.scala +++ /dev/null @@ -1,27 +0,0 @@ -//> using options -Werror -Yexplicit-nulls - -// like pos/i18601 -// but with a dedicated SC class -// that made the false positive redundancy warning go away - -import language.experimental.magic -extension (sc: StringContext) - def m: SC = SC(sc) - -class SC(sc: StringContext): - 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: not unreachable (as a counter-example) - } - - // 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/i2104-maybe.scala b/tests/pos/i2104-maybe.scala deleted file mode 100644 index 6687278d2efd..000000000000 --- a/tests/pos/i2104-maybe.scala +++ /dev/null @@ -1,22 +0,0 @@ -//> using options -Yexplicit-nulls -import language.experimental.magic -case class Pair[A, B](_1: A, _2: B) - -trait Cons[+H, +T] - -object Cons { - def apply[H, T](h: H, t: T): Cons[H, T] = ??? - def unapply[H, T](t: Cons[H, T]): Pair[H, T]? = ??? -} - - - -object Test { - def main(args: Array[String]): Unit = { - Cons(Option(1), None) match { - case Cons(Some(i), None) => - i: Int // error: found: Any(i), requires: Int - assert(i == 1) - } - } -} diff --git a/tests/pos/i2104b-maybe.scala b/tests/pos/i2104b-maybe.scala deleted file mode 100644 index fa6dfda0cf37..000000000000 --- a/tests/pos/i2104b-maybe.scala +++ /dev/null @@ -1,18 +0,0 @@ -//> using options -Yexplicit-nulls -import language.experimental.magic -case class Pair[A, B](_1: A, _2: B) - -trait Cons[+H, +T] - -object Cons { - def apply[H, T](h: H, t: T): Cons[H, T] = ??? - def unapply[H, T](t: Cons[H, T]): Pair[H, T]? = ??? -} - -object Test { - def main(args: Array[String]): Unit = { - Cons(Option(1), None) match { - case Cons(Some(i), None) => - } - } -} diff --git a/tests/pos/i23022-maybe.scala b/tests/pos/i23022-maybe.scala deleted file mode 100644 index b94bc92bc00c..000000000000 --- a/tests/pos/i23022-maybe.scala +++ /dev/null @@ -1,14 +0,0 @@ -//> using options -Yexplicit-nulls -import language.experimental.magic -trait ExtractorWithImplicit: - - object Yikes: - def unapply(implicit M: String): Any? = ??? - - def expand: Any = - given String = "Hey" - "Wut" match - case Yikes(_) => ??? - case _ => ??? - - diff --git a/tests/pos/i23459-maybe.scala b/tests/pos/i23459-maybe.scala deleted file mode 100644 index 46e1684ce81e..000000000000 --- a/tests/pos/i23459-maybe.scala +++ /dev/null @@ -1,24 +0,0 @@ -//> using options -Yexplicit-nulls -import language.experimental.magic -object TTest: - def unapplySeq(t: Int): Seq[Int]? = Seq(1, 2) - -case class Varargs(xs: Int*) - -def test = - 1 match - case TTest(x*) => () - - 1 match - case TTest(_*) => () - - 1 match - case TTest(1, rest*) => () - case TTest(_*) => () - - Varargs(1, 2, 3) match - case Varargs(x*) => () - - Varargs(1, 2, 3) match - case Varargs(1, rest*) => () - case Varargs(_*) => () diff --git a/tests/pos/i6621-maybe.scala b/tests/pos/i6621-maybe.scala deleted file mode 100644 index 5ed3ada1a6af..000000000000 --- a/tests/pos/i6621-maybe.scala +++ /dev/null @@ -1,11 +0,0 @@ -//> using options -Werror -deprecation -feature -Yexplicit-nulls - -import language.experimental.magic -object Unapply { - def unapply(a: Any): (Int, Int)? = - (1, 2) -} - -object Test { - val Unapply(x, y) = "": @unchecked -} diff --git a/tests/pos/i8997-maybe.scala b/tests/pos/i8997-maybe.scala deleted file mode 100644 index f33ce0e2a9c4..000000000000 --- a/tests/pos/i8997-maybe.scala +++ /dev/null @@ -1,9 +0,0 @@ -//> using options -Yexplicit-nulls -import language.experimental.magic -object Foo: - def unapply(n: Int)(using x: DummyImplicit)(using y: Int): Int? = ??? - -def test = - given Int = 3 - 1 match - case Foo(_) => diff --git a/tests/pos/inline-i1773-maybe.scala b/tests/pos/inline-i1773-maybe.scala deleted file mode 100644 index a462f2498fd6..000000000000 --- a/tests/pos/inline-i1773-maybe.scala +++ /dev/null @@ -1,16 +0,0 @@ -//> using options -Yexplicit-nulls -import language.experimental.magic -object Test { - implicit class Foo(sc: StringContext) { - object q { - def unapply(arg: Any): (Any, Any)? = - (sc.parts(0), sc.parts(1)) - } - } - - def main(args: Array[String]): Unit = { - val q"class $name extends $parent" = new Object - println(name) - println(parent) - } -} diff --git a/tests/pos/strict-pattern-bindings-3.0-migration-maybe.scala b/tests/pos/strict-pattern-bindings-3.0-migration-maybe.scala deleted file mode 100644 index d2ffb03d896d..000000000000 --- a/tests/pos/strict-pattern-bindings-3.0-migration-maybe.scala +++ /dev/null @@ -1,39 +0,0 @@ -//> using options -Werror -deprecation -feature -Yexplicit-nulls - -// These tests should pass under -Werror with source version less than 3.2 -import language.experimental.magic -import language.`3.0-migration` - -object Test: - // from filtering-fors.scala - val xs: List[AnyRef] = ??? - - for ((x: String) <- xs) do () - for (y@ (x: String) <- xs) do () - for ((x, y) <- xs) do () - - for ((x: String) <- xs if x.isEmpty) do () - for ((x: String) <- xs; y = x) do () - for ((x: String) <- xs; (y, z) <- xs) do () - for (case (x: String) <- xs; (y, z) <- xs) do () - for ((x: String) <- xs; case (y, z) <- xs) do () - - val pairs: List[AnyRef] = List((1, 2), "hello", (3, 4)) - for ((x, y) <- pairs) yield (y, x) - - // from unchecked-patterns.scala - val y :: ys = List(1, 2, 3) - val (1, c) = (1, 2) - val 1 *: cs = 1 *: Tuple() - - val (_: Int | _: AnyRef) = ??? : AnyRef - - val 1 = 2 - - 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 - val Some(s1) = Option(1) diff --git a/tests/pos/strict-pattern-bindings-3.1-maybe.scala b/tests/pos/strict-pattern-bindings-3.1-maybe.scala deleted file mode 100644 index 9ff692cc6b89..000000000000 --- a/tests/pos/strict-pattern-bindings-3.1-maybe.scala +++ /dev/null @@ -1,39 +0,0 @@ -//> using options -Werror -deprecation -feature -Yexplicit-nulls - -// These tests should pass under -Xfatal-warnings with source version less than 3.2 -import language.experimental.magic -import language.`3.1` - -object Test: - // from filtering-fors.scala - val xs: List[AnyRef] = ??? - - for ((x: String) <- xs) do () - for (y@ (x: String) <- xs) do () - for ((x, y) <- xs) do () - - for ((x: String) <- xs if x.isEmpty) do () - for ((x: String) <- xs; y = x) do () - for ((x: String) <- xs; (y, z) <- xs) do () - for (case (x: String) <- xs; (y, z) <- xs) do () - for ((x: String) <- xs; case (y, z) <- xs) do () - - val pairs: List[AnyRef] = List((1, 2), "hello", (3, 4)) - for ((x, y) <- pairs) yield (y, x) - - // from unchecked-patterns.scala - val y :: ys = List(1, 2, 3) - val (1, c) = (1, 2) - val 1 *: cs = 1 *: Tuple() - - val (_: Int | _: AnyRef) = ??? : AnyRef - - val 1 = 2 - - 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 - val Some(s1) = Option(1) diff --git a/tests/pos/t1048-maybe.scala b/tests/pos/t1048-maybe.scala deleted file mode 100644 index 7f0263d6ec4e..000000000000 --- a/tests/pos/t1048-maybe.scala +++ /dev/null @@ -1,16 +0,0 @@ -//> using options -Yexplicit-nulls -import language.experimental.magic -trait T[U] { - def x: T[_ <: U] -} - -object T { - def unapply[U](t: T[U]): T[_ <: U]? = t.x -} - -object Test { - def f[W](t: T[W]) = t match { - case T(T(_)) => () - } -} - diff --git a/tests/pos/t3136-maybe.scala b/tests/pos/t3136-maybe.scala deleted file mode 100644 index 093ef28e8732..000000000000 --- a/tests/pos/t3136-maybe.scala +++ /dev/null @@ -1,21 +0,0 @@ -//> using options -Yexplicit-nulls -import language.experimental.magic -class Type -class Symbol -case class PolyType(tps: List[Symbol], res: Type) extends Type -class OtherType extends Type - -// case class NullaryMethodType(tp: Type) extends Type - -object NullaryMethodType { - def apply(resTpe: Type): Type = PolyType(List(), resTpe) - def unapply(tp: Type): (Type)? = null -} - -object Test { - def TEST(tp: Type): String = - tp match { - case PolyType(ps1, PolyType(ps2, res @ PolyType(a, b))) => "1" + tp // couldn't find a simpler version that still crashes - case NullaryMethodType(meh) => "2" + meh - } -} diff --git a/tests/pos/t5041-maybe.scala b/tests/pos/t5041-maybe.scala deleted file mode 100644 index 661b6b4a8dc0..000000000000 --- a/tests/pos/t5041-maybe.scala +++ /dev/null @@ -1,11 +0,0 @@ -//> using options -Yexplicit-nulls -import language.experimental.magic -case class Token(text: String, startIndex: Int) - -object Comment { - def unapply(s: String): Token? = null -} - -object HiddenTokens { - "foo" match { case Comment(_) => } -} diff --git a/tests/pos/t796-maybe.scala b/tests/pos/t796-maybe.scala deleted file mode 100644 index 8f3876ddc314..000000000000 --- a/tests/pos/t796-maybe.scala +++ /dev/null @@ -1,28 +0,0 @@ -//> using options -Yexplicit-nulls -/** I know what I am doing is wrong -- since I am about to look into - * this bug, I add a test in pending/pos... however, I am afraid that - * once this bug is fixed, this test case might go into test/pos - * there it adds to the huge number of tiny little test cases. - * - * Ideally, an option in the bugtracking system would automatically - * handle "pos" bugs. - */ -import language.experimental.magic -object Test extends App { - - object Twice { - def apply(x: Int) = x * 2 - def unapply(x: Int): Tuple1[Int]? = - if (x % 2 == 0) Tuple1(x / 2) - else null - } - - def test(x: Int) = x match { - case Twice(y) => "x is two times " + y - case _ => "x is odd" - } - - Console.println(test(3)) - Console.println(test(4)) - -} diff --git a/tests/pos/t8045-maybe.scala b/tests/pos/t8045-maybe.scala deleted file mode 100644 index 2bee57b44a3c..000000000000 --- a/tests/pos/t8045-maybe.scala +++ /dev/null @@ -1,19 +0,0 @@ -//> using options -Yexplicit-nulls -import language.experimental.magic -object Test extends App { - case class Number(i: Int) - - object UnliftNumber { - def unapply(t: Any): Number? = t match { - case i: Int => Number(i) - case _ => null - } - } - - def eval(expr: Any): Option[Number] = expr match { - case UnliftNumber(n) => Some(n) - case _ => None - } - - println(eval(1)) -} diff --git a/tests/pos/unapplyComplex-maybe.scala b/tests/pos/unapplyComplex-maybe.scala deleted file mode 100644 index a4950f43a913..000000000000 --- a/tests/pos/unapplyComplex-maybe.scala +++ /dev/null @@ -1,41 +0,0 @@ -//> using options -Yexplicit-nulls -import language.experimental.magic -trait Complex extends Product2[Double, Double] { - def canEqual(other: Any) = other.isInstanceOf[Complex] -} - -class ComplexRect(val _1: Double, val _2: Double) extends Complex { - override def toString = "ComplexRect("+_1+","+_2+")" -} - -class ComplexPolar(val _1: Double, val _2: Double) extends Complex { - override def toString = "ComplexPolar("+_1+","+_2+")" -} - -object ComplexRect { - def unapply(z:Complex): Complex? = { - if (z.isInstanceOf[ComplexRect]) z else z match { - case ComplexPolar(mod, arg) => - new ComplexRect(mod*math.cos(arg), mod*math.sin(arg)) -} } } - -object ComplexPolar { - def unapply(z:Complex): Complex? = { - if (z.isInstanceOf[ComplexPolar]) z else z match { - case ComplexRect(re,im) => - new ComplexPolar(math.sqrt(re*re + im*im), math.atan(re/im)) -} } } - -object Test { - def main(args:Array[String]) = { - new ComplexRect(1,1) match { - case ComplexPolar(mod,arg) => // z @ ??? - Console.println("mod"+mod+"arg"+arg) - } - val Komplex = ComplexRect - new ComplexPolar(math.sqrt(2),math.Pi / 4.0) match { - case Komplex(re,im) => // z @ ??? - Console.println("re"+re+" im"+im) - } - } -} diff --git a/tests/pos/unapplyVal-maybe.scala b/tests/pos/unapplyVal-maybe.scala deleted file mode 100644 index d31b2c17656e..000000000000 --- a/tests/pos/unapplyVal-maybe.scala +++ /dev/null @@ -1,39 +0,0 @@ -//> using options -Yexplicit-nulls -package test // bug #1215 - -import language.experimental.magic -class Async { - def unapply(scrut: Any): Any? = null -} - -class Buffer { - val Put = new Async - //case class Put(x: Int) - - def joinPat(x: Any): Unit = { - x match { - case Put => - case Put(y) => - println("returning " + y) - } - } -} - - -object unapplyJoins extends App { // bug #1257 - - class Sync { - def apply(): Int = 42 - def unapply(scrut: Any): Boolean = false - } - - class Buffer { - object Get extends Sync - - val jp: PartialFunction[Any, Any] = { - case Get() => - } - } - - println((new Buffer).jp.isDefinedAt(42)) -} diff --git a/tests/run/fully-abstract-interface-maybe.check b/tests/run/fully-abstract-interface-maybe.check deleted file mode 100644 index 181c6eb0d1ec..000000000000 --- a/tests/run/fully-abstract-interface-maybe.check +++ /dev/null @@ -1,29 +0,0 @@ -CaseClassImplementation -underlying rep: class CaseClassImplementation$Const -1 -test1 OK -1 = 1 -test2 OK -1 = 1 - -underlying rep: class CaseClassImplementation$App -7 -test3 OK -AppliedOp(PlusOp, Const(1), App(MultOp,Const(2),Const(3))) = 7 -test4 OK -AppliedOp(PlusOp, Const(1), App(MultOp,Const(2),Const(3))) = 7 - -ListImplementation -underlying rep: class scala.collection.immutable.$colon$colon -1 -test1 OK -1 = 1 -test2 OK -1 = 1 - -underlying rep: class scala.collection.immutable.$colon$colon -7 -test3 OK -AppliedOp(List(+), List(1), List(List(*), List(2), List(3))) = 7 -test4 OK -AppliedOp(List(+), List(1), List(List(*), List(2), List(3))) = 7 diff --git a/tests/run/fully-abstract-interface-maybe.scala b/tests/run/fully-abstract-interface-maybe.scala deleted file mode 100644 index 79f6bc8fa4c8..000000000000 --- a/tests/run/fully-abstract-interface-maybe.scala +++ /dev/null @@ -1,331 +0,0 @@ -//> using options -Yexplicit-nulls -import language.experimental.magic -import scala.reflect.ClassTag - -object Test { - def main(args: Array[String]): Unit = { - println("CaseClassImplementation") - testInterface(CaseClassImplementation) - - println() - - println("ListImplementation") - testInterface(ListImplementation) - } - - def testInterface(arithmetic: Arithmetic): Unit = { - import arithmetic.* - val const1 = Constant(1) - println("underlying rep: " + const1.getClass) - println(const1.eval) - - const1 match { - case AppliedOp(_, _, _) => - println("test1 fail") - case c @ Constant(n) => - println("test1 OK") - println(s"$n = ${c.eval}") - } - - const1 match { - case _: AppliedOp => - println("test2 fail") - case c: Constant => - println("test2 OK") - println(s"${c.num} = ${c.eval}") - } - println() - - // 1 + (2 * 3) - val applied = AppliedOp(Op.Puls(), Constant(1), AppliedOp(Op.Mult(), Constant(2), Constant(3))) - - println("underlying rep: " + applied.getClass) - println(applied.eval) - - applied match { - case c @ Constant(n) => - println("test3 fail") - case a @ AppliedOp(op, x, y) => - println("test3 OK") - println(s"AppliedOp($op, $x, $y) = ${a.eval}") - } - - applied match { - case c: Constant => - println("test4 fail") - case a: AppliedOp => - println("test4 OK") - println(s"AppliedOp(${a.op}, ${a.lhs}, ${a.rhs}) = ${a.eval}") - } - - } -} - -abstract class Arithmetic { - - // === Numbers ========================================== - // Represents: - // trait Number - // case class Constant(n: Int) extends Number - // case class AppliedOp(op: Op, lhs: Number, rhs: Number) extends Number - - type Number - implicit def numberClassTag: ClassTag[Number] - - trait AbstractNumber { - def thisNumber: Number - def eval: Int = thisNumber match { - case Constant(n) => n - case AppliedOp(op, x, y) => op(x, y) - } - } - implicit def NumberDeco(t: Number): AbstractNumber - - // --- Constant ---------------------------------------- - - type Constant <: Number - implicit def constantClassTag: ClassTag[Constant] - - val Constant: ConstantExtractor - abstract class ConstantExtractor { - def apply(x: Int): Constant - def unapply(x: Constant): Int? - } - trait AbstractConstant { - def num: Int - } - implicit def ConstantDeco(t: Constant): AbstractConstant - - // --- AppliedOp ---------------------------------------- - - type AppliedOp <: Number - implicit def appliedOpClassTag: ClassTag[AppliedOp] - - trait AbstractAppliedOp { - def op: Op - def lhs: Number - def rhs: Number - } - implicit def AppliedOpDeco(t: AppliedOp): AbstractAppliedOp - - val AppliedOp: AppliedOpExtractor - abstract class AppliedOpExtractor { - def apply(op: Op, x: Number, y: Number): AppliedOp - def unapply(x: AppliedOp): (Op, Number, Number)? - } - - // === Operations ======================================= - // Represents: - // trait Op - // case object Puls extends Op - // case object Mult extends Op - - type Op - implicit def opClassTag: ClassTag[Op] - - trait AbstractOp { - def thisOp: Op - def apply(x: Number, y: Number): Int = thisOp match { - case Op.Puls() => x.eval + y.eval - case Op.Mult() => x.eval * y.eval - } - } - implicit def OpDeco(t: Op): AbstractOp - - val Op: OpModule - abstract class OpModule { - val Puls: PulsExtractor - abstract class PulsExtractor { - def apply(): Op - def unapply(x: Op): Boolean - } - - val Mult: MultExtractor - abstract class MultExtractor { - def apply(): Op - def unapply(x: Op): Boolean - } - } -} - -object CaseClassImplementation extends Arithmetic { - - // === Numbers ========================================== - // Represented as case classes - - sealed trait Num - final case class Const(n: Int) extends Num - final case class App(op: Op, x: Num, y: Num) extends Num - - type Number = Num - - def numberClassTag: ClassTag[Number] = implicitly - - def NumberDeco(t: Number): AbstractNumber = new AbstractNumber { - def thisNumber: Number = t - } - - // --- Constant ---------------------------------------- - - type Constant = Const - def constantClassTag: ClassTag[Constant] = implicitly - - def ConstantDeco(const: Constant): AbstractConstant = new AbstractConstant { - def num: Int = const.n - } - - object Constant extends ConstantExtractor { - def apply(x: Int): Constant = Const(x) - def unapply(x: Constant): Int? = x.n - } - - // --- AppliedOp ---------------------------------------- - - def AppliedOpDeco(t: AppliedOp): AbstractAppliedOp = new AbstractAppliedOp { - def op: Op = t.op - def lhs: Number = t.x - def rhs: Number = t.y - } - - type AppliedOp = App - def appliedOpClassTag: ClassTag[AppliedOp] = implicitly - - object AppliedOp extends AppliedOpExtractor { - def apply(op: Op, x: Number, y: Number): AppliedOp = App(op, x, y) - def unapply(app: AppliedOp): (Op, Number, Number)? = (app.op, app.x, app.y) - } - - // === Operations ======================================= - // Represented as case classes - - sealed trait Operation - case object PlusOp extends Operation - case object MultOp extends Operation - - type Op = Operation - def opClassTag: ClassTag[Op] = implicitly - - def OpDeco(t: Op): AbstractOp = new AbstractOp { - def thisOp: Op = t - } - - object Op extends OpModule { - object Puls extends PulsExtractor { - def apply(): Op = PlusOp - def unapply(x: Op): Boolean = x == PlusOp - } - object Mult extends MultExtractor { - def apply(): Op = MultOp - def unapply(x: Op): Boolean = x == MultOp - } - } -} - -object ListImplementation extends Arithmetic { - // Logically represented as: - // type Number <: List[Any] - // type Constant <: Number // List(n: Int) - // type AppliedOp <: Number // List(op: Op, lhs: Number, rhs: Number) - // - // type Op <: List[Any] // List(id: "+" | "*") - - // === Numbers ========================================== - - type Number = List[Any] - - def numberClassTag: ClassTag[Number] = new ClassTag[Number] { - def runtimeClass: Class[_] = classOf[List[_]] - override def unapply(x: Any): Option[List[Any]] = x match { - case ls: List[Any] if ls.length == 3 || (ls.length == 1 && ls(0).isInstanceOf[Int]) => - // Test that it is one of: - // type Constant <: Number // List(n: Int) - // type AppliedOp <: Number // List(op: Op, lhs: Number, rhs: Number) - Some(ls) - case _ => None - } - } - - def NumberDeco(t: Number): AbstractNumber = new AbstractNumber { - def thisNumber: Number = t - } - - // --- Constant ---------------------------------------- - - type Constant = List[Any] // List(n: Int) - def constantClassTag: ClassTag[Constant] = new ClassTag[Constant] { - def runtimeClass: Class[_] = classOf[List[_]] - override def unapply(x: Any): Option[List[Any]] = x match { - case ls: List[Any] if ls.length == 1 && ls(0).isInstanceOf[Int] => - // Test that it is: - // type Constant <: Number // List(n: Int) - Some(ls) - case _ => None - } - } - - def ConstantDeco(const: Constant): AbstractConstant = new AbstractConstant { - def num: Int = const(0).asInstanceOf[Int] - } - - object Constant extends ConstantExtractor { - def apply(x: Int): Constant = List(x) - def unapply(x: Constant): Int? = ConstantDeco(x).num - } - - // --- AppliedOp ---------------------------------------- - - def AppliedOpDeco(t: AppliedOp): AbstractAppliedOp = new AbstractAppliedOp { - def op: Op = t(0).asInstanceOf[Op] - def lhs: Number = t(1).asInstanceOf[Number] - def rhs: Number = t(2).asInstanceOf[Number] - } - - type AppliedOp = List[Any] // List(op: Op, lhs: Number, rhs: Number) - def appliedOpClassTag: ClassTag[AppliedOp] = new ClassTag[AppliedOp] { - def runtimeClass: Class[_] = classOf[List[_]] - override def unapply(x: Any): Option[List[Any]] = x match { - case ls: List[Any] if ls.length == 3 => - // Test that it is: - // type AppliedOp <: Number // List(op: Op, lhs: Number, rhs: Number) - Some(ls) - case _ => None - } - } - - object AppliedOp extends AppliedOpExtractor { - def apply(op: Op, x: Number, y: Number): AppliedOp = List(op, x, y) - def unapply(app: AppliedOp): (Op, Number, Number)? = { - val app2 = AppliedOpDeco(app) - (app2.op, app2.lhs, app2.rhs) - } - } - - // === Operations ======================================= - - type Op = List[Any] - def opClassTag: ClassTag[Op] = new ClassTag[Constant] { - def runtimeClass: Class[_] = classOf[List[_]] - override def unapply(x: Any): Option[List[Any]] = x match { - case op @ (("+" | "*") :: Nil) => - // Test that it is: - // type Op <: List[Any] // List(id: "+" | "*") - Some(op) - case _ => None - } - } - - def OpDeco(t: Op): AbstractOp = new AbstractOp { - def thisOp: Op = t - } - - object Op extends OpModule { - object Puls extends PulsExtractor { - def apply(): Op = List("+") - def unapply(x: Op): Boolean = x(0) == "+" - } - object Mult extends MultExtractor { - def apply(): Op = List("*") - def unapply(x: Op): Boolean = x(0) == "*" - } - } -} \ No newline at end of file diff --git a/tests/run/fully-abstract-nat-1-maybe.check b/tests/run/fully-abstract-nat-1-maybe.check deleted file mode 100644 index 37bcfe2c2448..000000000000 --- a/tests/run/fully-abstract-nat-1-maybe.check +++ /dev/null @@ -1,13 +0,0 @@ -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-1-maybe.scala b/tests/run/fully-abstract-nat-1-maybe.scala deleted file mode 100644 index f9e6f0c13a1c..000000000000 --- a/tests/run/fully-abstract-nat-1-maybe.scala +++ /dev/null @@ -1,144 +0,0 @@ -//> using options -Yexplicit-nulls - -import language.experimental.magic -object Test { - def main(args: Array[String]): Unit = { - println("CaseNums") - test(CaseNums) - println() - println("IntNums") - test(IntNums) - } - - def test(numbers: Numbers) = { - import numbers.* - - val zero: Nat = Zero() - val one: Nat = Succ(zero) - val two: Nat = Succ(one) - val three: Nat = Succ(two) - - zero match { - case Succ(p) => println("error") - case Zero() => println("ok") - } - - one match { - case Zero() => println("error") - case Succ(p) => println("ok") - } - - zero match { - case s: Succ => println("ok - unchecked error") - case z: Zero => println("ok - unchecked no error") - } - - def divOpt(a: Nat, b: Nat): Option[(Nat, Nat)] = b match { - case s @ Succ(_) => - // s is of type Nat though we know it is a Succ - Some(safeDiv(a, s.asInstanceOf[Succ])) - 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? - } - - implicit 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 - } - } - - 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)) (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) nat - 1 else null - } - - def SuccDeco(succ: Succ): SuccAPI = new SuccAPI { - def pred: Int = succ - 1 - } - - def safeDiv(a: Nat, b: Succ): (Nat, Nat) = (a / b, a % b) -} diff --git a/tests/run/fully-abstract-nat-2-maybe.check b/tests/run/fully-abstract-nat-2-maybe.check deleted file mode 100644 index d381a1625cc5..000000000000 --- a/tests/run/fully-abstract-nat-2-maybe.check +++ /dev/null @@ -1,13 +0,0 @@ -CaseNums -ok -ok -ok -None -Some((SuccClass(ZeroObj),SuccClass(ZeroObj))) - -IntNums -error -error -error -/ by zero -Some((1,1)) diff --git a/tests/run/fully-abstract-nat-2-maybe.scala b/tests/run/fully-abstract-nat-2-maybe.scala deleted file mode 100644 index 82fcb170acba..000000000000 --- a/tests/run/fully-abstract-nat-2-maybe.scala +++ /dev/null @@ -1,155 +0,0 @@ -//> using options -Yexplicit-nulls - -import language.experimental.magic -import scala.reflect.ClassTag - -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("error") // Will happen with IntNums - case z: Zero => println("ok") - } - - def divOpt(a: Nat, b: Nat): Option[(Nat, Nat)] = b match { - case s @ Succ(_) => Some(safeDiv(a, s)) - case _ => None - } - - try println(divOpt(one, zero)) - catch { case ex: java.lang.ArithmeticException => println(ex.getMessage) } - println(divOpt(three, two)) - } -} - -trait Numbers { - - type Nat - type Zero <: Nat - type Succ <: Nat - - implicit def natTag: ClassTag[Nat] - implicit def zeroTag: ClassTag[Zero] - implicit def succTag: ClassTag[Succ] - - val Zero: ZeroExtractor - trait ZeroExtractor { - def apply(): Zero - def unapply(zero: Zero): Boolean - } - - val Succ: SuccExtractor - trait SuccExtractor { - def apply(nat: Nat): Succ - def unapply(succ: Succ): Nat? - } - - implicit def SuccDeco(succ: Succ): SuccAPI - trait SuccAPI { - def pred: Nat - } - - def safeDiv(a: Nat, b: Succ): (Nat, Nat) -} - - -object CaseNums extends Numbers { - - trait NatClass - object ZeroObj extends NatClass { override def toString: String = "ZeroObj" } - case class SuccClass(pred: NatClass) extends NatClass - - type Nat = NatClass - type Zero = ZeroObj.type - type Succ = SuccClass - - def natTag: ClassTag[Nat] = implicitly[ClassTag[NatClass]] - def zeroTag: ClassTag[Zero] = implicitly[ClassTag[ZeroObj.type]] - def succTag: ClassTag[Succ] = implicitly[ClassTag[SuccClass]] - - object Zero extends ZeroExtractor { - def apply(): Zero = ZeroObj - def unapply(zero: Zero): Boolean = true - } - - object Succ extends SuccExtractor { - def apply(nat: Nat): Succ = SuccClass(nat) - def unapply(succ: Succ): Nat? = succ.pred - } - - 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)) (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 - - // BROKEN - // All class tags are identical: Nat, Zero and Succ cannot be distinguished - def natTag: ClassTag[Int] = ClassTag.Int - def zeroTag: ClassTag[Int] = ClassTag.Int // will also match any Int that is non zero - def succTag: ClassTag[Int] = ClassTag.Int // will also match 0 - - object Zero extends ZeroExtractor { - def apply(): Int = 0 - def unapply(zero: Zero): Boolean = true - } - - object Succ extends SuccExtractor { - def apply(nat: Nat): Int = nat + 1 - def unapply(succ: Succ): Int? = succ - 1 - } - - 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/i1748-maybe.check b/tests/run/i1748-maybe.check deleted file mode 100644 index 888299747af9..000000000000 --- a/tests/run/i1748-maybe.check +++ /dev/null @@ -1,2 +0,0 @@ -class - extends diff --git a/tests/run/i1748-maybe.scala b/tests/run/i1748-maybe.scala deleted file mode 100644 index 7bb0d7199347..000000000000 --- a/tests/run/i1748-maybe.scala +++ /dev/null @@ -1,16 +0,0 @@ -//> using options -Yexplicit-nulls -import language.experimental.magic -object Test { - implicit class Foo(sc: StringContext) { - object q { - def unapply(arg: Any): (Any, Any)? = - (sc.parts(0), sc.parts(1)) - } - } - - def main(args: Array[String]): Unit = { - val q"class $name extends $parent" = new Object - println(name) - println(parent) - } -} \ No newline at end of file diff --git a/tests/run/i1779-maybe.check b/tests/run/i1779-maybe.check deleted file mode 100644 index 4ef6e900e49d..000000000000 --- a/tests/run/i1779-maybe.check +++ /dev/null @@ -1 +0,0 @@ - extends diff --git a/tests/run/i1779-maybe.scala b/tests/run/i1779-maybe.scala deleted file mode 100644 index 7bfb18e97d29..000000000000 --- a/tests/run/i1779-maybe.scala +++ /dev/null @@ -1,15 +0,0 @@ -//> using options -Yexplicit-nulls -import language.experimental.magic -object Test { - implicit class Foo(sc: StringContext) { - object q { - def unapply(arg: Any): (Any, Any)? = - (sc.parts(0), sc.parts(1)) - } - } - - def main(args: Array[String]): Unit = { - val q"class $_ extends $_parent" = new Object - println(_parent) - } -} diff --git a/tests/run/i8577b-maybe.scala b/tests/run/i8577b-maybe.scala deleted file mode 100644 index a08b7e942e8d..000000000000 --- a/tests/run/i8577b-maybe.scala +++ /dev/null @@ -1,15 +0,0 @@ -//> using options -Yexplicit-nulls -import language.experimental.magic -object Macro: - opaque type StrCtx = StringContext - def apply(ctx: StringContext): StrCtx = ctx - def unapply(ctx: StrCtx): StringContext? = ctx - -extension (ctx: StringContext) def mac: Macro.StrCtx = Macro(ctx) -extension (inline ctx: Macro.StrCtx) inline def unapplySeq[U](inline input: U): Seq[U]? = - Seq(input) - -@main def Test: Unit = - val mac"$x" = 1 - val y: Int = x - assert(x == 1) diff --git a/tests/run/i8577c-maybe.scala b/tests/run/i8577c-maybe.scala deleted file mode 100644 index eb3faddef1c3..000000000000 --- a/tests/run/i8577c-maybe.scala +++ /dev/null @@ -1,15 +0,0 @@ -//> using options -Yexplicit-nulls -import language.experimental.magic -object Macro: - opaque type StrCtx = StringContext - def apply(ctx: StringContext): StrCtx = ctx - def unapply(ctx: StrCtx): StringContext? = ctx - -extension (ctx: StringContext) def mac: Macro.StrCtx = Macro(ctx) -extension [T] (inline ctx: Macro.StrCtx) inline def unapplySeq(inline input: T): Seq[T]? = - Seq(input) - -@main def Test: Unit = - val mac"$x" = 1 - val y: Int = x - assert(x == 1) diff --git a/tests/run/i8577d-maybe.scala b/tests/run/i8577d-maybe.scala deleted file mode 100644 index c69c63b4c6ec..000000000000 --- a/tests/run/i8577d-maybe.scala +++ /dev/null @@ -1,15 +0,0 @@ -//> using options -Yexplicit-nulls -import language.experimental.magic -object Macro: - opaque type StrCtx = StringContext - def apply(ctx: StringContext): StrCtx = ctx - def unapply(ctx: StrCtx): StringContext? = ctx - -extension (ctx: StringContext) def mac: Macro.StrCtx = Macro(ctx) -extension [T] (inline ctx: Macro.StrCtx) inline def unapplySeq[U](inline input: T): Seq[T]? = - Seq(input) - -@main def Test: Unit = - val mac"$x" = 1 - val y: Int = x - assert(x == 1) diff --git a/tests/run/i8577e-maybe.scala b/tests/run/i8577e-maybe.scala deleted file mode 100644 index ffc5bd240824..000000000000 --- a/tests/run/i8577e-maybe.scala +++ /dev/null @@ -1,19 +0,0 @@ -//> using options -Yexplicit-nulls -import language.experimental.magic -object Macro: - opaque type StrCtx = StringContext - def apply(ctx: StringContext): StrCtx = ctx - def unapply(ctx: StrCtx): StringContext? = ctx - -extension (ctx: StringContext) def mac: Macro.StrCtx = Macro(ctx) -extension [T] (inline ctx: Macro.StrCtx) inline def unapplySeq[U](inline input: (T, U)): Seq[(T, U)]? = - Seq(input) - -@main def Test: Unit = - val mac"$x" = (1, 2) - val x2: (Int, Int) = x - assert(x == (1, 2)) - - val mac"$y" = (1, "a") - val y2: (Int, String) = y - assert(y == (1, "a")) diff --git a/tests/run/i8577f-maybe.scala b/tests/run/i8577f-maybe.scala deleted file mode 100644 index 42d8d7263c3f..000000000000 --- a/tests/run/i8577f-maybe.scala +++ /dev/null @@ -1,15 +0,0 @@ -//> using options -Yexplicit-nulls -import language.experimental.magic -object Macro: - opaque type StrCtx = StringContext - def apply(ctx: StringContext): StrCtx = ctx - def unapply(ctx: StrCtx): StringContext? = ctx - -@main def Test: Unit = - extension (ctx: StringContext) def mac: Macro.StrCtx = Macro(ctx) - extension (inline ctx: Macro.StrCtx) inline def unapplySeq[U](inline input: U): Seq[U]? = - Seq(input) - - val mac"$x" = 1 - val y: Int = x - assert(x == 1) diff --git a/tests/run/i8577g-maybe.scala b/tests/run/i8577g-maybe.scala deleted file mode 100644 index a56cd914878c..000000000000 --- a/tests/run/i8577g-maybe.scala +++ /dev/null @@ -1,15 +0,0 @@ -//> using options -Yexplicit-nulls -import language.experimental.magic -object Macro: - opaque type StrCtx = StringContext - def apply(ctx: StringContext): StrCtx = ctx - def unapply(ctx: StrCtx): StringContext? = ctx - -extension (ctx: StringContext) def mac: Macro.StrCtx = Macro(ctx) -extension [T] (inline ctx: Macro.StrCtx) inline def unapplySeq[U](inline input: T | U): Seq[T | U]? = - Seq(input) - -@main def Test: Unit = - val mac"$x" = 1 - val y: Int = x - assert(x == 1) diff --git a/tests/run/i8577h-maybe.scala b/tests/run/i8577h-maybe.scala deleted file mode 100644 index 30aeee95eac0..000000000000 --- a/tests/run/i8577h-maybe.scala +++ /dev/null @@ -1,15 +0,0 @@ -//> using options -Yexplicit-nulls -import language.experimental.magic -object Macro: - opaque type StrCtx = StringContext - def apply(ctx: StringContext): StrCtx = ctx - def unapply(ctx: StrCtx): StringContext? = ctx - -extension (ctx: StringContext) def mac: Macro.StrCtx = Macro(ctx) -extension [T] (inline ctx: Macro.StrCtx) inline def unapplySeq[U](inline input: U | T): Seq[T | U]? = - Seq(input) - -@main def Test: Unit = - val mac"$x" = 1 - val y: Int = x - assert(x == 1) diff --git a/tests/run/patmat-maybe.check b/tests/run/patmat-maybe.check deleted file mode 100644 index c3b06e0f9d8a..000000000000 --- a/tests/run/patmat-maybe.check +++ /dev/null @@ -1,4 +0,0 @@ -Bob is 22 years old and lives in Paris -Hello Peter -Bob is 22 years old and lives in Paris -Hello PersonExtractor(Peter) diff --git a/tests/run/patmat-maybe.scala b/tests/run/patmat-maybe.scala deleted file mode 100644 index a74414453e46..000000000000 --- a/tests/run/patmat-maybe.scala +++ /dev/null @@ -1,47 +0,0 @@ -//> using options -Yexplicit-nulls -import language.experimental.magic -object Test1: - class User(val name: String, val age: Int, val city: String) - object User: - def unapply(user: User) = UserExtractor(user.name, user.age, user.city) - - case class UserExtractor(name: String, age: Int, city: String) - - class Person(val name: String) - object Person: - def unapply(person: Person) = PersonExtractor(person.name) - - case class PersonExtractor(name: String) - - def test = - val user = User("Bob", 22, "Paris") - (user: Any) match - case User(name, age, city) => println(s"$name is $age years old and lives in $city") - val p = Person("Peter") - (p: Any) match - case Person(n) => println(s"Hello $n") - -object Test2: - class User(val name: String, val age: Int, val city: String) - object User: - def unapply(user: User): UserExtractor? = UserExtractor(user.name, user.age, user.city) - - case class UserExtractor(name: String, age: Int, city: String) - - class Person(val name: String) - object Person: - def unapply(person: Person): Some[PersonExtractor] = Some(PersonExtractor(person.name)) - - case class PersonExtractor(name: String) - - def test = - val user = User("Bob", 22, "Paris") - (user: Any) match - case User(name, age, city) => println(s"$name is $age years old and lives in $city") - val p = Person("Peter") - (p: Any) match - case Person(n) => println(s"Hello $n") - -@main def Test = - Test1.test - Test2.test diff --git a/tests/run/patmat-option-named-maybe.scala b/tests/run/patmat-option-named-maybe.scala deleted file mode 100644 index f4c76d2c579f..000000000000 --- a/tests/run/patmat-option-named-maybe.scala +++ /dev/null @@ -1,23 +0,0 @@ -//> using options -Yexplicit-nulls -import language.experimental.magic -case class HasSingleField(f: HasSingleField) - -object Test { - - def main(args: Array[String]) = { - val s: Object = HasSingleField(null.asInstanceOf[HasSingleField]) - s match { - case Matcher(self) => - assert(self ne null) - } - } -} - -object Matcher { - def unapply(x: Object): HasSingleField? = { - if (x.isInstanceOf[HasSingleField]) - x.asInstanceOf[HasSingleField] - else - null - } -} diff --git a/tests/run/patmat-spec-maybe.scala b/tests/run/patmat-spec-maybe.scala deleted file mode 100644 index 68e753acda13..000000000000 --- a/tests/run/patmat-spec-maybe.scala +++ /dev/null @@ -1,63 +0,0 @@ -//> using options -Yexplicit-nulls -// To be kept in sync with docs/docs/reference/pattern-matching.md -import language.experimental.magic -object Test { - def main(args: Array[String]): Unit = { - object Even { - def unapply(s: String): Boolean = s.size % 2 == 0 - } - - "even" match { - case s @ Even() => println(s"$s has an even number of characters") - case s => println(s"$s has an odd number of characters") - } - // even has an even number of characters - - class FirstChars(s: String) extends Product { - def _1 = s.charAt(0) - def _2 = s.charAt(1) - - // Not used by pattern matching: Product is only used as a marker trait. - def canEqual(that: Any): Boolean = ??? - def productArity: Int = ??? - def productElement(n: Int): Any = ??? - } - - object FirstChars { - def unapply(s: String): FirstChars = new FirstChars(s) - } - - "Hi!" match { - case FirstChars(char1, char2) => - println(s"First: $char1; Second: $char2") - } - // First: H; Second: i - - object CharList { - def unapplySeq(s: String): Seq[Char]? = s.toList - } - - "example" match { - case CharList(c1, c2, c3, c4, _, _, _) => - println(s"$c1,$c2,$c3,$c4") - case _ => - println("Expected *exactly* 7 characters!") - } - // e,x,a,m - - class Nat(val x: Int) { - def get: Int = x - def isEmpty = x < 0 - } - - object Nat { - def unapply(x: Int): Nat = new Nat(x) - } - - 5 match { - case Nat(n) => println(s"$n is a natural number") - case _ => () - } - // 5 is a natural number - } -} diff --git a/tests/run/patmatch-classtag-maybe.scala b/tests/run/patmatch-classtag-maybe.scala deleted file mode 100644 index b6bbfbd54b87..000000000000 --- a/tests/run/patmatch-classtag-maybe.scala +++ /dev/null @@ -1,47 +0,0 @@ -//> using options -Yexplicit-nulls -import language.experimental.magic -import reflect.ClassTag -trait API { - type CaseDef - - implicit val tagForCaseDef: ClassTag[CaseDef] - - trait CaseDefCompanion { - def apply(x: String): CaseDef - def unapply(x: CaseDef): String? - } - lazy val CaseDef: CaseDefCompanion -} - -object dotc { - case class CaseDef(str: String) -} - -object Impl extends API { - type CaseDef = dotc.CaseDef - - val tagForCaseDef: ClassTag[dotc.CaseDef] = implicitly - - object CaseDef extends CaseDefCompanion { - def apply(str: String): CaseDef = dotc.CaseDef(str) - def unapply(x: CaseDef): String? = x.str - } -} - -object Test extends App { - val api: API = Impl - import api.* - - val x: Any = CaseDef("123") - - x match { - case cdef: CaseDef => - val x: CaseDef = cdef - println(cdef) - } - x match { - case cdef @ CaseDef(s) => - val x: CaseDef = cdef - println(s) - } -} diff --git a/tests/run/reducable-maybe.scala b/tests/run/reducable-maybe.scala deleted file mode 100644 index ac3f823bb6b0..000000000000 --- a/tests/run/reducable-maybe.scala +++ /dev/null @@ -1,64 +0,0 @@ -//> using options -Yexplicit-nulls -import language.experimental.magic -object Test extends App { - object Cons { - var count = 0 - - def unapply[T](xs: List[T]): (T, List[T])? = { - count += 1 - xs match { - case x :: xs1 => (x, xs1) - case _ => null - } - } - } - - object Guard { - var count = 0 - - def apply(): Boolean = { - count += 1 - false - } - } - - def reset(): Unit = { - Cons.count = 0 - Guard.count = 0 - } - - val xs = List(1, 2, 3) - test1(xs) - reset() - test2(xs) - - def test1(xs: List[Int]): Unit = { - val res = xs match { - case Cons(0, Nil) => 1 - case Cons(_, Nil) => 2 - case Cons(0, _) => 3 - case Cons(1, ys) => 4 - } - - assert(res == 4, res) - assert(Cons.count == 1, Cons.count) - } - - // #1313 - def test2(xs: List[Int]): Unit = { - val res = xs match { - case Cons(0, Nil) if Guard() => 1 - case Cons(0, Nil) => 2 - case Cons(_, Nil) if Guard() => 3 - case Cons(_, Nil) => 4 - case Cons(0, _) if Guard() => 5 - case Cons(0, _) => 6 - case Cons(1, ys) if Guard() => 7 - case Cons(1, ys) => 8 - } - - assert(res == 8, res) - assert(Cons.count == 1, Cons.count) - assert(Guard.count == 1, Guard.count) - } -} diff --git a/tests/run/t1048-maybe.check b/tests/run/t1048-maybe.check deleted file mode 100644 index f1e5eeed2d93..000000000000 --- a/tests/run/t1048-maybe.check +++ /dev/null @@ -1,2 +0,0 @@ -3 -2 diff --git a/tests/run/t1048-maybe.scala b/tests/run/t1048-maybe.scala deleted file mode 100644 index ba8fe9922108..000000000000 --- a/tests/run/t1048-maybe.scala +++ /dev/null @@ -1,23 +0,0 @@ -//> using options -Yexplicit-nulls -import language.experimental.magic -final case class W[A](v: A) - -object E { - def unapply(w: W[Any]): Any? = null -} - -object Bug { - def bug[A](e: Either[W[_], A]) = e match { - case Left(E(x)) => 1 - case Right(x) => 2 - case _ => 3 - } -} - -object Test { - def main(args: Array[String]): Unit = { - println(Bug.bug(Left(W(5)))) - println(Bug.bug(Right(5))) - } -} - diff --git a/tests/run/t1220-maybe.scala b/tests/run/t1220-maybe.scala deleted file mode 100644 index ca84cbf97bc6..000000000000 --- a/tests/run/t1220-maybe.scala +++ /dev/null @@ -1,17 +0,0 @@ -//> using options -Yexplicit-nulls -import language.experimental.magic -object Test extends App { - - class QSRichIterable[A](self: Iterable[A]) { - def filterMap[R](f: PartialFunction[A,R]) = - self filter (f.isDefinedAt) map f - } - - object Un { - def unapply(i: Int): Int? = i - } - - val richIter = new QSRichIterable(List(0, 1, 2, 3, 4)) - - assert((richIter filterMap {case Un(3) => 7}) == List(7)) -} diff --git a/tests/run/t4415-maybe.scala b/tests/run/t4415-maybe.scala deleted file mode 100644 index 3c00ac7abb1f..000000000000 --- a/tests/run/t4415-maybe.scala +++ /dev/null @@ -1,88 +0,0 @@ -//> using options -Yexplicit-nulls -/** - * Demonstration of issue with Extractors. If lines 15/16 are not present, get at runtime: - * - * Exception in thread "main" java.lang.VerifyError: (class: ExtractorIssue$$, method: convert signature: (LTopProperty;)LMyProp;) Accessing value from uninitialized register 5 - * at ExtractorIssue.main(ExtractorIssue.scala) - * at com.intellij.rt.execution.application.AppMain.main(AppMain.java:115)] - * - * If lines 15/16 are present, the compiler crashes: - * - * fatal error (server aborted): not enough arguments for method body%3: (val p: MyProp[java.lang.String])MyProp[_33]. - * Unspecified value parameter p. - */ -import language.experimental.magic -object Test { - - def main(args: Array[String]): Unit = { - convert(new SubclassProperty) - } - - def convert(prop: TopProperty): MyProp[_] = { - prop match { - - /////////////////////////////////////////////////////////////////////////////////////////////////////////////// - //case SubclassSecondMatch(p) => p // if these lines are present, the compiler crashes. If commented, unsafe byte - //case SecondMatch(p) => p // byte code is generated, which causes a java.lang.VerifyError at runtime - /////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - case SubclassMatch(p) => p - case StandardMatch(p) => p - } - } -} - -class TopProperty - -class StandardProperty extends TopProperty -class SubclassProperty extends StandardProperty - -class SecondProperty extends TopProperty -class SubclassSecondProperty extends StandardProperty - -trait MyProp[T] -case class MyPropImpl[T]() extends MyProp[T] - -object SubclassMatch { - - def unapply(prop: SubclassProperty) : MyProp[String]? = { - new MyPropImpl - } - - def apply(prop: MyProp[String]) : SubclassProperty = { - new SubclassProperty() - } -} - -object StandardMatch { - - def unapply(prop: StandardProperty) : MyProp[String]? = { - new MyPropImpl - } - - def apply(prop: MyProp[String]) : StandardProperty = { - new StandardProperty() - } -} - -object SubclassSecondMatch { - - def unapply(prop: SubclassSecondProperty) : MyProp[BigInt]? = { - new MyPropImpl - } - - def apply(prop: MyProp[String]) : SubclassSecondProperty = { - new SubclassSecondProperty() - } -} - -object SecondMatch { - - def unapply(prop: SecondProperty) : MyProp[BigInt]? = { - new MyPropImpl - } - - def apply(prop: MyProp[String]) : SecondProperty = { - new SecondProperty() - } -} diff --git a/tests/run/t7214-maybe.scala b/tests/run/t7214-maybe.scala deleted file mode 100644 index 1d457c2f7e1a..000000000000 --- a/tests/run/t7214-maybe.scala +++ /dev/null @@ -1,61 +0,0 @@ -//> using options -Yexplicit-nulls -// scalajs: --skip - -// pattern matcher crashes here trying to synthesize an uneeded outer test. -// no-symbol does not have an owner -// at scala.reflect.internal.SymbolTable.abort(SymbolTable.scala:49) -// at scala.tools.nsc.Global.abort(Global.scala:253) -// at scala.reflect.internal.Symbols$NoSymbol.owner(Symbols.scala:3248) -// at scala.reflect.internal.Symbols$Symbol.effectiveOwner(Symbols.scala:678) -// at scala.reflect.internal.Symbols$Symbol.isDefinedInPackage(Symbols.scala:664) -// at scala.reflect.internal.TreeGen.mkAttributedSelect(TreeGen.scala:188) -// at scala.reflect.internal.TreeGen.mkAttributedRef(TreeGen.scala:124) -// at scala.tools.nsc.ast.TreeDSL$CODE$.REF(TreeDSL.scala:308) -// at scala.tools.nsc.typechecker.PatternMatching$TreeMakers$TypeTestTreeMaker$treeCondStrategy$.outerTest(PatternMatching.scala:1209) -import language.experimental.magic -class Crash { - type Alias = C#T - - val c = new C - val t = new c.T - - // Crash via a Typed Pattern... - (t: Any) match { - case e: Alias => - } - - // ... or via a Typed Extractor Pattern. - object Extractor { - def unapply(a: Alias): Any? = null - } - (t: Any) match { - case Extractor(_) => - case _ => - } - - // checking that correct outer tests are applied when - // aliases for path dependent types are involved. - val c2 = new C - type CdotT = c.T - type C2dotT = c2.T - - val outerField = t.getClass.getDeclaredFields.find(_.getName contains ("outer")).get - outerField.setAccessible(true) - - (t: Any) match { - case _: C2dotT => - println(s"!!! wrong match. t.outer=${outerField.get(t)} / c2 = $c2") // this matches on 2.10.0 - case _: CdotT => - case _ => - println(s"!!! wrong match. t.outer=${outerField.get(t)} / c = $c") - } -} - -class C { - class T -} - -object Test extends App { - new Crash -} - diff --git a/tests/run/tuple-patterns-maybe.check b/tests/run/tuple-patterns-maybe.check deleted file mode 100644 index 4aacb5183367..000000000000 --- a/tests/run/tuple-patterns-maybe.check +++ /dev/null @@ -1,9 +0,0 @@ -2 -2 -3 -1 -10 -23 -1 -10 -23 diff --git a/tests/run/tuple-patterns-maybe.scala b/tests/run/tuple-patterns-maybe.scala deleted file mode 100644 index 01485ac1d481..000000000000 --- a/tests/run/tuple-patterns-maybe.scala +++ /dev/null @@ -1,42 +0,0 @@ -//> using options -Yexplicit-nulls -import language.experimental.magic -object Test extends App { - (1, 2) match { - case (1, x) => println(x) - } - val x: Any = (1, 2) - x match { - case (1, x) => println(x) - } - - //final class tuple2[+A, +B](_1: A, _2: B) - - final class TupleXXL1 private (es: Array[Object]) { - override def toString = elems.mkString("(", ",", ")") - def elems: Array[Object] = es - } - object TupleXXL1 { - def apply(elems: Array[Object]) = new TupleXXL1(elems.clone) - def apply(elems: Any*) = new TupleXXL1(elems.asInstanceOf[Seq[Object]].toArray) - def unapplySeq(x: TupleXXL1): Seq[Any]? = x.elems.toSeq - } - - val x3 = TupleXXL1(1, 2, 3) - x3 match { - case TupleXXL1(x1, x2, x3) => println(x3) - } - - val x23 = (1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23) - x23 match { - case (x1, x2, x3, x4, x5, x6, x7, x8, x9, x10, x11, x12, x13, x14, x15, x16, x17, x18, x19, x20, x21, x22, x23) => - println(x1) - println(x10) - println(x23) - } - (x23: Any) match { - case (x1, x2, x3, x4, x5, x6, x7, x8, x9, x10, x11, x12, x13, x14, x15, x16, x17, x18, x19, x20, x21, x22, x23) => - println(x1) - println(x10) - println(x23) - } -} diff --git a/tests/run/type-test-binding-maybe.check b/tests/run/type-test-binding-maybe.check deleted file mode 100644 index 14e859cfe54b..000000000000 --- a/tests/run/type-test-binding-maybe.check +++ /dev/null @@ -1,2 +0,0 @@ -ok -9 diff --git a/tests/run/type-test-binding-maybe.scala b/tests/run/type-test-binding-maybe.scala deleted file mode 100644 index 81643cde82b4..000000000000 --- a/tests/run/type-test-binding-maybe.scala +++ /dev/null @@ -1,36 +0,0 @@ -//> using options -Yexplicit-nulls -import language.experimental.magic -import scala.reflect.TypeTest - -sealed trait Foo { - - type X - type Y <: X - - def x: X - - def f(y: Y) = println("ok") - - given TypeTest[X, Y] = new TypeTest { - def unapply(x: X): Option[x.type & Y] = - Some(x.asInstanceOf[x.type & Y]) - } - - object Z { - def unapply(arg: Y): Int? = 9 - } -} - -object Test { - def main(args: Array[String]): Unit = { - test(new Foo { type X = Int; type Y = Int; def x: X = 1 }) - } - - def test(foo: Foo): Unit = { - foo.x match { - case x @ foo.Z(i) => // `x` is refined to type `foo.Y` - foo.f(x) - println(i) - } - } -} diff --git a/tests/run/unchecked-patterns-maybe.scala b/tests/run/unchecked-patterns-maybe.scala deleted file mode 100644 index adb96bed96ac..000000000000 --- a/tests/run/unchecked-patterns-maybe.scala +++ /dev/null @@ -1,13 +0,0 @@ -//> using options -Yexplicit-nulls -import language.experimental.magic -object Test extends App { - val x: Int = 2: @unchecked - val (y1: Some[Int]) = Some(1): Option[Int] @unchecked - - val a :: as = List(1, 2, 3): @unchecked - val lst @ b :: bs = List(1, 2, 3): @unchecked - val (1, c) = (1, 2): @unchecked - - object Positive { def unapply(i: Int): Int? = if i > 0 then i else null } - val Positive(p) = 5: @unchecked -} \ No newline at end of file diff --git a/tests/run/virtpatmat_stringinterp-maybe.check b/tests/run/virtpatmat_stringinterp-maybe.check deleted file mode 100644 index 7927f4f2d95a..000000000000 --- a/tests/run/virtpatmat_stringinterp-maybe.check +++ /dev/null @@ -1 +0,0 @@ -Node(1) diff --git a/tests/run/virtpatmat_stringinterp-maybe.scala b/tests/run/virtpatmat_stringinterp-maybe.scala deleted file mode 100644 index 0106050d5621..000000000000 --- a/tests/run/virtpatmat_stringinterp-maybe.scala +++ /dev/null @@ -1,18 +0,0 @@ -//> using options -Yexplicit-nulls - -import language.experimental.magic -import scala.language.implicitConversions - -object Test extends App { - case class Node(x: Int) - - implicit def sc2xml(sc: StringContext): XMLContext = new XMLContext(sc) - class XMLContext(sc: StringContext) { - object xml { - def unapplySeq(xml: Node): Seq[Node]? = List(Node(1)) - } - } - - val x: Node = Node(0) - x match { case xml"""""" => println(a) } -} From 1dc83956adc994bab61b5d0ab416173a76d4371a Mon Sep 17 00:00:00 2001 From: odersky Date: Fri, 21 Aug 2026 13:52:52 +0200 Subject: [PATCH 23/28] Make newSyntax setting depend on language imports --- compiler/src/dotty/tools/dotc/parsing/Scanners.scala | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) 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 From d33bb1eb25d3ca98530885a6cdf99c5a6f3c017e Mon Sep 17 00:00:00 2001 From: odersky Date: Fri, 21 Aug 2026 15:30:32 +0200 Subject: [PATCH 24/28] Set source version to future under magic --- .../src/dotty/tools/dotc/config/Feature.scala | 1 + tests/neg/i1793-maybe.scala | 2 +- tests/neg/i2378-maybe.scala | 4 +- tests/neg/patmat2-maybe.scala | 2 +- ...table-pattern-binding-messages-maybe.check | 36 ++++----- ...table-pattern-binding-messages-maybe.scala | 8 +- tests/neg/unchecked-patterns-maybe.scala | 15 ++-- tests/new/test.scala | 4 +- tests/patmat/optionless-maybe.check | 6 +- tests/patmat/optionless-maybe.scala | 8 +- .../captures/i24729-maybe.scala | 4 +- tests/pos/StringContext-maybe.scala | 74 +++++++++---------- tests/pos/extractor-types-maybe.scala | 4 +- tests/pos/i14896-maybe.scala | 2 +- tests/pos/i1793-maybe.scala | 2 +- tests/pos/i18175-maybe.scala | 2 +- tests/pos/misc-unapply_pos-maybe.scala | 2 +- tests/run-bootstrapped/parse-dates.check | 20 +++++ tests/run-bootstrapped/parse-dates.scala | 28 +++++++ .../stdlibExperimentalDefinitions.scala | 2 - tests/run/fully-abstract-nat-3-maybe.scala | 8 +- tests/run/fully-abstract-nat-maybe.scala | 42 ++++++----- tests/run/i1773-maybe.scala | 5 +- tests/run/i4177-maybe.scala | 2 +- tests/run/i8577a-maybe.scala | 2 +- tests/run/string-extractor-maybe.scala | 12 +-- tests/run/t6111-maybe.scala | 4 +- tests/run/tryPatternMatch-maybe.scala | 2 +- tests/run/type-test-nat-maybe.scala | 4 +- tests/run/unapply-maybe.scala | 6 +- tests/run/virtpatmat_unapply-maybe.scala | 2 +- tests/warn/maybe-typetest.check | 18 ++++- tests/warn/maybe-typetest.scala | 4 +- .../strict-pattern-bindings-3.2-maybe.scala | 39 ---------- 34 files changed, 203 insertions(+), 173 deletions(-) create mode 100644 tests/run-bootstrapped/parse-dates.check delete mode 100644 tests/warn/strict-pattern-bindings-3.2-maybe.scala diff --git a/compiler/src/dotty/tools/dotc/config/Feature.scala b/compiler/src/dotty/tools/dotc/config/Feature.scala index c968650fe2a4..a3bfa7ea5602 100644 --- a/compiler/src/dotty/tools/dotc/config/Feature.scala +++ b/compiler/src/dotty/tools/dotc/config/Feature.scala @@ -348,6 +348,7 @@ object Feature: true case `magic` => ctx.compilationUnit.magic = true + ctx.compilationUnit.sourceVersion = Some(SourceVersion.future) true case `inlineTraits` => ctx.compilationUnit.knowsInlineTraits = true diff --git a/tests/neg/i1793-maybe.scala b/tests/neg/i1793-maybe.scala index 70b24305d48d..cb4952e2791b 100644 --- a/tests/neg/i1793-maybe.scala +++ b/tests/neg/i1793-maybe.scala @@ -4,6 +4,6 @@ object Test { import scala.ref.WeakReference def unapply[T <: AnyVal](wr: WeakReference[T]): T? = { val x = wr.underlying.get - if (x != null) x else null // error + if x != null then x else null // error } } diff --git a/tests/neg/i2378-maybe.scala b/tests/neg/i2378-maybe.scala index 2271e7afdc98..05d0e90c7d5f 100644 --- a/tests/neg/i2378-maybe.scala +++ b/tests/neg/i2378-maybe.scala @@ -14,13 +14,13 @@ trait Toolbox { trait ApplyImpl { def unapply(tree: Tree): (Tree, Seq[Tree])? - def unapply(tree: tpd.Tree)(implicit c: Cap): (tpd.Tree, Seq[tpd.Tree])? + def unapply(tree: tpd.Tree)(using c: Cap): (tpd.Tree, Seq[tpd.Tree])? } } class Test(val tb: Toolbox) { import tb.* - implicit val cap: Cap = null.asInstanceOf[Cap] + 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 diff --git a/tests/neg/patmat2-maybe.scala b/tests/neg/patmat2-maybe.scala index b93e63a0a3b9..b93d9013be6d 100644 --- a/tests/neg/patmat2-maybe.scala +++ b/tests/neg/patmat2-maybe.scala @@ -6,7 +6,7 @@ import java.lang.IllegalArgumentException object IAE { def unapply(e: Exception): String? = - if (e.isInstanceOf[IllegalArgumentException]) e.getMessage + if e.isInstanceOf[IllegalArgumentException] then e.getMessage else null } diff --git a/tests/neg/refutable-pattern-binding-messages-maybe.check b/tests/neg/refutable-pattern-binding-messages-maybe.check index e734ac88c868..309d29dc2a3b 100644 --- a/tests/neg/refutable-pattern-binding-messages-maybe.check +++ b/tests/neg/refutable-pattern-binding-messages-maybe.check @@ -1,3 +1,11 @@ +-- Error: tests/neg/refutable-pattern-binding-messages-maybe.scala:6:14 ------------------------------------------------ +6 | val Positive(p) = 5 // error: refutable extractor + | ^^^^^^^^^^^^^^^ + | pattern binding uses refutable extractor `Test.Positive` + | + | If this usage is intentional, this can be communicated by adding `.runtimeChecked` after the expression, + | which may result in a MatchError at runtime. + | This patch can be rewritten automatically under -rewrite -source 3.8-migration. -- Error: tests/neg/refutable-pattern-binding-messages-maybe.scala:7:14 ------------------------------------------------ 7 | for Positive(i) <- List(1, 2, 3) do () // error: refutable extractor | ^^^^^^^^^^^ @@ -6,6 +14,14 @@ | If this usage is intentional, this can be communicated by adding the `case` keyword before the full pattern, | which will result in a filtering for expression (using `withFilter`). | This patch can be rewritten automatically under -rewrite -source 3.2-migration. +-- Error: tests/neg/refutable-pattern-binding-messages-maybe.scala:11:20 ----------------------------------------------- +11 | val i :: is = List(1, 2, 3) // error: pattern type more specialized + | ^^^^^^^^^^^^^ + | pattern's type ::[Int] is more specialized than the right hand side expression's type List[Int] + | + | If the narrowing is intentional, this can be communicated by adding `.runtimeChecked` after the expression, + | which may result in a MatchError at runtime. + | This patch can be rewritten automatically under -rewrite -source 3.8-migration. -- Error: tests/neg/refutable-pattern-binding-messages-maybe.scala:12:11 ----------------------------------------------- 12 | for ((x: String) <- xs) do () // error: pattern type more specialized | ^^^^^^ @@ -22,24 +38,8 @@ | 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. --- Warning: tests/neg/refutable-pattern-binding-messages-maybe.scala:6:14 ---------------------------------------------- -6 | val Positive(p) = 5 // warn: 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. --- Warning: tests/neg/refutable-pattern-binding-messages-maybe.scala:11:20 --------------------------------------------- -11 | val i :: is = List(1, 2, 3) // warn: 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. --- Warning: tests/neg/refutable-pattern-binding-messages-maybe.scala:17:10 --------------------------------------------- -17 | val 1 = 2 // warn: pattern type does not match +-- Error: tests/neg/refutable-pattern-binding-messages-maybe.scala:17:10 ----------------------------------------------- +17 | val 1 = 2 // error: pattern type does not match | ^ | pattern's type (1 : Int) does not match the right hand side expression's type (2 : Int) | diff --git a/tests/neg/refutable-pattern-binding-messages-maybe.scala b/tests/neg/refutable-pattern-binding-messages-maybe.scala index deb671a7be07..0269fff29b6e 100644 --- a/tests/neg/refutable-pattern-binding-messages-maybe.scala +++ b/tests/neg/refutable-pattern-binding-messages-maybe.scala @@ -1,18 +1,18 @@ -//> using options -source 3.8 -Yexplicit-nulls +//> using options -Yexplicit-nulls import language.experimental.magic object Test { // refutable extractor object Positive { def unapply(i: Int): Int? = if i > 0 then i else null } - val Positive(p) = 5 // warn: refutable extractor + 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) // warn: pattern type more specialized + 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 // warn: pattern type does not match + val 1 = 2 // error: pattern type does not match } diff --git a/tests/neg/unchecked-patterns-maybe.scala b/tests/neg/unchecked-patterns-maybe.scala index 8107111759b1..ed14f24082a8 100644 --- a/tests/neg/unchecked-patterns-maybe.scala +++ b/tests/neg/unchecked-patterns-maybe.scala @@ -6,24 +6,23 @@ 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) // warn - val (1, c) = (1, 2) // warn - val 1 *: cs = 1 *: Tuple() // warn + val x :: xs = List(1, 2, 3) // error + val (1, c) = (1, 2) // error + val 1 *: cs = 1 *: Tuple() // error - val (_: Int | _: AnyRef) = ??? : AnyRef // warn + val (_: Int | _: AnyRef) = ??? : AnyRef // error - val 1 = 2 // warn + 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 // warn - val Some(s1) = Option(1) // warn + 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 } -// nopos-error: No warnings can be incurred under -Werror (or -Xfatal-warnings) diff --git a/tests/new/test.scala b/tests/new/test.scala index 78703571b033..cd280dd87364 100644 --- a/tests/new/test.scala +++ b/tests/new/test.scala @@ -1,4 +1,5 @@ import language.experimental.magic +import language.future import scala.magic.* import scala.util.Either @@ -11,4 +12,5 @@ object Extract: def unapply[T](x: T): T ? String = Ok(x) @main def Test = 22 match - case Extract(s) => println(s) + case Extract(s) => + if (true) println(s) diff --git a/tests/patmat/optionless-maybe.check b/tests/patmat/optionless-maybe.check index 7b84b1f21e4c..6293a09e2e0d 100644 --- a/tests/patmat/optionless-maybe.check +++ b/tests/patmat/optionless-maybe.check @@ -1,6 +1,6 @@ --- [E029] Pattern Match Exhaustivity Warning: tests/patmat/optionless-maybe.scala:30:44 -30 | def qux(t: Tree)(implicit c: Cap): Unit = t match { - | ^ +-- [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(_) diff --git a/tests/patmat/optionless-maybe.scala b/tests/patmat/optionless-maybe.scala index f86ba1f100ac..29ef2c9de7c7 100644 --- a/tests/patmat/optionless-maybe.scala +++ b/tests/patmat/optionless-maybe.scala @@ -9,11 +9,11 @@ object Ident1 { trait Cap object Ident2 { - def unapply(tree: Tree)(implicit any: Cap): Ident = ??? + def unapply(tree: Tree)(using any: Cap): Ident = ??? } object Ident3 { - def unapply(tree: Tree)(implicit any: Cap): Ident? = ??? + def unapply(tree: Tree)(using any: Cap): Ident? = ??? } @@ -23,11 +23,11 @@ class Test { case Ident1(t) => } - def bar(t: Tree)(implicit c: Cap): Unit = t match { + def bar(t: Tree)(using c: Cap): Unit = t match { case Ident2(t) => } - def qux(t: Tree)(implicit c: Cap): Unit = t match { + def qux(t: Tree)(using c: Cap): Unit = t match { case Ident3(t) => } diff --git a/tests/pos-custom-args/captures/i24729-maybe.scala b/tests/pos-custom-args/captures/i24729-maybe.scala index 99162b59602f..2066a8f62a6a 100644 --- a/tests/pos-custom-args/captures/i24729-maybe.scala +++ b/tests/pos-custom-args/captures/i24729-maybe.scala @@ -15,9 +15,9 @@ extension [A](inline self: A) inline def ---> [B](inline y: B): (A, B) = (self, object +: { def unapply[A, CC[_] <: Seq[?], C <: SeqOps[A, CC, C]](t: (C & SeqOps[A, CC, C])^): (A, C^{t})? = - if(t.isEmpty) null + 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) None + 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 index ad60182f2ce7..7217fe991450 100644 --- a/tests/pos/StringContext-maybe.scala +++ b/tests/pos/StringContext-maybe.scala @@ -223,7 +223,7 @@ object StringContext { // glob-wildcard placeholders val patternLength = { var n = numWildcards - for(chunk <- patternChunks) { + for chunk <- patternChunks do { n += chunk.length } n @@ -235,13 +235,13 @@ object StringContext { val arr = new Array[Short](patternLength) var i = 0 var first = true - for(chunk <- patternChunks) { - if (first) first = false + for chunk <- patternChunks do { + if first then first = false else { arr(i) = -1 i += 1 } - for(c <- chunk) { + for c <- chunk do { arr(i) = c.toShort i += 1 } @@ -256,8 +256,8 @@ object StringContext { val arr = Array.fill(patternLength + 1)(-1) var i = 0 var j = 0 - for(chunk <- patternChunks) { - if (j < numWildcards) { + for chunk <- patternChunks do { + if j < numWildcards then { i += chunk.length arr(i) = j i += 1 @@ -267,7 +267,7 @@ object StringContext { arr } - while(patternIndex < patternLength || inputIndex < nameLength) { + while patternIndex < patternLength || inputIndex < nameLength do { matchIndices(patternIndex) match { case -1 => // do nothing case n => @@ -281,7 +281,7 @@ object StringContext { } } - val continue = if (patternIndex < patternLength) { + val continue = if patternIndex < patternLength then { val c = pattern(patternIndex) c match { case -1 => // zero-or-more-character wildcard @@ -291,7 +291,7 @@ object StringContext { patternIndex += 1 true case _ => // ordinary character - if (inputIndex < nameLength && input(inputIndex) == c) { + if inputIndex < nameLength && input(inputIndex) == c then { patternIndex += 1 inputIndex += 1 true @@ -302,8 +302,8 @@ object StringContext { } else false // Mismatch. Maybe restart. - if (!continue) { - if (0 < nextInputIndex && nextInputIndex <= nameLength) { + if !continue then { + if 0 < nextInputIndex && nextInputIndex <= nameLength then { patternIndex = nextPatternIndex inputIndex = nextInputIndex } else { @@ -327,7 +327,7 @@ object StringContext { s"""invalid escape ${ require(index >= 0 && index < str.length) val ok = s"""[\\b, \\t, \\n, \\f, \\r, \\\\, \\", \\', \\uxxxx]""" - if (index == str.length - 1) "at terminal" else s"'\\${str(index + 1)}' not one of $ok at" + 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 \\.""" ) @@ -335,30 +335,30 @@ object StringContext { s"""invalid unicode escape at index $index of $str""" ) - private[this] def readUEscape(src: String, startindex: Int): (Char, Int) = { + 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) { + if dindex >= 4 then { val usRead = uindex - startindex val digitsRead = dindex (codepoint.asInstanceOf[Char], usRead + digitsRead) } - else if (dindex + uindex >= len) + 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) loopCP(dindex + 1, (codepoint << 4) + e) + if e >= 0 && e <= 15 then loopCP(dindex + 1, (codepoint << 4) + e) else throw new InvalidUnicodeEscapeException(src, startindex, uindex + dindex) } } - if(uindex >= len) throw new InvalidUnicodeEscapeException(src, startindex, uindex - 1) + 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') loop(uindex + 1) + else if src(uindex) == 'u' then loop(uindex + 1) else loopCP(0, 0) } loop(startindex) @@ -384,28 +384,28 @@ object StringContext { * @return The string with all escape sequences expanded. */ def processEscapes(str: String): String = - str indexOf '\\' match { + str.indexOf('\\') match { case -1 => str case i => replace(str, i) } protected[scala] def processUnicode(str: String): String = - str indexOf "\\" match { + 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[this] def replace(str: String, first: Int): String = { + 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) { + if next >= 0 then { //require(str(next) == '\\') - if (next > i) b.append(str, i, next) + if next > i then b.append(str, i, next) var idx = next + 1 - if (idx >= len) throw new InvalidEscapeException(str, next) + if idx >= len then throw new InvalidEscapeException(str, next) val c = str(idx) match { case 'u' => 'u' case 'b' => '\b' @@ -418,13 +418,13 @@ object StringContext { case '\\' => '\\' case _ => throw new InvalidEscapeException(str, next) } - val (ch, advance) = if (c == 'u') readUEscape(str, idx) + val (ch, advance) = if c == 'u' then readUEscape(str, idx) else (c, 1) idx += advance - b append ch + b.append(ch) loop(idx, str.indexOf('\\', idx)) } else { - if (i < len) b.append(str, i, len) + if i < len then b.append(str, i, len) b.toString } } @@ -432,17 +432,17 @@ object StringContext { } //replace escapes with given first escape - private[this] def replaceU(str: String, first: Int): String = { + 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) { + if next >= 0 then { //require(str(next) == '\\') - if (next > i) b.append(str, i, next) + if next > i then b.append(str, i, next) var idx = next + 1 - if (idx >= len) { - if (idx == len) b.append('\\') + if idx >= len then { + if idx == len then b.append('\\') b.toString() } else { @@ -458,7 +458,7 @@ object StringContext { loop(idx, str.indexOf('\\', idx)) } } else { - if (i < len) b.append(str, i, len) + if i < len then b.append(str, i, len) b.toString() } } @@ -470,9 +470,9 @@ object StringContext { val pi = parts.iterator val ai = args.iterator val bldr = new JLSBuilder(process(pi.next())) - while (ai.hasNext) { - bldr append ai.next() - bldr append process(pi.next()) + while ai.hasNext do { + bldr.append(ai.next()) + bldr.append(process(pi.next())) } bldr.toString } @@ -483,7 +483,7 @@ object 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) + 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/extractor-types-maybe.scala b/tests/pos/extractor-types-maybe.scala index aaff8098dbe6..d93f7df7083b 100644 --- a/tests/pos/extractor-types-maybe.scala +++ b/tests/pos/extractor-types-maybe.scala @@ -1,8 +1,8 @@ //> using options -Yexplicit-nulls import language.experimental.magic package p1 { - object Ex { def unapply(p: Any): (_ <: Int)? = null } - object Foo { val Ex(_) = null } + object Ex { def unapply(p: Any): (? <: Int)? = null } + object Foo { val Ex(_) = null.runtimeChecked } } // a.scala:2: error: error during expansion of this match (this is a scalac bug). // The underlying error was: type mismatch; diff --git a/tests/pos/i14896-maybe.scala b/tests/pos/i14896-maybe.scala index 60c35ed70e66..c5ca411bbb2b 100644 --- a/tests/pos/i14896-maybe.scala +++ b/tests/pos/i14896-maybe.scala @@ -1,4 +1,4 @@ //> using options -Yexplicit-nulls import language.experimental.magic -object Ex { def unapply(p: Any): (_ <: Int)? = null } +object Ex { def unapply(p: Any): (? <: Int)? = null } object Foo { val Ex(_) = null: @unchecked } \ No newline at end of file diff --git a/tests/pos/i1793-maybe.scala b/tests/pos/i1793-maybe.scala index 4a266450b534..f55c9cece97c 100644 --- a/tests/pos/i1793-maybe.scala +++ b/tests/pos/i1793-maybe.scala @@ -4,6 +4,6 @@ object Test { import scala.ref.WeakReference def unapply[T <: AnyRef](wr: WeakReference[T]): T? = { val x: T = wr.underlying.get - if (x != null) x else null + if x != null then x else null } } diff --git a/tests/pos/i18175-maybe.scala b/tests/pos/i18175-maybe.scala index 6516c9ae021e..08bda78ac292 100644 --- a/tests/pos/i18175-maybe.scala +++ b/tests/pos/i18175-maybe.scala @@ -4,7 +4,7 @@ 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)(implicit n: Regex.Sanitizer[P]): P? = ??? + def unapply(s: CharSequence)(using n: Regex.Sanitizer[P]): P? = ??? object Regex: def apply[R <: String & Singleton](regex: R): Regex[Compile[R]] = ??? diff --git a/tests/pos/misc-unapply_pos-maybe.scala b/tests/pos/misc-unapply_pos-maybe.scala index 90794e3dcdc1..f9bf55dd093d 100644 --- a/tests/pos/misc-unapply_pos-maybe.scala +++ b/tests/pos/misc-unapply_pos-maybe.scala @@ -13,7 +13,7 @@ object Test { trait Foo { def name: String def unapply(x: String): Unit? = { - if (x == name) () else null + if x == name then () else null } } object Bar extends Foo { def name = "bar" } 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 index daba1e6009f2..a90218b54dca 100644 --- a/tests/run-bootstrapped/parse-dates.scala +++ b/tests/run-bootstrapped/parse-dates.scala @@ -52,3 +52,31 @@ def parseDate4(str: String): Date? = 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 e69e50f7a60b..0bbba019688f 100644 --- a/tests/run-tasty-inspector/stdlibExperimentalDefinitions.scala +++ b/tests/run-tasty-inspector/stdlibExperimentalDefinitions.scala @@ -104,8 +104,6 @@ val experimentalDefinitionInLibrary = Set( // New feature: magic "scala.magic.Ok", "scala.magic.Ok$", - "scala.magic.compiletime", - "scala.magic.compiletime$", "scala.magic.compiletime.Maybe", "scala.magic.compiletime.package$.$spec", "scala.magic.compiletime.package$.$wrappedType", diff --git a/tests/run/fully-abstract-nat-3-maybe.scala b/tests/run/fully-abstract-nat-3-maybe.scala index d328eb883a11..7ed8697191df 100644 --- a/tests/run/fully-abstract-nat-3-maybe.scala +++ b/tests/run/fully-abstract-nat-3-maybe.scala @@ -66,7 +66,7 @@ trait Numbers { def unapply(nat: Nat): Succ? } - implicit def SuccDeco(succ: Succ): SuccAPI + def SuccDeco(succ: Succ): SuccAPI trait SuccAPI { def pred: Nat } @@ -110,7 +110,7 @@ object CaseNums extends Numbers { def safeDiv(a: Nat, b: Succ): (Nat, Nat) = { def sdiv(div: Nat, rem: Nat): (Nat, Nat) = - if (lessOrEq(rem, b)) (div, rem) + if lessOrEq(rem, b) then (div, rem) else sdiv(Succ(div), minus(rem, b)) sdiv(Zero(), a) } @@ -142,13 +142,13 @@ object IntNums extends Numbers { object Succ extends SuccExtractor { def apply(nat: Nat): Int = nat + 1 def unapply(nat: Nat): Int? = - if (nat > 0) nat - 1 else null + if nat > 0 then nat - 1 else null } object SuccRefine extends SuccRefineExtractor { def unapply(nat: Nat): Succ? = - if (nat > 0) nat else null + if nat > 0 then nat else null } def SuccDeco(succ: Succ): SuccAPI = new SuccAPI { diff --git a/tests/run/fully-abstract-nat-maybe.scala b/tests/run/fully-abstract-nat-maybe.scala index 692fd573a4ba..ee76205fe1f5 100644 --- a/tests/run/fully-abstract-nat-maybe.scala +++ b/tests/run/fully-abstract-nat-maybe.scala @@ -2,6 +2,7 @@ import language.experimental.magic import scala.magic.* import scala.reflect.ClassTag +import language.implicitConversions object Test { def main(args: Array[String]): Unit = { @@ -21,7 +22,7 @@ object Test { println() { - import UnboundedIntImplementation.* + import UnboundedIntImplementation.{*, given} val large = (BigInt(1) << 100).asInstanceOf[Succ] large match { case Zero() => println("test fail") @@ -33,7 +34,10 @@ object Test { } def testInterface(numbers: Numbers): Unit = { - import numbers.* + 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) @@ -79,19 +83,20 @@ abstract class Numbers { // case class Succ(pred: Nat) extends Nat type Nat - implicit def natClassTag: ClassTag[Nat] + def natClassTag: ClassTag[Nat] trait AbstractNat { def value: Int def succ: Succ } - implicit def NatDeco(nat: Nat): AbstractNat + def NatDeco(nat: Nat): AbstractNat + given Conversion[Nat, AbstractNat] = NatDeco(_) // --- Zero ---------------------------------------- type Zero <: Nat - implicit def zeroClassTag: ClassTag[Zero] + def zeroClassTag: ClassTag[Zero] val Zero: ZeroExtractor abstract class ZeroExtractor { @@ -103,7 +108,7 @@ abstract class Numbers { type Succ <: Nat - implicit def succClassTag: ClassTag[Succ] + def succClassTag: ClassTag[Succ] val Succ: SuccExtractor abstract class SuccExtractor { @@ -114,7 +119,8 @@ abstract class Numbers { trait AbstractSucc { def pred: Nat } - implicit def SuccDeco(succ: Succ): AbstractSucc + def SuccDeco(succ: Succ): AbstractSucc + given Conversion[Succ, AbstractSucc] = SuccDeco(_) } @@ -130,7 +136,7 @@ object CaseClassImplementation extends Numbers { def natClassTag: ClassTag[Nat] = implicitly - implicit def NatDeco(nat: Nat): AbstractNat = new AbstractNat { + def NatDeco(nat: Nat): AbstractNat = new AbstractNat { def value: Int = nat match { case Succ(n) => 1 + n.value case _ => 0 @@ -175,7 +181,7 @@ object IntImplementation extends Numbers { def natClassTag: ClassTag[Nat] = intClassTag(_ >= 0) - implicit def NatDeco(nat: Nat): AbstractNat = new AbstractNat { + def NatDeco(nat: Nat): AbstractNat = new AbstractNat { def value: Int = nat def succ: Succ = nat + 1 } @@ -207,7 +213,7 @@ object IntImplementation extends Numbers { } private def intClassTag(cond: Int => Boolean): ClassTag[Int] = new ClassTag[Int] { - def runtimeClass: Class[_] = classOf[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 @@ -223,7 +229,7 @@ object UnboundedIntImplementation extends Numbers { type Nat = Any // Int | BigInt def natClassTag: ClassTag[Nat] = new ClassTag[Any] { - def runtimeClass: Class[_] = classOf[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) @@ -231,14 +237,14 @@ object UnboundedIntImplementation extends Numbers { } } - implicit def NatDeco(nat: Nat): AbstractNat = new AbstractNat { + 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) BigInt(nat) + 1 + if nat == Integer.MAX_VALUE then BigInt(nat) + 1 else nat + 1 case nat: BigInt => nat + 1 } @@ -249,8 +255,8 @@ object UnboundedIntImplementation extends Numbers { 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) Some(0) else None + def runtimeClass: Class[?] = classOf[Int] + override def unapply(x: Any): Option[Int] = if x == 0 then Some(0) else None } object Zero extends ZeroExtractor { @@ -263,7 +269,7 @@ object UnboundedIntImplementation extends Numbers { type Succ = Any // Int | BigInt def succClassTag: ClassTag[Succ] = new ClassTag[Any] { - def runtimeClass: Class[_] = classOf[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) @@ -276,12 +282,12 @@ object UnboundedIntImplementation extends Numbers { def unapply(succ: Succ): Nat? = Ok(succ.pred) // succ > 0 checked by class tag before calling the unapply } - implicit def SuccDeco(succ: Succ): AbstractSucc = new AbstractSucc { + 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) n.intValue() + if n.isValidInt then n.intValue() else n } } diff --git a/tests/run/i1773-maybe.scala b/tests/run/i1773-maybe.scala index b0650eb3dca3..7f38c4268113 100644 --- a/tests/run/i1773-maybe.scala +++ b/tests/run/i1773-maybe.scala @@ -1,15 +1,16 @@ //> using options -Yexplicit-nulls import language.experimental.magic object Test { - implicit class Foo(sc: StringContext) { + 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 + 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 index c9eeab18f0a9..c8cdaf93bdc4 100644 --- a/tests/run/i4177-maybe.scala +++ b/tests/run/i4177-maybe.scala @@ -1,7 +1,7 @@ //> using options -Yexplicit-nulls import language.experimental.magic object Test { - private[this] var count = 0 + private var count = 0 def test(x: Int) = { count += 1; true } diff --git a/tests/run/i8577a-maybe.scala b/tests/run/i8577a-maybe.scala index 90baf5490853..9b91f614f3fe 100644 --- a/tests/run/i8577a-maybe.scala +++ b/tests/run/i8577a-maybe.scala @@ -10,6 +10,6 @@ extension (inline ctx: Macro.StrCtx) inline def unapplySeq(inline input: Int): S Seq(input) @main def Test: Unit = - val mac"$x" = 1 + val mac"$x" = 1.runtimeChecked val y: Int = x assert(x == 1) diff --git a/tests/run/string-extractor-maybe.scala b/tests/run/string-extractor-maybe.scala index 50dffb123bc6..84dedd606f52 100644 --- a/tests/run/string-extractor-maybe.scala +++ b/tests/run/string-extractor-maybe.scala @@ -3,8 +3,8 @@ import language.experimental.magic final class StringExtract(val s: String) extends AnyVal { def length = s.length def lengthCompare(n: Int) = s.length compare n - def apply(idx: Int): Char = s charAt idx - def head: Char = s charAt 0 + def 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 @@ -16,8 +16,8 @@ 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 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 @@ -28,12 +28,12 @@ final class ThreeStringExtract(val s: String) extends AnyVal { object Bippy { def unapplySeq(x: Any): StringExtract? = - if ((x == null) || (x == "")) null + 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 + if (x == null) || (x == "") then null else new ThreeStringExtract("" + x).get } diff --git a/tests/run/t6111-maybe.scala b/tests/run/t6111-maybe.scala index 92f521d249b2..e9b0811f7102 100644 --- a/tests/run/t6111-maybe.scala +++ b/tests/run/t6111-maybe.scala @@ -7,14 +7,14 @@ import language.experimental.magic import scala.magic.* object Foo { - def unapply[S, T](scrutinee: S)(implicit evidence: FooHasType[S, T]): T? = scrutinee match { + 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 { - implicit object int extends FooHasType[Int, (Int, Int)] + given int: FooHasType[Int, (Int, Int)] = new FooHasType[Int, (Int, Int)] } // resurrected from neg/t997 diff --git a/tests/run/tryPatternMatch-maybe.scala b/tests/run/tryPatternMatch-maybe.scala index 002bb1f1b5a8..692faa9f25fb 100644 --- a/tests/run/tryPatternMatch-maybe.scala +++ b/tests/run/tryPatternMatch-maybe.scala @@ -7,7 +7,7 @@ import java.util.concurrent.TimeoutException object IAE { def unapply(e: Exception): String? = - if (e.isInstanceOf[IllegalArgumentException] && e.getMessage != null) e.getMessage + if e.isInstanceOf[IllegalArgumentException] && e.getMessage != null then e.getMessage else null } diff --git a/tests/run/type-test-nat-maybe.scala b/tests/run/type-test-nat-maybe.scala index 69e35ae5cd3f..9ee8faa29b23 100644 --- a/tests/run/type-test-nat-maybe.scala +++ b/tests/run/type-test-nat-maybe.scala @@ -38,7 +38,7 @@ trait Peano { protected def typeTestOfZero: TypeTest[Nat, Zero] protected def typeTestOfSucc: TypeTest[Nat, Succ] - implicit def succDeco(succ: Succ): SuccAPI + def succDeco(succ: Succ): SuccAPI trait SuccAPI { def pred: Nat } @@ -111,7 +111,7 @@ object ClassNums extends Peano { case _ => acc } def natValue(x: Int): Nat = - if (x == 0) ZeroObject + if x == 0 then ZeroObject else new SuccClass(natValue(x - 1)) val i = intValue(m, 0) val j = intValue(n, 0) diff --git a/tests/run/unapply-maybe.scala b/tests/run/unapply-maybe.scala index 03107f40dce8..345ffbc514d1 100644 --- a/tests/run/unapply-maybe.scala +++ b/tests/run/unapply-maybe.scala @@ -21,7 +21,7 @@ object Fii { def unapply(x: Any): Boolean = x.isInstanceOf[Bar] } object Faa { - def unapply(x: Any): String? = if(x.isInstanceOf[Bar]) x.asInstanceOf[Bar].name else null + 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 @@ -30,7 +30,7 @@ object FaaPreciseSome { def unapply(x: Bar) = Some(x.name) // return type Some[String] } object VarFoo { - def unapply(a : Int)(implicit b : Int) : Int? = a + b + def unapply(a : Int)(using b : Int) : Int? = a + b } object Foo { @@ -60,7 +60,7 @@ object Foo { assert(doMatch3(b) == "medium") assert(doMatch4(b) == "medium") assert(doMatch5(b) == "medium") - implicit val bc: Int = 3 + given bc: Int = 3 assert(7 == (4 match { case VarFoo(x) => x })) diff --git a/tests/run/virtpatmat_unapply-maybe.scala b/tests/run/virtpatmat_unapply-maybe.scala index 147a457e9cc8..0350506ce971 100644 --- a/tests/run/virtpatmat_unapply-maybe.scala +++ b/tests/run/virtpatmat_unapply-maybe.scala @@ -3,7 +3,7 @@ import language.experimental.magic class IntList(val hd: Int, val tl: IntList) object NilIL extends IntList(0, null.asInstanceOf[IntList]) object IntList { - def unapply(il: IntList): (Int, IntList)? = if(il eq NilIL) null else (il.hd, il.tl) + 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) } diff --git a/tests/warn/maybe-typetest.check b/tests/warn/maybe-typetest.check index 341dfaa39b26..9a94900e586d 100644 --- a/tests/warn/maybe-typetest.check +++ b/tests/warn/maybe-typetest.check @@ -1,3 +1,17 @@ +-- [E165] Type Warning: tests/warn/maybe-typetest.scala:7:12 ----------------------------------------------------------- +7 | case y: String? => println(y) // warn typetest // warn unmatchable + | ^^^^^^^ + | pattern selector should be an instance of Matchable, + | but it has unmatchable type T instead + | + | longer explanation available when compiling with `-explain` +-- [E165] Type Warning: tests/warn/maybe-typetest.scala:13:12 ---------------------------------------------------------- +13 | case y: Option[String] => println(y)// warn typetest // warn unmatchable + | ^^^^^^^^^^^^^^ + | pattern selector should be an instance of Matchable, + | but it has unmatchable type T instead + | + | longer explanation available when compiling with `-explain` -- [E030] Match case Unreachable Warning: tests/warn/maybe-typetest.scala:11:9 ----------------------------------------- 11 | case _ => // warn unreachable | ^ @@ -7,7 +21,7 @@ | ^ | Unreachable case -- [E092] Pattern Match Unchecked Warning: tests/warn/maybe-typetest.scala:7:9 ----------------------------------------- -7 | case y: String? => println(y) // warn typetest +7 | case y: String? => println(y) // warn typetest // warn unmatchable | ^ | the type test for String? cannot be checked at runtime because its type arguments can't be determined from T | @@ -19,7 +33,7 @@ | | longer explanation available when compiling with `-explain` -- [E092] Pattern Match Unchecked Warning: tests/warn/maybe-typetest.scala:13:9 ---------------------------------------- -13 | case y: Option[String] => println(y)// warn typetest +13 | case y: Option[String] => println(y)// warn typetest // warn unmatchable | ^ |the type test for Option[String] cannot be checked at runtime because its type arguments can't be determined from T | diff --git a/tests/warn/maybe-typetest.scala b/tests/warn/maybe-typetest.scala index 5a8013a6b121..b0cca5901cd5 100644 --- a/tests/warn/maybe-typetest.scala +++ b/tests/warn/maybe-typetest.scala @@ -4,13 +4,13 @@ import scala.magic.* def Test[T](x: T) = x match - case y: String? => println(y) // warn typetest + 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 + case y: Option[String] => println(y)// warn typetest // warn unmatchable case _ => Some(x) match case y: Option[String] => println(y)// warn typetest diff --git a/tests/warn/strict-pattern-bindings-3.2-maybe.scala b/tests/warn/strict-pattern-bindings-3.2-maybe.scala deleted file mode 100644 index b5c36f0364a5..000000000000 --- a/tests/warn/strict-pattern-bindings-3.2-maybe.scala +++ /dev/null @@ -1,39 +0,0 @@ -//> using options -Yexplicit-nulls - -// These tests should fail under -Werror with source version source version 3.2 or later -import language.experimental.magic -import language.`3.2` - -object Test: - // from filtering-fors.scala - val xs: List[AnyRef] = ??? - - for ((x: String) <- xs) do () // warn - for (y@ (x: String) <- xs) do () // warn - for ((x, y) <- xs) do () // warn - - for ((x: String) <- xs if x.isEmpty) do () // warn - for ((x: String) <- xs; y = x) do () // warn - for ((x: String) <- xs; (y, z) <- xs) do () // warn // warn - for (case (x: String) <- xs; (y, z) <- xs) do () // warn - for ((x: String) <- xs; case (y, z) <- xs) do () // warn - - val pairs: List[AnyRef] = List((1, 2), "hello", (3, 4)) - for ((x, y) <- pairs) yield (y, x) // warn - - // from unchecked-patterns.scala - val y :: ys = List(1, 2, 3) // warn - val (1, c) = (1, 2) // warn - val 1 *: cs = 1 *: Tuple() // warn - - val (_: Int | _: AnyRef) = ??? : AnyRef // warn - - val 1 = 2 // warn - - 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 // warn - val Some(s1) = Option(1) // warn From 77cd4c57e7a72cf1d38712bb943245800278cdd5 Mon Sep 17 00:00:00 2001 From: odersky Date: Fri, 21 Aug 2026 17:14:33 +0200 Subject: [PATCH 25/28] Allow if without then in maybe blocks --- .../dotty/tools/dotc/core/Definitions.scala | 6 +- .../dotty/tools/dotc/parsing/Parsers.scala | 40 +++++--- .../src/dotty/tools/dotc/typer/Typer.scala | 96 +++++++++++-------- docs/_docs/internals/syntax.md | 1 + tests/neg/if-without-then.check | 12 +++ tests/neg/if-without-then.scala | 12 +++ tests/run-bootstrapped/parse-dates.scala | 13 +-- 7 files changed, 119 insertions(+), 61 deletions(-) create mode 100644 tests/neg/if-without-then.check create mode 100644 tests/neg/if-without-then.scala diff --git a/compiler/src/dotty/tools/dotc/core/Definitions.scala b/compiler/src/dotty/tools/dotc/core/Definitions.scala index 307ff6b81df2..48db2614a692 100644 --- a/compiler/src/dotty/tools/dotc/core/Definitions.scala +++ b/compiler/src/dotty/tools/dotc/core/Definitions.scala @@ -482,10 +482,14 @@ class Definitions { def AnyKindType: TypeRef = AnyKindClass.typeRef // Magic stuff - @tu lazy val MagicPackageClass: ClassSymbol = requiredPackage("scala.magic").moduleClass.asClass + @tu lazy val MagicPackage: Symbol = requiredPackage("scala.magic") + @tu lazy val MagicPackageClass: ClassSymbol = MagicPackage.moduleClass.asClass @tu lazy val MagicMaybeClass: ClassSymbol = requiredClass("scala.magic.compiletime.Maybe") @tu lazy val MagicValidClass: ClassSymbol = requiredClass("scala.magic.runtime.Valid") @tu lazy val MagicFailClass: ClassSymbol = requiredClass("scala.magic.runtime.Fail") + @tu lazy val Magic_provided1: Symbol = MagicPackage.info.member(termName("provided")).suchThat(_.info.isInstanceOf[MethodType]).symbol + @tu lazy val Magic_provided2: Symbol = MagicPackage.info.member(termName("provided")).suchThat(_.info.isInstanceOf[PolyType]).symbol + @tu lazy val Magic_CanErr: Symbol = MagicPackageClass.requiredType("CanErr") @tu lazy val MagicOkModule: Symbol = requiredModule("scala.magic.Ok") @tu lazy val Magic_OkUnapply: Symbol = MagicOkModule.requiredMethod(nme.unapply) diff --git a/compiler/src/dotty/tools/dotc/parsing/Parsers.scala b/compiler/src/dotty/tools/dotc/parsing/Parsers.scala index 4ecb57753cab..0533d8f2c7f4 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.magicEnabled && 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; @@ -5250,6 +5267,7 @@ object Parsers { * | Annotations LocalModifiers TmplDef * | Extension * | Expr1 + * | ‘if’ Expr [‘else’ Expr] * | */ def blockStatSeq(outermost: Boolean = false): List[Tree] = checkNoEscapingPlaceholders { diff --git a/compiler/src/dotty/tools/dotc/typer/Typer.scala b/compiler/src/dotty/tools/dotc/typer/Typer.scala index 5252d737a176..ae10464f9e00 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.magicEnabled) + val elsep1 = + if tree.elsep.isEmpty then tpd.unitLiteral else typed(tree.elsep) + val labelType = defn.Magic_CanErr.typeRef.appliedTo(elsep1.tpe.widen) + inferImplicit(labelType, EmptyTree, tree.span) match + case fail: SearchFailure if !fail.isAmbiguous => + errorTree(tree, em"`if` without `then` is illegal here since no given of type $labelType is available.") + case _ => + val desugared = + if tree.elsep.isEmpty + then cpy.Apply(tree)(untpd.ref(defn.Magic_provided1), tree.cond :: Nil) + else cpy.Apply(tree)(untpd.ref(defn.Magic_provided2), tree.cond :: untpd.TypedSplice(elsep1) :: Nil) + typedApply(desugared, pt) + else + val cond1 = typed(tree.cond, defn.BooleanType) - def isIncomplete(tree: untpd.If): Boolean = tree.elsep match - case EmptyTree => true - case elsep: untpd.If => isIncomplete(elsep) - case _ => false + def isIncomplete(tree: untpd.If): Boolean = tree.elsep match + case EmptyTree => true + case elsep: untpd.If => isIncomplete(elsep) + case _ => false - // Insert a GADT cast if the type of the branch does not conform - // to the type assigned to the whole if tree. - // This happens when the computation of the type of the if tree - // uses GADT constraints. See #15646. - def gadtAdaptBranch(tree: Tree, branchPt: Type): Tree = - TypeComparer.testSubType(tree.tpe.widenExpr, branchPt) match { - case CompareResult.OKwithGADTUsed => - insertGadtCast(tree, tree.tpe.widen, branchPt) - case _ => tree - } + // Insert a GADT cast if the type of the branch does not conform + // to the type assigned to the whole if tree. + // This happens when the computation of the type of the if tree + // uses GADT constraints. See #15646. + def gadtAdaptBranch(tree: Tree, branchPt: Type): Tree = + TypeComparer.testSubType(tree.tpe.widenExpr, branchPt) match { + case CompareResult.OKwithGADTUsed => + insertGadtCast(tree, tree.tpe.widen, branchPt) + case _ => tree + } - val branchPt = if isIncomplete(tree) then defn.UnitType else pt.dropIfProto + val branchPt = if isIncomplete(tree) then defn.UnitType else pt.dropIfProto - val result = - if tree.elsep.isEmpty then - val thenp1 = typed(tree.thenp, branchPt)(using cond1.nullableContextIf(true)) - val elsep1 = tpd.unitLiteral.withSpan(tree.span.endPos) - cpy.If(tree)(cond1, thenp1, elsep1).withType(defn.UnitType) - else - val thenp1 :: elsep1 :: Nil = harmonic(harmonize, pt) { - val thenp0 = typed(tree.thenp, branchPt)(using cond1.nullableContextIf(true)) - val elsep0 = typed(tree.elsep, branchPt)(using cond1.nullableContextIf(false)) - thenp0 :: elsep0 :: Nil - }: @unchecked - - val resType = thenp1.tpe | elsep1.tpe - val thenp2 :: elsep2 :: Nil = - (thenp1 :: elsep1 :: Nil) map { t => - // Adapt each branch to ensure that their types conforms to the - // type assigned to the if tree by inserting GADT casts. - gadtAdaptBranch(t, resType) + val result = + if tree.elsep.isEmpty then + val thenp1 = typed(tree.thenp, branchPt)(using cond1.nullableContextIf(true)) + val elsep1 = tpd.unitLiteral.withSpan(tree.span.endPos) + cpy.If(tree)(cond1, thenp1, elsep1).withType(defn.UnitType) + else + val thenp1 :: elsep1 :: Nil = harmonic(harmonize, pt) { + val thenp0 = typed(tree.thenp, branchPt)(using cond1.nullableContextIf(true)) + val elsep0 = typed(tree.elsep, branchPt)(using cond1.nullableContextIf(false)) + thenp0 :: elsep0 :: Nil }: @unchecked - cpy.If(tree)(cond1, thenp2, elsep2).withType(resType) + val resType = thenp1.tpe | elsep1.tpe + val thenp2 :: elsep2 :: Nil = + (thenp1 :: elsep1 :: Nil) map { t => + // Adapt each branch to ensure that their types conforms to the + // type assigned to the if tree by inserting GADT casts. + gadtAdaptBranch(t, resType) + }: @unchecked - def thenPathInfo = cond1.notNullInfoIf(true).seq(result.thenp.notNullInfo) - def elsePathInfo = cond1.notNullInfoIf(false).seq(result.elsep.notNullInfo) - result.withNotNullInfo(thenPathInfo.alt(elsePathInfo)) - end typedIf + cpy.If(tree)(cond1, thenp2, elsep2).withType(resType) + + def thenPathInfo = cond1.notNullInfoIf(true).seq(result.thenp.notNullInfo) + def elsePathInfo = cond1.notNullInfoIf(false).seq(result.elsep.notNullInfo) + result.withNotNullInfo(thenPathInfo.alt(elsePathInfo)) + } /** Decompose function prototype into a list of parameter prototypes and a result * prototype tree, using WildcardTypes where a type is not known. 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/if-without-then.check b/tests/neg/if-without-then.check new file mode 100644 index 000000000000..bb1ed6010e94 --- /dev/null +++ b/tests/neg/if-without-then.check @@ -0,0 +1,12 @@ +-- Error: tests/neg/if-without-then.scala:5:2 -------------------------------------------------------------------------- +5 | if (true) println("yes") // error + | ^^^^^^^^^^^^^^^^^^^^^^^^ + | `if` without `then` is illegal here since no given of type scala.magic.CanErr[Unit] is available. +-- Error: tests/neg/if-without-then.scala:7:2 -------------------------------------------------------------------------- +7 | if (true) // error + | ^^^^^^^^^ + | `if` without `then` is illegal here since no given of type scala.magic.CanErr[Unit] is available. +-- Error: tests/neg/if-without-then.scala:10:2 ------------------------------------------------------------------------- +10 | if true else "error" // error + | ^^^^^^^^^^^^^^^^^^^^ + | `if` without `then` is illegal here since no given of type scala.magic.CanErr[String] is available. diff --git a/tests/neg/if-without-then.scala b/tests/neg/if-without-then.scala new file mode 100644 index 000000000000..0f331db016eb --- /dev/null +++ b/tests/neg/if-without-then.scala @@ -0,0 +1,12 @@ +//> using options -Yexplicit-nulls +import language.experimental.magic + +def foo = + if (true) println("yes") // error + + if (true) // error + println("yes") + + if true else "error" // error + + diff --git a/tests/run-bootstrapped/parse-dates.scala b/tests/run-bootstrapped/parse-dates.scala index a90218b54dca..29e4b5bd2cc1 100644 --- a/tests/run-bootstrapped/parse-dates.scala +++ b/tests/run-bootstrapped/parse-dates.scala @@ -2,7 +2,7 @@ import language.experimental.magic import scala.magic.* -extension (str: String) def parseInt: Int ? Unit = +extension (str: String) def parseInt: Int? = try str.toInt catch case ex: NumberFormatException => null @@ -34,8 +34,8 @@ def parseDate3(str: String): Date ? String = 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")? - provided(1 <= day && day <= 31, s"day $day outside allowed range 1..31") - provided(1 <= month && month <= 12, s"month $month outside allowed range 1..12") + 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") @@ -47,8 +47,8 @@ def parseDate4(str: String): Date? = val day = d.parseInt? val month = m.parseInt? val year = y.parseInt? - provided(1 <= day && day <= 31) - provided(1 <= month && month <= 12) + if 1 <= day && day <= 31 + if 1 <= month && month <= 12 Date(day, month, year) case _ => null @@ -77,6 +77,3 @@ def parseDate4(str: String): Date? = println(parseDate2("1/13/2000")) println(parseDate3("1/13/2000")) println(parseDate4("1/13/2000")) - - - From 2415280c60cbdf3cd6fb358d194b26e3e900deba Mon Sep 17 00:00:00 2001 From: odersky Date: Sun, 23 Aug 2026 16:56:24 +0200 Subject: [PATCH 26/28] Disallow maybes with wildcard arguments --- .../src/dotty/tools/dotc/typer/Checking.scala | 4 ++- tests/{pos => neg}/i14896-maybe.scala | 2 +- tests/neg/t8128-maybe.check | 4 +++ tests/neg/t8128-maybe.scala | 8 +++++ tests/pos/extractor-types-maybe.scala | 32 ------------------- tests/pos/t8128-maybe.scala | 2 +- 6 files changed, 17 insertions(+), 35 deletions(-) rename tests/{pos => neg}/i14896-maybe.scala (63%) create mode 100644 tests/neg/t8128-maybe.check create mode 100644 tests/neg/t8128-maybe.scala delete mode 100644 tests/pos/extractor-types-maybe.scala diff --git a/compiler/src/dotty/tools/dotc/typer/Checking.scala b/compiler/src/dotty/tools/dotc/typer/Checking.scala index 52798352e040..ce80d0eaaf50 100644 --- a/compiler/src/dotty/tools/dotc/typer/Checking.scala +++ b/compiler/src/dotty/tools/dotc/typer/Checking.scala @@ -129,11 +129,13 @@ object Checking { checkBounds(args, bounds, instantiate, tree.tpe, tpt) def checkWildcardApply(tp: Type): Unit = tp match { - case tp @ AppliedType(tycon, _) => + case tp @ AppliedType(tycon, _) if tp.hasWildcardArg => if tp.isUnreducibleWild then report.errorOrMigrationWarning( showInferred(UnreducibleApplication(tycon), tp, tpt), tree.srcPos, MigrationVersion.Scala2to3) + else if tp.typeSymbol == defn.MagicMaybeClass then + report.error(em"Maybe type may not contain wildcard arguments", tree.srcPos) case _ => } def checkValidIfApply(using Context): Unit = diff --git a/tests/pos/i14896-maybe.scala b/tests/neg/i14896-maybe.scala similarity index 63% rename from tests/pos/i14896-maybe.scala rename to tests/neg/i14896-maybe.scala index c5ca411bbb2b..c881fc970f93 100644 --- a/tests/pos/i14896-maybe.scala +++ b/tests/neg/i14896-maybe.scala @@ -1,4 +1,4 @@ //> using options -Yexplicit-nulls import language.experimental.magic -object Ex { def unapply(p: Any): (? <: Int)? = null } +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/t8128-maybe.check b/tests/neg/t8128-maybe.check new file mode 100644 index 000000000000..1c2467c64dec --- /dev/null +++ b/tests/neg/t8128-maybe.check @@ -0,0 +1,4 @@ +-- Error: tests/neg/t8128-maybe.scala:6:23 ----------------------------------------------------------------------------- +6 | def unapply(m: Any): Maybe[?, Unit] = Ok("") // error + | ^^^^^^^^^^^^^^ + | Maybe type may not contain wildcard arguments diff --git a/tests/neg/t8128-maybe.scala b/tests/neg/t8128-maybe.scala new file mode 100644 index 000000000000..6340658af1ce --- /dev/null +++ b/tests/neg/t8128-maybe.scala @@ -0,0 +1,8 @@ +//> using options -Yexplicit-nulls +import language.experimental.magic +import scala.magic.* +import compiletime.Maybe +object G { + def unapply(m: Any): Maybe[?, Unit] = Ok("") // error +} + diff --git a/tests/pos/extractor-types-maybe.scala b/tests/pos/extractor-types-maybe.scala deleted file mode 100644 index d93f7df7083b..000000000000 --- a/tests/pos/extractor-types-maybe.scala +++ /dev/null @@ -1,32 +0,0 @@ -//> using options -Yexplicit-nulls -import language.experimental.magic -package p1 { - object Ex { def unapply(p: Any): (? <: Int)? = null } - object Foo { val Ex(_) = null.runtimeChecked } -} -// a.scala:2: error: error during expansion of this match (this is a scalac bug). -// The underlying error was: type mismatch; -// found : Some[_$1(in value x$1)] where type _$1(in value x$1) -// required: Some[_$1(in method unapply)] -// object Foo { val Ex(_) = null } -// ^ -// one error found - -package p2 { - trait Other { - class Quux - object Baz { def unapply(x: Any): Quux? = null } - } - trait Reifiers { - def f(): Unit = { - val u2: Other = ??? - (null: Any) match { case u2.Baz(x) => println(x) } //: u2.Quux) } - // The underlying error was: type mismatch; - // found : Other#Quux - // required: u2.Quux - // x match { case u2.Baz(x) => println(x: u2.Quux) } - // ^ - // one error found - } - } -} diff --git a/tests/pos/t8128-maybe.scala b/tests/pos/t8128-maybe.scala index 34e4d66e5b72..888390e8a1f2 100644 --- a/tests/pos/t8128-maybe.scala +++ b/tests/pos/t8128-maybe.scala @@ -3,7 +3,7 @@ import language.experimental.magic import scala.magic.* import compiletime.Maybe object G { - def unapply(m: Any): Maybe[?, Unit] = Ok("") + def unapply(m: Any): Maybe[Any, Unit] = Ok("") } object H { From cd3d5d33e70d12e91a68bcb527e3f553e826122d Mon Sep 17 00:00:00 2001 From: odersky Date: Sun, 23 Aug 2026 16:58:07 +0200 Subject: [PATCH 27/28] Optimizations of code patterns around maybes --- compiler/src/dotty/tools/dotc/ast/tpd.scala | 6 ++ .../dotty/tools/dotc/core/Definitions.scala | 3 + .../dotty/tools/dotc/inlines/Inliner.scala | 30 +++++- .../dotty/tools/dotc/inlines/Inlines.scala | 88 +++++++++-------- .../tools/dotc/transform/BetaReduce.scala | 11 ++- .../tools/dotc/transform/PatternMatcher.scala | 21 ++-- .../scala/magic/package.scala | 4 +- library/src/scala/magic/Err.scala | 5 +- tests/pos/maybe-translation.scala | 97 +++++++++++++++++++ 9 files changed, 207 insertions(+), 58 deletions(-) create mode 100644 tests/pos/maybe-translation.scala diff --git a/compiler/src/dotty/tools/dotc/ast/tpd.scala b/compiler/src/dotty/tools/dotc/ast/tpd.scala index 6ad5f1c61b34..596e244b4a07 100644 --- a/compiler/src/dotty/tools/dotc/ast/tpd.scala +++ b/compiler/src/dotty/tools/dotc/ast/tpd.scala @@ -98,6 +98,12 @@ object tpd extends Trees.Instance[Type] with TypedTreeInfo { def If(cond: Tree, thenp: Tree, elsep: Tree)(using Context): If = ta.assignType(untpd.If(cond, thenp, elsep), thenp, elsep) + def conditional(cond: Tree, thenp: Tree, elsep: Tree)(using Context): Tree = + cond match + case Literal(Constant(true)) => thenp + case Literal(Constant(false)) => elsep + case _ => If(cond, thenp, elsep) + def InlineIf(cond: Tree, thenp: Tree, elsep: Tree)(using Context): If = ta.assignType(untpd.InlineIf(cond, thenp, elsep), thenp, elsep) diff --git a/compiler/src/dotty/tools/dotc/core/Definitions.scala b/compiler/src/dotty/tools/dotc/core/Definitions.scala index 48db2614a692..469d5ad95c65 100644 --- a/compiler/src/dotty/tools/dotc/core/Definitions.scala +++ b/compiler/src/dotty/tools/dotc/core/Definitions.scala @@ -492,6 +492,7 @@ class Definitions { @tu lazy val Magic_CanErr: Symbol = MagicPackageClass.requiredType("CanErr") @tu lazy val MagicOkModule: Symbol = requiredModule("scala.magic.Ok") + @tu lazy val Magic_OkApply: Symbol = MagicOkModule.requiredMethod(nme.apply) @tu lazy val Magic_OkUnapply: Symbol = MagicOkModule.requiredMethod(nme.unapply) @tu lazy val MagicErrModule: Symbol = requiredModule("scala.magic.Err") @@ -501,6 +502,8 @@ class Definitions { @tu lazy val Magic_spec: Symbol = MagicCompiletimePackage.requiredMethod("$spec") @tu lazy val Magic_wrappedType: Symbol = MagicCompiletimePackage.requiredMethod("$wrappedType") + @tu lazy val MagicRuntimePackageClass = requiredPackage("scala.magic.runtime").moduleClass.asClass + // More synthetic symbols @tu lazy val andType: TypeSymbol = enterBinaryAlias(tpnme.AND, AndType(_, _)) @tu lazy val orType: TypeSymbol = enterBinaryAlias(tpnme.OR, OrType(_, _, soft = false)) diff --git a/compiler/src/dotty/tools/dotc/inlines/Inliner.scala b/compiler/src/dotty/tools/dotc/inlines/Inliner.scala index 008254c01432..e2011db1a1b9 100644 --- a/compiler/src/dotty/tools/dotc/inlines/Inliner.scala +++ b/compiler/src/dotty/tools/dotc/inlines/Inliner.scala @@ -15,6 +15,7 @@ import config.Printers.inlining import ErrorReporting.errorTree import util.{SimpleIdentitySet, SrcPos} import Nullables.computeNullableDeeply +import config.Printers.transforms import collection.mutable import reporting.trace @@ -210,6 +211,33 @@ object Inliner: else constToLiteral(rootTree) + /** If tree is an equality test == with known outcome and no side effects, replace it + * by a constant true or false. + * Known outcome means currently: + * - arguments are both constants, or + * - at least one argument is of Unit type + */ + def reduceEQ(tree: Tree)(using Context): Tree = tree match + case Apply(sel @ Select(arg1, nme.EQ), arg2 :: Nil) if isPureExpr(arg1) && isPureExpr(arg2) => + val tp1 = arg1.tpe.widen + val tp2 = arg2.tpe.widen + def const(b: Boolean) = + cpy.Literal(tree)(Constant(b)) + .showing(i"REDUCE $tree to $result in ${ctx.compilationUnit} in ${ctx.owner.ownersIterator.toList}/${arg1.tpe},${arg2.tpe}", transforms) + def reduceUnit(tp1: Type, tp2: Type) = + if tp1.isRef(defn.UnitClass) then + if tp2.isRef(defn.UnitClass) then const(true) + else if !tp2.isBottomType && !tp2.isTopType then const(false) + else EmptyTree + else EmptyTree + (tp1, tp2) match + case (ConstantType(c1), ConstantType(c2)) => + if c1 == c2 then const(true) else const(false) + case _ => + reduceUnit(tp1, tp2).orElse(reduceUnit(tp2, tp1)).orElse(tree) + case _ => + tree + private[inlines] def newSym(name: Name, flags: FlagSet, info: Type, span: Span)(using Context): Symbol = newSymbol(ctx.owner, name, flags, info, coord = span) end Inliner @@ -806,7 +834,7 @@ class Inliner(val call: tpd.Tree)(using Context): // corresponding arguments or proxies on the type and term level. It also changes // the owner from the inlined method to the current owner. - // This is reused through InlineTraitAncestors for inline traits, so inlinedMethod might not exist there + // This is reused through InlineTraitAncestors for inline traits, so inlinedMethod might not exist there val oldOwners = if (inlinedMethod.exists) then inlinedMethod :: Nil else Nil val newOwners = if (inlinedMethod.exists) then ctx.owner :: Nil else Nil diff --git a/compiler/src/dotty/tools/dotc/inlines/Inlines.scala b/compiler/src/dotty/tools/dotc/inlines/Inlines.scala index 7007ccb4893f..87a5e2a640d1 100644 --- a/compiler/src/dotty/tools/dotc/inlines/Inlines.scala +++ b/compiler/src/dotty/tools/dotc/inlines/Inlines.scala @@ -129,15 +129,15 @@ object Inlines: private def inlineTraitAncestors(cls: TypeDef)(using Context): List[Tree] = cls match { case tpd.TypeDef(_, tmpl: Template) => val parentTrees: Map[Symbol, Tree] = tmpl.parents.map(par => symbolFromParent(par) -> par).toMap.filter(_._1.isInlineTrait) - + // TODO: We need to stop inlining if there is a non-inline trait or class that sits between the inline trait and the current class. - // Because we also inline into other inline traits, it should be possible to do this by just + // Because we also inline into other inline traits, it should be possible to do this by just // looking at the direct parents of the class instead of also needing to look at the indirect parents (baseClasses). // See inline-trait-non-inline-blocks-inlining.scala val ancestors: List[ClassSymbol] = cls.tpe.baseClasses.filter(sym => sym != cls.symbol && sym.isInlineTrait && !(cls.symbol.asClass.ownersIterator.toList.tail.exists(p => p.isInlineTrait)) // We can skip anything that would be inlined into a class that lives somewhere inside an inline trait - // because it must be on the RHS of a member definition in the inline trait and so pruned out later + // because it must be on the RHS of a member definition in the inline trait and so pruned out later ) ancestors.flatMap(ancestor => @@ -151,7 +151,7 @@ object Inlines: report.error(s"unknown base type ${baseTpe.show} for ancestor ${ancestor.show} of ${cls.symbol.show}") None parentTrees.get(ancestor).orElse(baseTree.map(_.withSpan(cls.span))) - ).flatMap { tree => + ).flatMap { tree => tree.tpe match { case Specialization(spec) if spec.hasSpecializedParams && !spec.isFullySpecialized => None // these can only exist in cases where we don't want to inline because: // 1) they will be pruned out later anyway and if we inline them we will create a loop (as in tests/pos/specialized-trait-inlining-causes-implementation-required-loop-bad.scala) @@ -285,7 +285,7 @@ object Inlines: tree3 end inlineCall - private def updateFlagsFromInlinedParent(child: FlagSet, parent: FlagSet): FlagSet = + private def updateFlagsFromInlinedParent(child: FlagSet, parent: FlagSet): FlagSet = var updatedFlags = child // Parent needs to be initialised so child must also as initialisers have been inlined if (!parent.is(NoInits)) @@ -296,8 +296,8 @@ object Inlines: updatedFlags &~= PureInterface updatedFlags - private def checkInnerClasses(tmpl: Template)(using Context) = - tmpl.body.foreach { + private def checkInnerClasses(tmpl: Template)(using Context) = + tmpl.body.foreach { // If we want to add these back, some work was done on this in the original Master Thesis // (https://infoscience.epfl.ch/server/api/core/bitstreams/9413f583-46bc-4106-b994-0be32f20eeba/content) case innerClass: TypeDef if innerClass.symbol.isClass => report.error("Inline traits may not define inner classes or traits.", innerClass.srcPos) @@ -319,13 +319,13 @@ object Inlines: end checkAndTransformInlineTrait - private def checkInlineTraitOverrides(clsSym: ClassSymbol)(using Context) = - // We need to enforce `override` modifier constraints to ensure that the behaviour is the same as ordinary traits. + private def checkInlineTraitOverrides(clsSym: ClassSymbol)(using Context) = + // We need to enforce `override` modifier constraints to ensure that the behaviour is the same as ordinary traits. // The usual checks only apply in refChecks which is too late for us. def checkInlineTraitOverride(member: Symbol, other: Symbol) = if !member.is(Override) && !other.is(Deferred) && member.owner == clsSym then report.error( - OverrideError("needs `override` modifier", + OverrideError("needs `override` modifier", other.info, member, other, @@ -335,11 +335,11 @@ object Inlines: ) else if member.owner != clsSym && other.owner != clsSym && !other.owner.derivesFrom(member.owner) - && !(member.isAnyOverride || member.hasAnnotation(defn.UncheckedOverrideAnnot)) - && (!other.is(Deferred) || other.isAllOf(Given | HasDefault)) - && !member.is(Deferred) + && !(member.isAnyOverride || member.hasAnnotation(defn.UncheckedOverrideAnnot)) + && (!other.is(Deferred) || other.isAllOf(Given | HasDefault)) + && !member.is(Deferred) && !other.name.is(DefaultGetterName) then - + report.error( OverrideError( s"${clsSym} inherits conflicting members:\n " @@ -354,7 +354,7 @@ object Inlines: , clsSym.srcPos ) - OverridingPairsChecker(clsSym, clsSym.thisType).checkAll(checkInlineTraitOverride) + OverridingPairsChecker(clsSym, clsSym.thisType).checkAll(checkInlineTraitOverride) def inlineParentInlineTraits(cls: Tree)(using Context): Tree = cls match { @@ -366,31 +366,31 @@ object Inlines: if cls.symbol.isAnonymousClass && ancestors.exists(tree => Specialization.unapply(tree.tpe).exists(anc => anc.isSpecialized || anc.isFullySpecializedToTopClassesOrNothing)) then // No need to inline into specialized trait anonymous class instances; these will later be replaced by $impl$ classes. return cls - + val cycleFound = ancestors.exists { parent => val parentSym = symbolFromParent(parent) - val errorPos = + val errorPos = // Trying to inline into the tree which defines parentSym (need to catch this separately - // as need to catch it before we inline the second time to avoid tripping an assertion) + // as need to catch it before we inline the second time to avoid tripping an assertion) if cls.symbol.ownersIterator.contains(parentSym) then - Some(cls.srcPos) - else if ctx.inlineTraitState.inlineOrigins(cls.symbol).contains(parentSym) then + Some(cls.srcPos) + else if ctx.inlineTraitState.inlineOrigins(cls.symbol).contains(parentSym) then // Select the user code that caused this error so we get two errors if there are two problematic inlines, not one - val userPos = tpd.enclosingInlineds.last.srcPos + val userPos = tpd.enclosingInlineds.last.srcPos // Trying to inline into the inlined body of parentSym not in the defn tree - Some(userPos) + Some(userPos) else None // Fine - + errorPos.foreach(pos => report.error(s"Inlining of inline traits looped. Tried to inline ${parentSym} into its own body.", pos) ) - + errorPos.nonEmpty } - if cycleFound then + if cycleFound then return cls - + val newDefs = inContext(ctx.withOwner(cls.symbol)) { ancestors.foldLeft((List.empty[Tree], impl.body)) { case ((inlineDefs, childDefs), parent) => @@ -399,10 +399,10 @@ object Inlines: val overriddenSymbols = clsOverriddenSyms ++ inlineDefs.flatMap(_.symbol.allOverriddenSymbols) // Need to put the new defs first because we process in linearization order to make overridees correct, // but we want parent definitions to come first so that if child inline traits refer to values defined in a parent - // inline trait these are defined. - val inlinedDefs1 = parentTraitInliner.expandDefs(overriddenSymbols) ::: inlineDefs + // inline trait these are defined. + val inlinedDefs1 = parentTraitInliner.expandDefs(overriddenSymbols) ::: inlineDefs cls.symbol.flags = updateFlagsFromInlinedParent(cls.symbol.flags, parent.symbol.flags) - + val childDefs1 = parentTraitInliner.adaptSuperCalls(childDefs) (parentTraitInliner.adaptSuperCalls(inlinedDefs1), childDefs1) } @@ -410,11 +410,11 @@ object Inlines: val newbody = newDefs._1 ::: newDefs._2 val paramAccessors = newbody.filter(_.symbol.is(ParamAccessor)) - + for pacc <- paramAccessors otherstat <- newbody if !otherstat.symbol.is(ParamAccessor) && otherstat.denot.matches(pacc.denot.asSingleDenotation) - do report.error(s"Inlining of inline trait created name conflict on ${pacc.denot.name}. Constructor parameters of inline receivers may not collide with members of inline traits.", pacc.srcPos) - + do report.error(s"Inlining of inline trait created name conflict on ${pacc.denot.name}. Constructor parameters of inline receivers may not collide with members of inline traits.", pacc.srcPos) + val impl1 = cpy.Template(impl)(body = newbody) cpy.TypeDef(cls)(rhs = impl1) @@ -720,7 +720,7 @@ object Inlines: /** The Inlined node representing the inlined call */ def expand(rhsToInline: Tree): Tree = - // Special handling of `requireConst` and `codeOf` + // Special handling of `requireConst`, `codeOf`, and `magic.Ok` callValueArgss match case (arg :: Nil) :: Nil => if inlinedMethod == defn.Compiletime_requireConst then @@ -730,6 +730,8 @@ object Inlines: return unitLiteral.withSpan(call.span) else if inlinedMethod == defn.Compiletime_codeOf then return Intrinsics.codeOf(arg, call.srcPos) + else if inlinedMethod == defn.Magic_OkApply && arg.tpe.isNotNullNorMaybe then + return arg case _ => // Special handling of `constValue[T]`, `constValueOpt[T]`, `constValueTuple[T]`, `summonInline[T]` and `summonAll[T]` @@ -917,11 +919,11 @@ object Inlines: } end expandDefs - def adaptSuperCalls(defs: List[Tree]) = + def adaptSuperCalls(defs: List[Tree]) = val ttmap = TreeTypeMap(treeMap = { // We go through all ancestor inline traits so eventually we will find the one with matching parentSym case sel@Select(Super(qual, mix), name) if sel.symbol.owner == parentSym => - // At that point either the method is overridden so needs mangling (and we just copied and mangled it in this inlining phase), + // At that point either the method is overridden so needs mangling (and we just copied and mangled it in this inlining phase), // or not, in which case call directly by original name. In both cases we are calling the method resulting from inlining, on the // inline receiver class. Select(This(ctx.owner.asClass), paramAccessorsMapper.getParamAccessorName(sel.symbol.owner, name).getOrElse(name)) @@ -977,11 +979,11 @@ object Inlines: } override protected val inlinerTypeMap: InlinerTypeMap = InlineTraitTypeMap() - + override protected val inlinerTreeMap: InlinerTreeMap = InlineTraitTreeMap() override protected def computeThisBindings(): Unit = () - + override protected def canElideThis(tpe: ThisType): Boolean = true override protected def inlineCtx(inlineTyper: InlineTyper)(using Context): Context = @@ -1024,7 +1026,7 @@ object Inlines: paramAccessorsMapper .getParamAccessorRhs(vdef.symbol.owner, vdef.symbol.name) .getOrElse(inlinedRhs(vdef, inlinedSym)) - + val rhs1 = rhs.changeNonLocalOwners(inlinedSym) tpd.ValDef(inlinedSym.asTerm, rhs1).withSpan(parent.span) @@ -1049,7 +1051,7 @@ object Inlines: ctx.typeAssigner.assignType(untpd.TypeDef(inlinedSym.name.asTypeName, TypeTree(inlinedRhsType)), inlinedSym).withSpan(parent.span) else tpd.TypeDef(inlinedSym.asType).withSpan(parent.span) - + private def inlinedRhs(vddef: ValOrDefDef, inlinedSym: Symbol)(using Context): Tree = val rhs = vddef.rhs.changeOwner(vddef.symbol, inlinedSym) @@ -1060,14 +1062,14 @@ object Inlines: rhs else val symbolMap = mutable.Map[Symbol, Symbol]() - // TODO: This inlines also some calls to inline defs that were made in the inline trait body, is that ok? + // TODO: This inlines also some calls to inline defs that were made in the inline trait body, is that ok? val rhs1 = Inlined(tpd.ref(parentSym).withSpan(parent.span), Nil, inlined(rhs)._2.withSpan(parent.span).cloneIn(parentSym.source)).withSpan(parent.span) - + // In case of nested inline trait inlines, because BodyAnnotation is out of date, // body inlined misses nested expansion, but we have the symbols for the items that should be there // Remove them so that they can be inlined properly later. val ttmap = TreeTypeMap(treeMap = { - case tree@TypeDef(name, tmpl: Template) if Inlines.needsInlining(tree) => + case tree@TypeDef(name, tmpl: Template) if Inlines.needsInlining(tree) => val newSym = tree.symbol.copy(coord = spanCoord(tree.span)) // Coord should correspond to original location because we will inline from there. newSym.info = ClassInfo(tree.symbol.owner.thisType, newSym.asClass, tree.symbol.asClass.parentTypes, Scopes.newScope) @@ -1153,7 +1155,7 @@ object Inlines: class InlineTraitState( // For a class symbol created during inlining of an inline trait, - // the chain of inlined traits which produced it. We don't actually care about the order. + // the chain of inlined traits which produced it. We don't actually care about the order. // Used as a "seen list" for cycle checking. Persists across invocations of InlineParentTrait val inlineOrigins: mutable.Map[Symbol, Set[Symbol]] = mutable.HashMap[Symbol, Set[Symbol]]().withDefaultValue(Set.empty), val inlineTraitsPhase: InlineTraitState.InlineContext = InlineTraitState.InlineContext.None diff --git a/compiler/src/dotty/tools/dotc/transform/BetaReduce.scala b/compiler/src/dotty/tools/dotc/transform/BetaReduce.scala index 7a03cd2a15f5..84f18cafa68e 100644 --- a/compiler/src/dotty/tools/dotc/transform/BetaReduce.scala +++ b/compiler/src/dotty/tools/dotc/transform/BetaReduce.scala @@ -45,6 +45,12 @@ class BetaReduce extends MiniPhase: if app1 ne app then report.log(i"beta reduce $app -> $app1") app1 + /** Cleanup ifs after reduceEQ */ + override def transformIf(tree: If)(using Context): Tree = tree.cond match + case Literal(Constant(true)) => tree.thenp + case Literal(Constant(false)) => tree.elsep + case _ => tree + object BetaReduce: import ast.tpd.* @@ -71,6 +77,9 @@ object BetaReduce: * type X1 = T1; ...; type Xm = Tm;val/def x1 = e1; ...; val/def xn = en; b * * This beta-reduction preserves the integrity of `Inlined` tree nodes. + * + * Also, replace some == tests between constants with known outcomes by true/false. + * This is useful since such tests can arise though inlining, e.g. in maybe-translation.scala. */ def apply(tree: Tree)(using Context): Tree = val bindingsBuf = new ListBuffer[DefTree] @@ -111,7 +120,7 @@ object BetaReduce: case None => tree case _ => - tree + inlines.Inliner.reduceEQ(tree) /** Beta-reduces a call to `ddef` with arguments `args` and registers new bindings. * @return optionally, the expanded call, or none if the actual argument diff --git a/compiler/src/dotty/tools/dotc/transform/PatternMatcher.scala b/compiler/src/dotty/tools/dotc/transform/PatternMatcher.scala index bb88f348d185..edb448f5c13f 100644 --- a/compiler/src/dotty/tools/dotc/transform/PatternMatcher.scala +++ b/compiler/src/dotty/tools/dotc/transform/PatternMatcher.scala @@ -388,13 +388,16 @@ object PatternMatcher { if gm.tpe.widen.isRef(defn.MagicMaybeClass) then if isErrMatch then val MagicMaybeType(_, errArg, nullable) = gm.tpe.widen.runtimeChecked - val select = gm.asInstance(defn.MagicFailClass.typeRef.appliedTo(defn.AnyType)) - .select(nme.elem) - if nullable then - If(gm.nullTest(cond = true), - unitLiteral.asInstance(errArg), - select) - else select + if errArg.isRef(defn.UnitClass) then + unitLiteral + else + val select = gm.asInstance(defn.MagicFailClass.typeRef.appliedTo(defn.AnyType)) + .select(nme.elem) + if nullable then + If(gm.nullTest(cond = true), + unitLiteral.ensureConforms(errArg), + select) + else select else val validTpe = defn.MagicValidClass.typeRef If(gm.isInstance(validTpe), @@ -951,7 +954,7 @@ object PatternMatcher { case GuardTest => scrutinee case EqualTest(tree) => - tree.equal(scrutinee) + inlines.Inliner.reduceEQ(tree.equal(scrutinee)) case LengthTest(len, exact) => val lengthCompareSym = defn.Seq_lengthCompare.matchingMember(scrutinee.tpe) if (lengthCompareSym.exists) @@ -1166,7 +1169,7 @@ object PatternMatcher { if (acc.isEmpty) emitCondWithPos(otherPlan) else acc.select(nme.ZAND).appliedTo(emitCondWithPos(otherPlan)) } - If(conditions, emit(plan.onSuccess), unitLiteral) + conditional(conditions, emit(plan.onSuccess), unitLiteral) } } emitWithMashedConditions(plan :: Nil) diff --git a/library/src-bootstrapped/scala/magic/package.scala b/library/src-bootstrapped/scala/magic/package.scala index c8dc4994c3fa..7183e1efeec3 100644 --- a/library/src-bootstrapped/scala/magic/package.scala +++ b/library/src-bootstrapped/scala/magic/package.scala @@ -23,10 +23,10 @@ package object magic { case Ok(y) => Ok(y) case Err(e) => Err(f(e)) - inline def provided(cond: Boolean)(using CanErr[Unit]): Unit = + inline def provided(inline cond: Boolean)(using CanErr[Unit]): Unit = if !cond then boundary.break(Err(())) - inline def provided[E](cond: Boolean, inline e: E)(using CanErr[E]): Unit = + 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/magic/Err.scala b/library/src/scala/magic/Err.scala index 1abc7be15628..b5f5daffd167 100644 --- a/library/src/scala/magic/Err.scala +++ b/library/src/scala/magic/Err.scala @@ -8,9 +8,10 @@ import annotation.experimental @experimental object Err: - /** `inline` neded since nonbootrapped 3.9.0 compiler + /** `inline` needed since nonbootrapped 3.9.0 compiler * uses a different erasure for Maybe than boostrapped - * 3.10.0 compiler. + * 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)) diff --git a/tests/pos/maybe-translation.scala b/tests/pos/maybe-translation.scala new file mode 100644 index 000000000000..6d9f659c08c4 --- /dev/null +++ b/tests/pos/maybe-translation.scala @@ -0,0 +1,97 @@ +//> using options -Yexplicit-nulls +import language.experimental.magic +import scala.magic.* + +def foo(x: Int): Int? = + if x > 0 && x < 10 then x else null + +def bar(x: Int?) = + maybe: + val y = x? + if y > 0 + y + +/* With -Vprint:erasure should produce something like: + + (boundary1[Object]: + { + val y: Int = + matchResult1[Int]: + { + case val x1: Object = x + if (null == x1).unary_!() then + { + case val x4: Int = + Int.unbox( + (if x1.isInstanceOf[scala.magic.runtime.Valid] then + x1.asInstanceOf[scala.magic.runtime.Valid].elem() + else x1):Object + ) + case val y: Int = x4 + return[matchResult1] y:Int + } + else () + { + case val x3: scala.runtime.BoxedUnit = + scala.runtime.BoxedUnit.UNIT + case val e: scala.runtime.BoxedUnit = x3 + return[matchResult1] return[boundary1] null:Object:Object + } + throw new MatchError(x1) + } + if (y > 0).unary_!() then return[boundary1] null:Object:Object else () + Int.box(y:Int) + } + ):Object:Object +*/ + +def baz(x: Int ? String) = + maybe: + val y = x.withErr("not an int")? + if y > 0 else "not positive" + y + +/* With -Vprint:erasure should produce something like: + + (boundary2[Object]: + { + val y: Int = + { + val x$proxy1: Object = scala.magic.withErr(x, "not an int") + matchResult2[Int]: + { + case val x6: Object = x$proxy1 + if + (null == x6).unary_!() && + x6.isInstanceOf[scala.magic.runtime.Fail].unary_!() + then + { + case val x9: Int = + Int.unbox( + (if x6.isInstanceOf[scala.magic.runtime.Valid] then + x6.asInstanceOf[scala.magic.runtime.Valid].elem() + else x6):Object + ) + case val y: Int = x9 + return[matchResult2] y:Int + } + else () + { + case val x7: Object = + x6.asInstanceOf[scala.magic.runtime.Fail].elem() + case val e: String = x7.asInstanceOf[String] + return[matchResult2] + return[boundary2] + new scala.magic.runtime.Fail(e):Object:Object + } + throw new MatchError(x6) + } + } + if (y > 0).unary_!() then + return[boundary2] + new scala.magic.runtime.Fail("not positive"):Object:Object + else () + Int.box(y:Int) + } + ):Object:Object +*/ From 782e0db4e098093cf45846ee9e400ddb72c3aedb Mon Sep 17 00:00:00 2001 From: odersky Date: Mon, 24 Aug 2026 10:04:48 +0200 Subject: [PATCH 28/28] Fix rebase breakage --- tests/run/i1773-maybe.scala | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/run/i1773-maybe.scala b/tests/run/i1773-maybe.scala index 7f38c4268113..6f40e67a9f8c 100644 --- a/tests/run/i1773-maybe.scala +++ b/tests/run/i1773-maybe.scala @@ -1,5 +1,6 @@ //> using options -Yexplicit-nulls import language.experimental.magic +import language.implicitConversions object Test { into class Foo(sc: StringContext) { object q {