diff --git a/compiler/src/dotty/tools/dotc/ast/Desugar.scala b/compiler/src/dotty/tools/dotc/ast/Desugar.scala index 8401d255155c..9024c363ecd7 100644 --- a/compiler/src/dotty/tools/dotc/ast/Desugar.scala +++ b/compiler/src/dotty/tools/dotc/ast/Desugar.scala @@ -8,7 +8,7 @@ import Symbols.*, StdNames.*, Trees.*, ContextOps.* import Decorators.* import Annotations.Annotation import NameKinds.{UniqueName, ContextBoundParamName, ContextFunctionParamName, DefaultGetterName, WildcardParamName} -import typer.{Namer, Checking} +import typer.{Namer, Checking, ErrorReporting} import util.{Property, SourceFile, SourcePosition, SrcPos, Chars} import config.{Feature, Config} import config.Feature.{sourceVersion, migrateTo3, enabled} @@ -214,9 +214,10 @@ object desugar { def valDef(vdef0: ValDef)(using Context): Tree = val vdef @ ValDef(_, tpt, rhs) = vdef0 val valName = normalizeName(vdef, tpt).asTermName + val tpt1 = desugarQualifiedTypes(tpt, valName) var mods1 = vdef.mods - val vdef1 = cpy.ValDef(vdef)(name = valName).withMods(mods1) + val vdef1 = cpy.ValDef(vdef)(name = valName, tpt = tpt1).withMods(mods1) if isSetterNeeded(vdef) then val setterParam = makeSyntheticParameter(tpt = SetterParamTree().watching(vdef)) @@ -233,6 +234,14 @@ object desugar { else vdef1 end valDef + def caseDef(cdef: CaseDef)(using Context): CaseDef = + if Feature.qualifiedTypesEnabled then + val CaseDef(pat, guard, body) = cdef + val pat1 = DesugarQualifiedTypesInPatternMap().transform(pat) + cpy.CaseDef(cdef)(pat1, guard, body) + else + cdef + def mapParamss(paramss: List[ParamClause]) (mapTypeParam: TypeDef => TypeDef) (mapTermParam: ValDef => ValDef)(using Context): List[ParamClause] = @@ -2279,6 +2288,8 @@ object desugar { case PatDef(mods, pats, tpt, rhs) => val pats1 = if (tpt.isEmpty) pats else pats map (Typed(_, tpt)) flatTree(pats1 map (makePatDef(tree, mods, _, rhs))) + case QualifiedTypeTree(parent, paramName, qualifier) => + qualifiedType(parent, paramName.getOrElse(nme.WILDCARD), qualifier, tree.span) case ext: ExtMethods => Block(List(ext), syntheticUnitLiteral.withSpan(ext.span)) case f: FunctionWithMods if f.hasErasedParams => makeFunctionWithValDefs(f, pt) @@ -2457,4 +2468,51 @@ object desugar { collect(tree) buf.toList } + + /** Desugar subtrees that are `QualifiedTypeTree`s using `outerParamName` as + * the qualified parameter name. + */ + private def desugarQualifiedTypes(tpt: Tree, outerParamName: TermName)(using Context): Tree = + def transform(tree: Tree): Tree = + tree match + case QualifiedTypeTree(parent, None, qualifier) => + qualifiedType(transform(parent), outerParamName, qualifier, tree.span) + case QualifiedTypeTree(parent, paramName, qualifier) => + cpy.QualifiedTypeTree(tree)(transform(parent), paramName, qualifier) + case TypeApply(fn, args) => + cpy.TypeApply(tree)(transform(fn), args) + case AppliedTypeTree(fn, args) => + cpy.AppliedTypeTree(tree)(transform(fn), args) + case InfixOp(left, op, right) => + cpy.InfixOp(tree)(transform(left), op, transform(right)) + case Parens(arg) => + cpy.Parens(tree)(transform(arg)) + case _ => + tree + + if Feature.qualifiedTypesEnabled then + trace(i"desugar qualified types in pattern: $tpt", Printers.qualifiedTypes): + transform(tpt) + else + tpt + + private class DesugarQualifiedTypesInPatternMap extends UntypedTreeMap: + override def transform(tree: Tree)(using Context): Tree = + tree match + case Typed(ident @ Ident(name: TermName), tpt) => + cpy.Typed(tree)(ident, desugarQualifiedTypes(tpt, name)) + case _ => + super.transform(tree) + + /** Returns the annotated type used to represent the qualified type with the + * given components: + * `parent @qualified[parent]((paramName: parent) => qualifier)`. + */ + def qualifiedType(parent: Tree, paramName: TermName, qualifier: Tree, span: Span)(using Context): Tree = + val param = makeParameter(paramName, parent, EmptyModifiers) // paramName: parent + val predicate = WildcardFunction(List(param), qualifier) // (paramName: parent) => qualifier + val qualifiedAnnot = scalaAnnotationDot(nme.qualified) + val annot = Apply(TypeApply(qualifiedAnnot, List(parent)), predicate).withSpan(span) // @qualified[parent](predicate) + Annotated(parent, annot).withSpan(span) // parent @qualified[parent](predicate) + } diff --git a/compiler/src/dotty/tools/dotc/ast/tpd.scala b/compiler/src/dotty/tools/dotc/ast/tpd.scala index 2abae103780f..cbcc94135264 100644 --- a/compiler/src/dotty/tools/dotc/ast/tpd.scala +++ b/compiler/src/dotty/tools/dotc/ast/tpd.scala @@ -1437,6 +1437,19 @@ object tpd extends Trees.Instance[Type] with TypedTreeInfo { def unapply(ts: List[Tree]): Option[List[Tree]] = if ts.nonEmpty && ts.head.isType then Some(ts) else None + + /** An extractor for trees that are constant values. */ + object ConstantTree: + def unapply(tree: Tree)(using Context): Option[Constant] = + tree match + case Inlined(_, Nil, expr) => unapply(expr) + case Typed(expr, _) => unapply(expr) + case Literal(c) if c.tag == Constants.NullTag => Some(c) + case _ => + tree.tpe.widenTermRefExpr.normalized.simplified match + case ConstantType(c) => Some(c) + case _ => None + /** Split argument clauses into a leading type argument clause if it exists and * remaining clauses */ diff --git a/compiler/src/dotty/tools/dotc/ast/untpd.scala b/compiler/src/dotty/tools/dotc/ast/untpd.scala index e9a6e148ce86..e1fc4f73cbfa 100644 --- a/compiler/src/dotty/tools/dotc/ast/untpd.scala +++ b/compiler/src/dotty/tools/dotc/ast/untpd.scala @@ -155,6 +155,13 @@ object untpd extends Trees.Instance[Untyped] with UntypedTreeInfo { */ case class CapturesAndResult(refs: List[Tree], parent: Tree)(implicit @constructorOnly src: SourceFile) extends TypTree + /** `{ x: parent with qualifier }` if `paramName == Some(x)`, + * `parent with qualifier` otherwise. + * + * Only relevant under `qualifiedTypes`. + */ + case class QualifiedTypeTree(parent: Tree, paramName: Option[TermName], qualifier: Tree)(implicit @constructorOnly src: SourceFile) extends TypTree + /** A type tree appearing somewhere in the untyped DefDef of a lambda, it will be typed using `tpFun`. * * @param isResult Is this the result type of the lambda? This is handled specially in `Namer#valOrDefDefSig`. @@ -462,7 +469,7 @@ object untpd extends Trees.Instance[Untyped] with UntypedTreeInfo { def New(tpt: Tree, argss: List[List[Tree]])(using Context): Tree = ensureApplied(argss.foldLeft(makeNew(tpt))(Apply(_, _))) - /** A new expression with constrictor and possibly type arguments. See + /** A new expression with constructor and possibly type arguments. See * `New(tpt, argss)` for details. */ def makeNew(tpt: Tree)(using Context): Tree = { @@ -700,6 +707,10 @@ object untpd extends Trees.Instance[Untyped] with UntypedTreeInfo { case tree: CapturesAndResult if (refs eq tree.refs) && (parent eq tree.parent) => tree case _ => finalize(tree, untpd.CapturesAndResult(refs, parent)) + def QualifiedTypeTree(tree: Tree)(parent: Tree, paramName: Option[TermName], qualifier: Tree)(using Context): Tree = tree match + case tree: QualifiedTypeTree if (parent eq tree.parent) && (paramName eq tree.paramName) && (qualifier eq tree.qualifier) => tree + case _ => finalize(tree, untpd.QualifiedTypeTree(parent, paramName, qualifier)(tree.source)) + def TypedSplice(tree: Tree)(splice: tpd.Tree)(using Context): ProxyTree = tree match { case tree: TypedSplice if splice `eq` tree.splice => tree case _ => finalize(tree, untpd.TypedSplice(splice)(using ctx)) @@ -763,6 +774,8 @@ object untpd extends Trees.Instance[Untyped] with UntypedTreeInfo { cpy.MacroTree(tree)(transform(expr)) case CapturesAndResult(refs, parent) => cpy.CapturesAndResult(tree)(transform(refs), transform(parent)) + case QualifiedTypeTree(parent, paramName, qualifier) => + cpy.QualifiedTypeTree(tree)(transform(parent), paramName, transform(qualifier)) case _ => super.transformMoreCases(tree) } @@ -822,6 +835,8 @@ object untpd extends Trees.Instance[Untyped] with UntypedTreeInfo { this(x, expr) case CapturesAndResult(refs, parent) => this(this(x, refs), parent) + case QualifiedTypeTree(parent, paramName, qualifier) => + this(this(x, parent), qualifier) case _ => super.foldMoreCases(x, tree) } diff --git a/compiler/src/dotty/tools/dotc/config/Feature.scala b/compiler/src/dotty/tools/dotc/config/Feature.scala index 8fda99be6896..12f168ce30a4 100644 --- a/compiler/src/dotty/tools/dotc/config/Feature.scala +++ b/compiler/src/dotty/tools/dotc/config/Feature.scala @@ -32,6 +32,7 @@ object Feature: val saferExceptions = experimental("saferExceptions") val pureFunctions = experimental("pureFunctions") val captureChecking = experimental("captureChecking") + val qualifiedTypes = experimental("qualifiedTypes") val into = experimental("into") val modularity = experimental("modularity") val quotedPatternsWithPolymorphicFunctions = experimental("quotedPatternsWithPolymorphicFunctions") @@ -60,6 +61,7 @@ object Feature: (saferExceptions, "Enable safer exceptions"), (pureFunctions, "Enable pure functions for capture checking"), (captureChecking, "Enable experimental capture checking"), + (qualifiedTypes, "Enable experimental qualified types"), (into, "Allow into modifier on parameter types"), (modularity, "Enable experimental modularity features"), (packageObjectValues, "Enable experimental package objects as values"), @@ -146,6 +148,10 @@ object Feature: if ctx.run != null then ctx.run.nn.ccEnabledSomewhere else enabledBySetting(captureChecking) + /** Is qualifiedTypes enabled for this compilation unit? */ + def qualifiedTypesEnabled(using Context) = + enabledBySetting(qualifiedTypes) + def sourceVersionSetting(using Context): SourceVersion = SourceVersion.valueOf(ctx.settings.source.value) diff --git a/compiler/src/dotty/tools/dotc/config/Printers.scala b/compiler/src/dotty/tools/dotc/config/Printers.scala index 4c66e1cdf833..2ace7b1f402a 100644 --- a/compiler/src/dotty/tools/dotc/config/Printers.scala +++ b/compiler/src/dotty/tools/dotc/config/Printers.scala @@ -51,6 +51,7 @@ object Printers { val overload = noPrinter val patmatch = noPrinter val pickling = noPrinter + val qualifiedTypes = noPrinter val quotePickling = noPrinter val plugins = noPrinter val recheckr = noPrinter diff --git a/compiler/src/dotty/tools/dotc/core/Definitions.scala b/compiler/src/dotty/tools/dotc/core/Definitions.scala index 83c85adb0f43..276c32c0cde9 100644 --- a/compiler/src/dotty/tools/dotc/core/Definitions.scala +++ b/compiler/src/dotty/tools/dotc/core/Definitions.scala @@ -616,9 +616,11 @@ class Definitions { @tu lazy val Int_/ : Symbol = IntClass.requiredMethod(nme.DIV, List(IntType)) @tu lazy val Int_* : Symbol = IntClass.requiredMethod(nme.MUL, List(IntType)) @tu lazy val Int_== : Symbol = IntClass.requiredMethod(nme.EQ, List(IntType)) + @tu lazy val Int_!= : Symbol = IntClass.requiredMethod(nme.NE, List(IntType)) @tu lazy val Int_>= : Symbol = IntClass.requiredMethod(nme.GE, List(IntType)) @tu lazy val Int_<= : Symbol = IntClass.requiredMethod(nme.LE, List(IntType)) @tu lazy val Int_> : Symbol = IntClass.requiredMethod(nme.GT, List(IntType)) + @tu lazy val Int_< : Symbol = IntClass.requiredMethod(nme.LT, List(IntType)) @tu lazy val LongType: TypeRef = valueTypeRef("scala.Long", java.lang.Long.TYPE, LongEnc, nme.specializedTypeNames.Long) def LongClass(using Context): ClassSymbol = LongType.symbol.asClass @tu lazy val Long_+ : Symbol = LongClass.requiredMethod(nme.PLUS, List(LongType)) @@ -661,6 +663,8 @@ class Definitions { @tu lazy val StringClass: ClassSymbol = requiredClass("java.lang.String") def StringType: Type = StringClass.typeRef @tu lazy val StringModule: Symbol = StringClass.linkedClass + @tu lazy val String_== : TermSymbol = enterMethod(StringClass, nme.EQ, methOfAnyRef(BooleanType), Final) + @tu lazy val String_!= : TermSymbol = enterMethod(StringClass, nme.NE, methOfAnyRef(BooleanType), Final) @tu lazy val String_+ : TermSymbol = enterMethod(StringClass, nme.raw.PLUS, methOfAny(StringType), Final) @tu lazy val String_valueOf_Object: Symbol = StringModule.info.member(nme.valueOf).suchThat(_.info.firstParamTypes match { case List(pt) => pt.isAny || pt.stripNull().isAnyRef @@ -1036,6 +1040,7 @@ class Definitions { @tu lazy val DeprecatedAnnot: ClassSymbol = requiredClass("scala.deprecated") @tu lazy val DeprecatedOverridingAnnot: ClassSymbol = requiredClass("scala.deprecatedOverriding") @tu lazy val DeprecatedInheritanceAnnot: ClassSymbol = requiredClass("scala.deprecatedInheritance") + @tu lazy val QualifiedAnnot: ClassSymbol = requiredClass("scala.annotation.qualified") @tu lazy val ImplicitAmbiguousAnnot: ClassSymbol = requiredClass("scala.annotation.implicitAmbiguous") @tu lazy val ImplicitNotFoundAnnot: ClassSymbol = requiredClass("scala.annotation.implicitNotFound") @tu lazy val InferredDepFunAnnot: ClassSymbol = requiredClass("scala.caps.internal.inferredDepFun") diff --git a/compiler/src/dotty/tools/dotc/core/StdNames.scala b/compiler/src/dotty/tools/dotc/core/StdNames.scala index ac1f4f448722..e200dc5bf0b4 100644 --- a/compiler/src/dotty/tools/dotc/core/StdNames.scala +++ b/compiler/src/dotty/tools/dotc/core/StdNames.scala @@ -587,6 +587,7 @@ object StdNames { val productElementName: N = "productElementName" val productIterator: N = "productIterator" val productPrefix: N = "productPrefix" + val qualified : N = "qualified" val quotes : N = "quotes" val raw_ : N = "raw" val rd: N = "rd" diff --git a/compiler/src/dotty/tools/dotc/core/TypeComparer.scala b/compiler/src/dotty/tools/dotc/core/TypeComparer.scala index 210e7f12b4b4..943b591fe380 100644 --- a/compiler/src/dotty/tools/dotc/core/TypeComparer.scala +++ b/compiler/src/dotty/tools/dotc/core/TypeComparer.scala @@ -27,6 +27,7 @@ import Capabilities.Capability import NameKinds.WildcardParamName import MatchTypes.isConcrete import scala.util.boundary, boundary.break +import qualified_types.{QualifiedType, QualifiedTypes} /** Provides methods to compare types. */ @@ -884,6 +885,11 @@ class TypeComparer(@constructorOnly initctx: Context) extends ConstraintHandling println(i"assertion failed while compare captured $tp1 <:< $tp2") throw ex compareCapturing || fourthTry + case QualifiedType(parent2, qualifier2) => + val parentSub = recur(tp1, parent2) + val qualifierSub = QualifiedTypes.typeImplies(tp1, qualifier2) + //println(i"compare qualified $tp1 <:< $tp2, parentSub = $parentSub, qualifierSub = $qualifierSub") + parentSub && qualifierSub case tp2: AnnotatedType if tp2.isRefining => (tp1.derivesAnnotWith(tp2.annot.sameAnnotation) || tp1.isBottomType) && recur(tp1, tp2.parent) @@ -2110,7 +2116,7 @@ class TypeComparer(@constructorOnly initctx: Context) extends ConstraintHandling * since `T >: Int` is subsumed by both alternatives in the first match clause. * * However, the following should not: - * + * * def foo[T](e: Expr[T]): T = e match * case I1(_) | B(_) => 42 * diff --git a/compiler/src/dotty/tools/dotc/core/Types.scala b/compiler/src/dotty/tools/dotc/core/Types.scala index b06bd5c00a28..c0bc675a1106 100644 --- a/compiler/src/dotty/tools/dotc/core/Types.scala +++ b/compiler/src/dotty/tools/dotc/core/Types.scala @@ -41,7 +41,7 @@ import compiletime.uninitialized import cc.* import CaptureSet.IdentityCaptRefMap import Capabilities.* - +import qualified_types.{QualifiedType, QualifiedAnnotation} import scala.annotation.internal.sharable import scala.annotation.threadUnsafe @@ -56,7 +56,7 @@ object Types extends TypeUtils { * The principal subclasses and sub-objects are as follows: * * ```none - * Type -+- ProxyType --+- NamedType ----+--- TypeRef + * Type -+- TypeProxy --+- NamedType ----+--- TypeRef * | | \ * | +- SingletonType-+-+- TermRef * | | | @@ -191,9 +191,10 @@ object Types extends TypeUtils { /** Is this type a (possibly refined, applied, aliased or annotated) type reference * to the given type symbol? - * @sym The symbol to compare to. It must be a class symbol or abstract type. + * @param sym The symbol to compare to. It must be a class symbol or abstract type. * It makes no sense for it to be an alias type because isRef would always * return false in that case. + * @param skipRefined If true, skip refinements, annotated types and applied types. */ def isRef(sym: Symbol, skipRefined: Boolean = true)(using Context): Boolean = this match { case this1: TypeRef => @@ -211,7 +212,7 @@ object Types extends TypeUtils { else this1.underlying.isRef(sym, skipRefined) case this1: TypeVar => this1.instanceOpt.isRef(sym, skipRefined) - case this1: AnnotatedType => + case this1: AnnotatedType if (!this1.isRefining || skipRefined) => this1.parent.isRef(sym, skipRefined) case _ => false } @@ -1567,6 +1568,7 @@ object Types extends TypeUtils { def apply(tp: Type) = /*trace(i"deskolemize($tp) at $variance", show = true)*/ tp match { case tp: SkolemType => range(defn.NothingType, atVariance(1)(apply(tp.info))) + case QualifiedType(_, _) => tp case _ => mapOver(tp) } } @@ -2102,7 +2104,7 @@ object Types extends TypeUtils { /** Is `this` isomorphic to `that`, assuming pairs of matching binders `bs`? * It is assumed that `this.ne(that)`. */ - protected def iso(that: Any, bs: BinderPairs): Boolean = this.equals(that) + def iso(that: Any, bs: BinderPairs): Boolean = this.equals(that) /** Equality used for hash-consing; uses `eq` on all recursive invocations, * except where a BindingType is involved. The latter demand a deep isomorphism check. @@ -3486,7 +3488,7 @@ object Types extends TypeUtils { case _ => false } - override protected def iso(that: Any, bs: BinderPairs) = that match + override def iso(that: Any, bs: BinderPairs) = that match case that: AndType => tp1.equals(that.tp1, bs) && tp2.equals(that.tp2, bs) case _ => false } @@ -3640,7 +3642,7 @@ object Types extends TypeUtils { case _ => false } - override protected def iso(that: Any, bs: BinderPairs) = that match + override def iso(that: Any, bs: BinderPairs) = that match case that: OrType => tp1.equals(that.tp1, bs) && tp2.equals(that.tp2, bs) && isSoft == that.isSoft case _ => false } @@ -4966,7 +4968,7 @@ object Types extends TypeUtils { * anymore, or NoType if the variable can still be further constrained or a provisional * instance type in the constraint can be retracted. */ - private[core] def permanentInst = inst + def permanentInst = inst private[core] def setPermanentInst(tp: Type): Unit = inst = tp if tp.exists && owningState != null then @@ -5831,7 +5833,8 @@ object Types extends TypeUtils { def make(underlying: Type, annots: List[Annotation])(using Context): Type = annots.foldLeft(underlying)(apply(_, _)) def apply(parent: Type, annot: Annotation)(using Context): AnnotatedType = - unique(CachedAnnotatedType(parent, annot)) + val annot1 = if annot.symbol == defn.QualifiedAnnot then QualifiedAnnotation(annot) else annot + unique(CachedAnnotatedType(parent, annot1)) end AnnotatedType // Special type objects and classes ----------------------------------------------------- diff --git a/compiler/src/dotty/tools/dotc/parsing/Parsers.scala b/compiler/src/dotty/tools/dotc/parsing/Parsers.scala index c607d0377d83..a85af68778c6 100644 --- a/compiler/src/dotty/tools/dotc/parsing/Parsers.scala +++ b/compiler/src/dotty/tools/dotc/parsing/Parsers.scala @@ -447,6 +447,13 @@ object Parsers { finally inMatchPattern = saved } + private var inQualifiedType = false + private def fromWithinQualifiedType[T](body: => T): T = + val saved = inQualifiedType + inQualifiedType = true + try body + finally inQualifiedType = saved + private var staged = StageKind.None def withinStaged[T](kind: StageKind)(op: => T): T = { val saved = staged @@ -1629,6 +1636,7 @@ object Parsers { * | TypTypeParamClause ‘=>>’ Type * | FunParamClause ‘=>>’ Type * | MatchType + * | QualifiedType2 -- under qualifiedTypes * | InfixType * FunType ::= (MonoFunType | PolyFunType) * MonoFunType ::= FunTypeArgs (‘=>’ | ‘?=>’) Type @@ -1639,6 +1647,11 @@ object Parsers { * | `(' [ FunArgType {`,' FunArgType } ] `)' * | '(' [ TypedFunParam {',' TypedFunParam } ')' * MatchType ::= InfixType `match` <<< TypeCaseClauses >>> + * QualifiedType2 ::= InfixType `with` PostfixExpr + * IntoType ::= [‘into’] IntoTargetType + * | ‘( IntoType ‘)’ + * IntoTargetType ::= Type + * | FunTypeArgs (‘=>’ | ‘?=>’) IntoType */ def typ(inContextBound: Boolean = false): Tree = val start = in.offset @@ -1698,6 +1711,8 @@ object Parsers { functionRest(t :: Nil) case MATCH => matchType(t) + case WITH if in.featureEnabled(Feature.qualifiedTypes) => + qualifiedTypeShort(t) case FORSOME => syntaxError(ExistentialTypesNoLongerSupported()) t @@ -1832,6 +1847,7 @@ object Parsers { def funParamClauses(): List[List[ValDef]] = if in.token == LPAREN then funParamClause() :: funParamClauses() else Nil + /** InfixType ::= RefinedType {id [nl] RefinedType} * | RefinedType `^` -- under captureChecking */ @@ -1891,7 +1907,7 @@ object Parsers { def withType(): Tree = withTypeRest(annotType()) def withTypeRest(t: Tree): Tree = - if in.token == WITH then + if in.token == WITH && !in.featureEnabled(Feature.qualifiedTypes) then val withOffset = in.offset in.nextToken() if in.token == LBRACE || in.token == INDENT then @@ -2039,6 +2055,7 @@ object Parsers { * | ‘(’ ArgTypes ‘)’ * | ‘(’ NamesAndTypes ‘)’ * | Refinement + * | QualifiedType -- under qualifiedTypes * | TypeSplice -- deprecated syntax (since 3.0.0) * | SimpleType1 TypeArgs * | SimpleType1 `#' id @@ -2049,7 +2066,10 @@ object Parsers { makeTupleOrParens(inParensWithCommas(argTypes(namedOK = false, wildOK = true, tupleOK = true))) } else if in.token == LBRACE then - atSpan(in.offset) { RefinedTypeTree(EmptyTree, refinement(indentOK = false)) } + if in.featureEnabled(Feature.qualifiedTypes) && in.lookahead.token == IDENTIFIER then + qualifiedType() + else + atSpan(in.offset) { RefinedTypeTree(EmptyTree, refinement(indentOK = false)) } else if (isSplice) splice(isType = true) else @@ -2213,6 +2233,30 @@ object Parsers { else inBraces(refineStatSeq()) + /** QualifiedType ::= `{` Ident `:` Type `with` Block `}` + */ + def qualifiedType(): Tree = + val startOffset = in.offset + accept(LBRACE) + val id = ident() + accept(COLONfollow) + val tp = fromWithinQualifiedType(typ()) + accept(WITH) + val qualifier = block(simplify = true) + accept(RBRACE) + QualifiedTypeTree(tp, Some(id), qualifier).withSpan(Span(startOffset, qualifier.span.end)) + + /** `with` PostfixExpr + */ + def qualifiedTypeShort(t: Tree): Tree = + if inQualifiedType then + t + else + accept(WITH) + val qualifier = postfixExpr() + QualifiedTypeTree(t, None, qualifier).withSpan(Span(t.span.start, qualifier.span.end)) + + /** TypeBounds ::= [`>:' TypeBound ] [`<:' TypeBound ] * TypeBound ::= Type * | CaptureSet -- under captureChecking @@ -2289,7 +2333,12 @@ object Parsers { def typeDependingOn(location: Location): Tree = if location.inParens then typ() - else if location.inPattern then rejectWildcardType(refinedType()) + else if location.inPattern then + val t = rejectWildcardType(refinedType()) + if in.featureEnabled(Feature.qualifiedTypes) && in.token == WITH then + qualifiedTypeShort(t) + else + t else infixType() /* ----------- EXPRESSIONS ------------------------------------------------ */ @@ -3131,10 +3180,11 @@ object Parsers { if (isIdent(nme.raw.BAR)) { in.nextToken(); pattern1(location) :: patternAlts(location) } else Nil - /** Pattern1 ::= PatVar `:` RefinedType - * | [‘-’] integerLiteral `:` RefinedType - * | [‘-’] floatingPointLiteral `:` RefinedType - * | Pattern2 + /** Pattern1 ::= PatVar `:` QualifiedType3 + * | [‘-’] integerLiteral `:` QualifiedType3 + * | [‘-’] floatingPointLiteral `:` QualifiedType3 + * | Pattern2 + * QualifiedType3 ::= RefinedType [`with` PostfixExpr] */ def pattern1(location: Location = Location.InPattern): Tree = val p = pattern2(location) diff --git a/compiler/src/dotty/tools/dotc/printing/RefinedPrinter.scala b/compiler/src/dotty/tools/dotc/printing/RefinedPrinter.scala index 324d4f0c1d23..c2f22ddcaf2c 100644 --- a/compiler/src/dotty/tools/dotc/printing/RefinedPrinter.scala +++ b/compiler/src/dotty/tools/dotc/printing/RefinedPrinter.scala @@ -841,6 +841,10 @@ class RefinedPrinter(_ctx: Context) extends PlainPrinter(_ctx) { prefix ~~ idx.toString ~~ "|" ~~ tpeText ~~ "|" ~~ argsText ~~ "|" ~~ contentText ~~ postfix case CapturesAndResult(refs, parent) => changePrec(GlobalPrec)("^{" ~ Text(refs.map(toText), ", ") ~ "}" ~ toText(parent)) + case QualifiedTypeTree(parent, paramName, predicate) => + paramName match + case Some(name) => "{" ~ toText(name) ~ ": " ~ toText(parent) ~ " with " ~ toText(predicate) ~ "}" + case None => toText(parent) ~ " with " ~ toText(predicate) case ContextBoundTypeTree(tycon, pname, ownName) => toText(pname) ~ " : " ~ toText(tycon) ~ (" as " ~ toText(ownName) `provided` !ownName.isEmpty) case _ => diff --git a/compiler/src/dotty/tools/dotc/qualified_types/ArgRefType.scala b/compiler/src/dotty/tools/dotc/qualified_types/ArgRefType.scala new file mode 100644 index 000000000000..7e51ec8eaff3 --- /dev/null +++ b/compiler/src/dotty/tools/dotc/qualified_types/ArgRefType.scala @@ -0,0 +1,20 @@ +package dotty.tools.dotc.qualified_types +import dotty.tools.dotc.core.Types.{ + SingletonType, + CachedProxyType, + Type +} +import dotty.tools.dotc.core.Contexts.Context +import dotty.tools.dotc.core.Hashable.Binders + + +/** Reference to the argument of an [[ENode.Lambda]]. + * + * @param index  + * Debruijn index of the argument, starting from 0 + * @param underyling + * Underlying type of the argument + */ +final case class ArgRefType(index: Int, underlying: Type) extends CachedProxyType, SingletonType: + override def underlying(using Context): Type = underlying + override def computeHash(bs: Binders): Int = doHash(bs, index, underlying) diff --git a/compiler/src/dotty/tools/dotc/qualified_types/EGraph.scala b/compiler/src/dotty/tools/dotc/qualified_types/EGraph.scala new file mode 100644 index 000000000000..dcd6ad109be0 --- /dev/null +++ b/compiler/src/dotty/tools/dotc/qualified_types/EGraph.scala @@ -0,0 +1,441 @@ +package dotty.tools.dotc.qualified_types + +import scala.collection.mutable +import scala.collection.mutable.ArrayBuffer +import scala.collection.mutable.ListBuffer + +import dotty.tools.dotc.ast.tpd.{ + closureDef, + singleton, + Apply, + ConstantTree, + Ident, + Lambda, + Literal, + New, + Select, + This, + Tree, + TreeMap, + TreeOps, + TypeApply, + TypeTree +} +import dotty.tools.dotc.config.Printers +import dotty.tools.dotc.core.Constants.Constant +import dotty.tools.dotc.core.Contexts.Context +import dotty.tools.dotc.core.Contexts.ctx +import dotty.tools.dotc.core.Decorators.i +import dotty.tools.dotc.core.Flags +import dotty.tools.dotc.core.Hashable.Binders +import dotty.tools.dotc.core.Names.Designator +import dotty.tools.dotc.core.StdNames.nme +import dotty.tools.dotc.core.Symbols.{defn, NoSymbol, Symbol} +import dotty.tools.dotc.core.Types.{ + AppliedType, + CachedConstantType, + CachedProxyType, + ConstantType, + LambdaType, + MethodType, + NamedType, + NoPrefix, + SingletonType, + SkolemType, + TermParamRef, + TermRef, + Type, + TypeRef, + TypeVar, + ValueType +} +import dotty.tools.dotc.core.Uniques +import dotty.tools.dotc.qualified_types.ENode.Op +import dotty.tools.dotc.reporting.trace +import dotty.tools.dotc.transform.TreeExtractors.BinaryOp +import dotty.tools.dotc.util.{EqHashMap, HashMap} +import dotty.tools.dotc.util.Spans.Span + +import annotation.threadUnsafe as tu +import reflect.ClassTag + +object EGraph: + /** Are costly (non-constant-time) assertions enabled? */ + final val checksEnabled = true + +final class EGraph(rootContext: Context): + + /** Cache for unique E-Nodes + * + * Invariant: Each key is `eq` to its associated value. + * + * Invariant: If a node is in this map, then its children also are. + */ + private val index: HashMap[ENode, ENode] = HashMap() + + /** Map from nodes to their unique, canonical representations. + * + * Invariant: After a call to [[repair]], if a node is in the index but not + * in this map, then it is its own representant and it is canonical. + * + * Invariant: After a call to [[repair]], values of this map are canonical. + */ + private val representantOf: EqHashMap[ENode, ENode] = EqHashMap() + + /** Map from child nodes to their parent nodes + * + * Invariant: After a call to [[repair]], values of this map are canonical. + */ + private val usedBy: EqHashMap[ENode, mutable.Set[ENode]] = EqHashMap() + + /** Worklist for nodes that need to be repairedConstantType(Constant(value). + * + * This queue is filled by [[merge]] and processed by [[repair]]. + * + * Invariant: After a call to [[repair]], this queue is empty. + */ + private val worklist = mutable.Queue.empty[ENode] + + val trueNode: ENode.Atom = constant(true) + val falseNode: ENode.Atom = constant(false) + val minusOneIntNode: ENode.Atom = constant(-1) + val zeroIntNode: ENode.Atom = constant(0) + val oneIntNode: ENode.Atom = constant(1) + + /** Returns the canonical node for the given constant value */ + def constant(value: Any): ENode.Atom = + val node = ENode.Atom(ConstantType(Constant(value))(using rootContext)) + index.getOrElseUpdate(node, node).asInstanceOf[ENode.Atom] + + def registered(node: ENode): Boolean = + index.contains(node) + + /** Adds the given node to the E-Graph, returning its canonical representant. + * + * Pre-condition: The node must be normalized, and its children must be + * canonical. + */ + private def unique(node: ENode): ENode = + if index.contains(node) then + representant(index(node)) + else + index.update(node, node) + node match + case ENode.Atom(tp) => + () + case ENode.Constructor(sym) => + () + case ENode.Select(qual, member) => + addUse(qual, node) + case ENode.Apply(fn, args) => + addUse(fn, node) + for arg <- args do + addUse(arg, node) + case ENode.OpApply(op, args) => + for arg <- args do + addUse(arg, node) + case ENode.TypeApply(fn, args) => + addUse(fn, node) + case ENode.Lambda(paramTps, retTp, body) => + addUse(body, node) + node + node + + private def representant(node: ENode): ENode = + representantOf.get(node) match + case None => node + case Some(repr) => + // There must be no cycles in the `representantOf` map. + // If a node is canonical, it must have no representant. + assert(repr ne node, s"Node $node has itself as representant ($repr)") + representant(repr) + + def assertCanonical(node: ENode): Unit = + if EGraph.checksEnabled then + // By the invariants, if a node is in the index (meaning it is tracked by + // this E-Graph), and has no representant, then it is itself a canonical + // node. We double-check by forcing a deep canonicalization. + assert(index.contains(node) && index(node) == node, s"Node $node is not unique in this E-Graph") + assert(!representantOf.contains(node), s"Node $node has a representant: ${representantOf(node)}") + val canonical = canonicalize(node) + assert(node eq canonical, s"Recanonicalization of $node did not return itself, but $canonical") + + private def addUse(child: ENode, parent: ENode): Unit = + usedBy.getOrElseUpdate(child, mutable.Set.empty) += parent + + override def toString(): String = + s"EGraph{\nindex = $index,\nrepresentantOf = $representantOf,\nusedBy = $usedBy,\nworklist = $worklist}\n" + + def toDot(): String = + val sb = new StringBuilder() + sb.append("digraph EGraph {\nnode [height=.1 shape=record]\n") + for node <- index.valuesIterator do + sb.append(node.toDot()) + for (node, repr) <- representantOf.iterator do + sb.append(s"${node.dotId()} -> ${repr.dotId()} [style=dotted]\n") + for (child, parents) <- usedBy.iterator do + for parent <- parents do + sb.append(s"${child.dotId()} -> ${parent.dotId()} [style=dashed]\n") + sb.append("}\n") + sb.toString() + + def equiv(node1: ENode, node2: ENode)(using Context): Boolean = + trace(i"EGraph.equiv", Printers.qualifiedTypes): + // val margin = ctx.base.indentTab * (ctx.base.indent) + // println(s"$margin node1: $node1\n$margin node2: $node2") + val repr1 = representant(node1) + val repr2 = representant(node2) + repr1 eq repr2 + + def merge(a: ENode, b: ENode): Unit = + //println(s"EGraph.merge $a and $b") + if EGraph.checksEnabled then + assert(index.contains(a) && index(a) == a, s"Node $a is not unique in this E-Graph") + assert(index.contains(b) && index(b) == b, s"Node $b is not unique in this E-Graph") + val aRepr = representant(a) + val bRepr = representant(b) + if aRepr eq bRepr then return + if EGraph.checksEnabled then + assert(aRepr != bRepr, s"$aRepr and $bRepr are `equals` but not `eq`") + + /// Update representantOf and usedBy maps + val (newRepr, oldRepr) = order(aRepr, bRepr) + representantOf(oldRepr) = newRepr + val oldUses = usedBy.getOrElse(oldRepr, mutable.Set.empty) + usedBy.getOrElseUpdate(newRepr, mutable.Set.empty) ++= oldUses + usedBy.remove(oldRepr) + + // Propagate truth values over disjunctions, conjunctions and equalities + oldRepr match + case ENode.OpApply(Op.And, args) if newRepr eq trueNode => + args.foreach(merge(_, trueNode)) + case ENode.OpApply(Op.Or, args) if newRepr eq falseNode => + args.foreach(merge(_, falseNode)) + case ENode.OpApply(Op.Equal, args) if newRepr eq trueNode => + merge(args(0), args(1)) + case _ => + () + + // Enqueue all nodes that use the oldRepr for repair + worklist.enqueueAll(oldUses) + + private def order(a: ENode, b: ENode): (ENode, ENode) = + (a, b) match + case (ENode.Atom(_: ConstantType), _) => (a, b) + case (_, ENode.Atom(_: ConstantType)) => (b, a) + case (_: ENode.Constructor, _) => (a, b) + case (_, _: ENode.Constructor) => (b, a) + case (_: ENode.Atom, _) => (a, b) + case (_, _: ENode.Atom) => (b, a) + case (_: ENode.Select, _) => (a, b) + case (_, _: ENode.Select) => (b, a) + case (_: ENode.Apply, _) => (a, b) + case (_, _: ENode.Apply) => (b, a) + case (_: ENode.TypeApply, _) => (a, b) + case (_, _: ENode.TypeApply) => (b, a) + case _ => (a, b) + + def repair(): Unit = + while !worklist.isEmpty do + val head = worklist.dequeue() + val headRepr = representant(head) + val headCanonical = canonicalize(head, deep = false) + if headRepr ne headCanonical then + merge(headRepr, headCanonical) + + assertInvariants() + + def assertInvariants(): Unit = + if EGraph.checksEnabled then + //println(s"EGraph.assertInvariants") + //println(toDot()) + + assert(worklist.isEmpty, "Worklist is not empty") + + // Check that all nodes in the index are canonical + for (node, node2) <- index.iterator do + assert(node eq node2, s"Key and value in index are not equal: $node ne $node2") + + val repr = representant(node) + assertCanonical(repr) + + def uses(node: ENode): mutable.Set[ENode] = + usedBy.getOrElse(node, mutable.Set.empty) + + node match + case ENode.Atom(tp) => () + case ENode.Constructor(sym) => () + case ENode.Select(qual, member) => + index.contains(qual) && uses(qual).contains(node) + case ENode.Apply(fn, args) => + index.contains(fn) && uses(fn).contains(node) + args.forall(arg => index.contains(arg) && uses(arg).contains(node)) + case ENode.OpApply(op, args) => + args.forall(arg => index.contains(arg) && uses(arg).contains(node)) + case ENode.TypeApply(fn, args) => + index.contains(fn) && uses(fn).contains(node) + case ENode.Lambda(paramTps, retTp, body) => + index.contains(body) && uses(body).contains(node) + + for (node, repr) <- representantOf.iterator do + assert(index.contains(node), s"Node $node is not in the index") + + for (child, parents) <- usedBy.iterator do + assertCanonical(child) + + // ----------------------------------- + // Canonicalization + // ----------------------------------- + + def canonicalize(node: ENode, deep: Boolean = true): ENode = + def recur(node: ENode): ENode = if deep then canonicalize(node, deep) else representant(node) + val res = representant(unique( + node match + case ENode.Atom(tp) => + node + case ENode.Constructor(sym) => + node + case ENode.Select(qual, member) => + normalizeSelect(recur(qual), member) + case ENode.Apply(fn, args) => + ENode.Apply(recur(fn), args.map(recur)) + case ENode.OpApply(op, args) => + normalizeOp(op, args.map(recur)) + case ENode.TypeApply(fn, args) => + ENode.TypeApply(recur(fn), args) + case ENode.Lambda(paramTps, retTp, body) => + ENode.Lambda(paramTps, retTp, recur(body)) + )) + //println(s"Canonicalized $node to $res") + res + + private def normalizeSelect(qual: ENode, member: Symbol): ENode = + getAppliedConstructor(qual) match + case Some(constr) => + val memberIndex = constr.fields.indexOf(member) + + if memberIndex >= 0 then + val args = getTermArguments(qual) + assert(args.size == constr.fields.size) + args(memberIndex) + else + ENode.Select(qual, member) + case None => + ENode.Select(qual, member) + + private def getAppliedConstructor(node: ENode): Option[ENode.Constructor] = + node match + case ENode.Apply(fn, args) => getAppliedConstructor(fn) + case ENode.TypeApply(fn, args) => getAppliedConstructor(fn) + case node: ENode.Constructor => Some(node) + case _ => None + + private def getTermArguments(node: ENode): List[ENode] = + node match + case ENode.Apply(fn, args) => getTermArguments(fn) ::: args + case ENode.TypeApply(fn, args) => getTermArguments(fn) + case _ => Nil + + private def normalizeOp(op: ENode.Op, args: List[ENode]): ENode = + val res = op match + case Op.Equal => + assert(args.size == 2, s"Expected 2 arguments for equality, got $args") + if args(0) eq args(1) then + trueNode + else ENode.OpApply(op, args.sortBy(_.hashCode())) + case Op.And => + assert(args.size == 2, s"Expected 2 arguments for conjunction, got $args") + if (args(0) eq falseNode) || (args(1) eq falseNode) then falseNode + else if args(0) eq trueNode then args(1) + else if args(1) eq trueNode then args(0) + else ENode.OpApply(op, args) + case Op.Or => + assert(args.size == 2, s"Expected 2 arguments for disjunction, got $args") + if (args(0) eq trueNode) || (args(1) eq trueNode) then trueNode + else if args(0) eq falseNode then args(1) + else if args(1) eq falseNode then args(0) + else ENode.OpApply(op, args) + case Op.IntSum => + val (const, nonConsts) = decomposeIntSum(args) + makeIntSum(const, nonConsts) + case Op.IntProduct => + val (consts, nonConsts) = decomposeIntProduct(args) + makeIntProduct(consts, nonConsts) + case Op.IntLessThan => constFoldBinaryOp[Int, Boolean](op, args, _ < _) + case Op.IntLessEqual => constFoldBinaryOp[Int, Boolean](op, args, _ <= _) + case Op.IntGreaterThan => constFoldBinaryOp[Int, Boolean](op, args, _ > _) + case Op.IntGreaterEqual => constFoldBinaryOp[Int, Boolean](op, args, _ >= _) + case _ => + ENode.OpApply(op, args) + res + + private def constFoldBinaryOp[T: ClassTag, S](op: ENode.Op, args: List[ENode], fn: (T, T) => S): ENode = + args match + case List(ENode.Atom(ConstantType(Constant(c1: T))), ENode.Atom(ConstantType(Constant(c2: T)))) => + constant(fn(c1, c2)) + case _ => + ENode.OpApply(op, args) + + private def decomposeIntProduct(args: List[ENode]): (Int, List[ENode]) = + val factors = + args.flatMap: + case ENode.OpApply(Op.IntProduct, innerFactors) => innerFactors + case arg => List(arg) + val (consts, nonConsts) = + factors.partitionMap: + case ENode.Atom(ConstantType(Constant(c: Int))) => Left(c) + case factor => Right(factor) + (consts.product, nonConsts.sortBy(_.hashCode())) + + private def makeIntProduct(const: Int, nonConsts: List[ENode]): ENode = + if const == 0 then + zeroIntNode + else if const == 1 then + if nonConsts.isEmpty then oneIntNode + else if nonConsts.size == 1 then nonConsts.head + else ENode.OpApply(Op.IntProduct, nonConsts) + else + val constNode = constant(const) + nonConsts match + case Nil => + constNode + //case List(ENode.OpApply(Op.IntSum, summands)) => + // ENode.OpApply( + // Op.IntSum, + // summands.map(summand => unique(makeIntProduct(const, List(summand)))) + // ) + case _ => + ENode.OpApply(Op.IntProduct, constNode :: nonConsts) + + private def decomposeIntSum(args: List[ENode]): (Int, List[ENode]) = + val summands: List[ENode] = + args.flatMap: + case ENode.OpApply(Op.IntSum, innerSummands) => innerSummands + case arg => List(arg) + val decomposed: List[(Int, List[ENode])] = + summands.map: + case ENode.OpApply(Op.IntProduct, args) => + args match + case ENode.Atom(ConstantType(Constant(const: Int))) :: nonConsts => (const, nonConsts) + case nonConsts => (1, nonConsts) + case ENode.Atom(ConstantType(Constant(const: Int))) => (const, Nil) + case other => (1, List(other)) + val grouped = decomposed.groupMapReduce(_._2)(_._1)(_ + _) + val const = grouped.getOrElse(Nil, 0) + val nonConsts = + grouped + .toList + .filter((nonConsts, const) => const != 0 && !nonConsts.isEmpty) + .sortBy((nonConsts, const) => nonConsts.hashCode()) + .map((nonConsts, const) => unique(makeIntProduct(const, nonConsts))) + (const, nonConsts) + + private def makeIntSum(const: Int, nonConsts: List[ENode]): ENode = + if const == 0 then + if nonConsts.isEmpty then zeroIntNode + else if nonConsts.size == 1 then nonConsts.head + else ENode.OpApply(Op.IntSum, nonConsts) + else + val constNode = constant(const) + if nonConsts.isEmpty then constNode + else ENode.OpApply(Op.IntSum, constNode :: nonConsts) diff --git a/compiler/src/dotty/tools/dotc/qualified_types/ENode.scala b/compiler/src/dotty/tools/dotc/qualified_types/ENode.scala new file mode 100644 index 000000000000..f55bfe287a37 --- /dev/null +++ b/compiler/src/dotty/tools/dotc/qualified_types/ENode.scala @@ -0,0 +1,428 @@ +package dotty.tools.dotc.qualified_types + +import dotty.tools.dotc.ast.{tpd, untpd} +import dotty.tools.dotc.config.Printers +import dotty.tools.dotc.config.Settings.Setting.value +import dotty.tools.dotc.core.Constants.Constant +import dotty.tools.dotc.core.Contexts.Context +import dotty.tools.dotc.core.Contexts.ctx +import dotty.tools.dotc.core.Decorators.i +import dotty.tools.dotc.core.Flags +import dotty.tools.dotc.core.Flags.EmptyFlags +import dotty.tools.dotc.core.Hashable.Binders +import dotty.tools.dotc.core.Names.Designator +import dotty.tools.dotc.core.Names.{termName, Name} +import dotty.tools.dotc.core.StdNames.nme +import dotty.tools.dotc.core.Symbols.{defn, NoSymbol, Symbol} +import dotty.tools.dotc.core.Symbols.Symbol +import dotty.tools.dotc.core.Types.{ + AppliedType, + CachedProxyType, + ConstantType, + ClassInfo, + LambdaType, + MethodType, + NamedType, + NoPrefix, + SingletonType, + SkolemType, + TermParamRef, + TermRef, + ThisType, + Type, + TypeMap, + TypeRef, + TypeVar, + ValueType +} +import dotty.tools.dotc.ast.tpd.TreeOps +import dotty.tools.dotc.qualified_types.ENode.Op +import dotty.tools.dotc.reporting.trace +import dotty.tools.dotc.transform.TreeExtractors.{BinaryOp, UnaryOp} +import dotty.tools.dotc.util.Spans.Span +import scala.collection.mutable.ListBuffer + +enum ENode: + import ENode.* + + case Atom(tp: SingletonType) + case Constructor(constr: Symbol)(val fields: List[Symbol]) + case Select(qual: ENode, member: Symbol) + case Apply(fn: ENode, args: List[ENode]) + case OpApply(fn: ENode.Op, args: List[ENode]) + case TypeApply(fn: ENode, args: List[Type]) + case Lambda(paramTps: List[ArgRefType], retTp: Type, body: ENode) + + override def toString(): String = + val res = + this match + case Atom(tp) => typeToString(tp) + case Constructor(constr) => s"new ${designatorToString(constr.lastKnownDenotation.owner)}" + case Select(qual, member) => s"$qual.${designatorToString(member)}" + case Apply(fn, args) => s"$fn(${args.mkString(", ")})" + case OpApply(op, args) => s"(${args.mkString(" " + op.operatorString() + " ")})" + case TypeApply(fn, args) => s"$fn[${args.map(typeToString).mkString(", ")}]" + case Lambda(paramTps, retTp, body) => s"${lambdaSignatureString(paramTps, retTp)} => $body" + s"<$res>#${System.identityHashCode(this).toHexString}>" + //res + + def dotId() = + "n" + System.identityHashCode(this).toHexString.substring(1) + + def toDot(): String = + val id = dotId() + val fields: List[ENode | String] = + this match + case Atom(tp) => this.toString :: Nil + case Constructor(constr) => this.toString :: Nil + case Select(qual, member) => qual :: designatorToString(member) :: Nil + case Apply(fn, args) => fn :: args + case OpApply(op, args) => op.operatorString() :: args + case TypeApply(fn, args) => this.toString :: Nil + case Lambda(paramTps, retTp, body) => lambdaSignatureString(paramTps, retTp) :: body :: Nil + val fieldStrings = + fields.zipWithIndex.map: (field, i) => + field match + case child: ENode => s"" + case str: String => str.replace("<", "\\<").replace(">", "\\>") + val nodeString = s"$id [label=\"${fieldStrings.mkString("|")}\"];\n" + val edgesString = + fields.zipWithIndex.map: (field, i) => + field match + case child: ENode => s"$id:p$i -> ${child.dotId()};\n" + case _ => "" + nodeString + edgesString.mkString + + def mapTypes(f: Type => Type)(using Context): ENode = + this match + case Atom(tp) => + val mappedTp = f(tp) + if mappedTp eq tp then this + else + mappedTp match + case mappedTp: SingletonType => Atom(mappedTp) + case _ => + //throw new Error(s"Warning: ENode Atom type mapped to non-singleton type: $mappedTp") + Atom(SkolemType(mappedTp)) + case Constructor(constr) => this + case Select(qual, member) => Select(qual.mapTypes(f), member) + case Apply(fn, args) => Apply(fn.mapTypes(f), args.map(_.mapTypes(f))) + case OpApply(op, args) => OpApply(op, args.map(_.mapTypes(f))) + case TypeApply(fn, args) => TypeApply(fn.mapTypes(f), args.map(f)) + case Lambda(paramTps, retTp, body) => + Lambda(paramTps.map(f(_).asInstanceOf[ArgRefType]), f(retTp), body.mapTypes(f)) + + def foreachType(f: Type => Unit)(using Context): Unit = + this match + case Atom(tp) => f(tp) + case Constructor(_) => () + case Select(qual, _) => qual.foreachType(f) + case Apply(fn, args) => + fn.foreachType(f) + args.foreach(_.foreachType(f)) + case OpApply(_, args) => args.foreach(_.foreachType(f)) + case TypeApply(fn, args) => + fn.foreachType(f) + args.foreach(f) + case Lambda(paramTps, retTp, body) => + paramTps.foreach(f) + f(retTp) + body.foreachType(f) + + // ----------------------------------- + // Conversion from E-Nodes to Trees + // ----------------------------------- + + private class SubstTypesMap + + def toTree(paramRefs: List[tpd.Tree] = Nil)(using Context): tpd.Tree = + this match + case Atom(tp) => + tp match + case ArgRefType(index, underlying) => + paramRefs(index) + case tp: TermParamRef => + untpd.Ident(tp.paramName).withType(tp) + case _ => + tpd.singleton(tp) + case Constructor(sym) => + val tycon = sym.owner.asClass.classDenot.classInfo.selfType + tpd.New(tycon).select(TermRef(tycon, sym)) + case Select(qual, member) => + qual.toTree(paramRefs).select(member) + case Apply(fn, args) => + tpd.Apply(fn.toTree(paramRefs), args.map(_.toTree(paramRefs))) + case OpApply(op, args) => + def unaryOp(symbol: Symbol): tpd.Tree = + args(0).toTree(paramRefs).select(symbol).appliedToNone + def binaryOp(symbol: Symbol): tpd.Tree = + args(0).toTree(paramRefs).select(symbol).appliedTo(args(1).toTree(paramRefs)) + op match + case Op.IntSum => + args.foldLeft(tpd.Literal(Constant(0)): tpd.Tree): (acc, arg) => + acc.select(defn.Int_+).appliedTo(arg.toTree(paramRefs)) + case Op.IntMinus => + binaryOp(defn.Int_-) + case Op.IntProduct => + args.foldLeft(tpd.Literal(Constant(1)): tpd.Tree): (acc, arg) => + acc.select(defn.Int_*).appliedTo(arg.toTree(paramRefs)) + case Op.LongSum => + ??? + case Op.LongMinus => + ??? + case Op.LongProduct => + ??? + case Op.Equal => + args(0).toTree(paramRefs).equal(args(1).toTree(paramRefs)) + case Op.NotEqual => + val lhs = args(0).toTree(paramRefs) + val rhs = args(1).toTree(paramRefs) + tpd.applyOverloaded(lhs, nme.NE, rhs :: Nil, Nil, defn.BooleanType) + case Op.Not => unaryOp(defn.Boolean_!) + case Op.And => binaryOp(defn.Boolean_&&) + case Op.Or => binaryOp(defn.Boolean_||) + case Op.IntLessThan => binaryOp(defn.Int_<) + case Op.IntLessEqual => binaryOp(defn.Int_<=) + case Op.IntGreaterThan => binaryOp(defn.Int_>) + case Op.IntGreaterEqual => binaryOp(defn.Int_>=) + case TypeApply(fn, args) => + tpd.TypeApply(fn.toTree(paramRefs), args.map(tpd.TypeTree(_, false))) + case Lambda(paramTps, retTp, body) => + val paramNames = paramTps.map(arg => termName("arg" + arg.index)) + val mt = MethodType(paramNames)( + mt => + // TODO(mbovel): subst ArgParamRefs + val newParamRefs = ListBuffer.from(paramRefs) + for paramTp <- paramTps do + newParamRefs += _.underlying.subst + paramTps.map(_.underlying), + mt => retTp + ) + tpd.Lambda(mt, myParamRefs => body.toTree(paramRefs ++ myParamRefs)) + + +object ENode: + private def paramToString(param: Type): String = + param match + case tp: ArgRefType => + s"arg${tp.index}: ${typeToString(tp.underlying)}" + case _ => + typeToString(param) + + private def typeToString(tp: Type): String = + tp match + case tp: NamedType => + val prefixString = if isEmptyPrefix(tp.prefix) then "" else typeToString(tp.prefix) + "." + prefixString + designatorToString(tp.designator) + s"#${System.identityHashCode(tp).toHexString}" + case tp: ConstantType => + val res = tp.value.value.toString + //s"${res}#${System.identityHashCode(tp).toHexString}" + res + case tp: SkolemType => + "(?" + tp.hashCode + ": " + typeToString(tp.info) + ")" + case tp: ThisType => + typeToString(tp.tref) + ".this" + case tp: TypeVar => + tp.origin.paramName.toString() + case tp: ArgRefType => + s"arg${tp.index}" + case tp: AppliedType => + val argsString = tp.args.map(typeToString).mkString(", ") + s"${typeToString(tp.tycon)}[$argsString]" + case _ => + tp.toString + + private def lambdaSignatureString(paramTps: List[ArgRefType], retTp: Type): String = + val paramsString = paramTps.map(paramToString).mkString(", ") + s"($paramsString) => ${typeToString(retTp)}" + + private def isEmptyPrefix(tp: Type): Boolean = + tp match + case tp: NoPrefix.type => + true + case tp: ThisType => + tp.tref.designator match + case d: Symbol => d.lastKnownDenotation.name.toTermName == nme.EMPTY_PACKAGE + case _ => false + case _ => false + + private def designatorToString(d: Designator): String = + d match + case d: Symbol => d.lastKnownDenotation.name.toString + case _ => d.toString + + enum Op: + case IntSum + case IntMinus + case IntProduct + case LongSum + case LongMinus + case LongProduct + case Equal + case NotEqual + case Not + case And + case Or + case IntLessThan + case IntLessEqual + case IntGreaterThan + case IntGreaterEqual + + def operatorString(): String = + this match + case IntSum => "+" + case IntMinus => "-" + case IntProduct => "*" + case LongSum => "+" + case LongMinus => "-" + case LongProduct => "*" + case Equal => "==" + case NotEqual => "!=" + case Not => "!" + case And => "&&" + case Or => "||" + case IntLessThan => "<" + case IntLessEqual => "<=" + case IntGreaterThan => ">" + case IntGreaterEqual => ">=" + + // ----------------------------------- + // Conversion from Trees to E-Nodes + // ----------------------------------- + + def fromTree( + tree: tpd.Tree, + paramSyms: List[Symbol] = Nil, + paramTps: List[ArgRefType] = Nil + )(using Context): Option[ENode] = + val d = defn // Need a stable path to match on `defn` members + + def mapType(tp: Type): Type = + normalizeType(tp.subst(paramSyms, paramTps)) + + def binaryOpNode(op: ENode.Op, lhs: tpd.Tree, rhs: tpd.Tree): Option[ENode] = + for + lhsNode <- fromTree(lhs, paramSyms, paramTps) + rhsNode <- fromTree(rhs, paramSyms, paramTps) + yield OpApply(op, List(lhsNode, rhsNode)) + + def unaryOpNode(op: ENode.Op, arg: tpd.Tree): Option[ENode] = + for argNode <- fromTree(arg, paramSyms, paramTps) yield + OpApply(op, List(argNode)) + + def isValidEqual(sym: Symbol, lhs: tpd.Tree, rhs: tpd.Tree): Boolean = + sym == defn.Int_== + || sym == defn.Boolean_== + || sym == defn.String_== + || sym.name == nme.EQ && { + val classSymbol = lhs.tpe.classSymbol + classSymbol.exists && (classSymbol == defn.StringClass || hasCaseClassEquals(classSymbol)) + } + + trace(i"ENode.fromTree $tree", Printers.qualifiedTypes): + tree match + case tpd.Literal(_) | tpd.Ident(_) | tpd.This(_) if tree.tpe.isInstanceOf[SingletonType] => + Some(Atom(mapType(tree.tpe).asInstanceOf[SingletonType])) + case tpd.Select(tpd.New(_), nme.CONSTRUCTOR) => + constructorNode(tree.symbol) + case tree: tpd.Select if isCaseClassApply(tree.symbol) => + constructorNode(tree.symbol.owner.linkedClass.primaryConstructor) + case tpd.Select(qual, name) => + for qualNode <- fromTree(qual, paramSyms, paramTps) yield + Select(qualNode, tree.symbol) + case BinaryOp(lhs, sym, rhs) if isValidEqual(sym, lhs, rhs) => binaryOpNode(ENode.Op.Equal, lhs, rhs) + case BinaryOp(lhs, d.Int_!= | d.Boolean_!= | d.String_!=, rhs) => binaryOpNode(ENode.Op.NotEqual, lhs, rhs) + case UnaryOp(d.Boolean_!, arg) => unaryOpNode(ENode.Op.Not, arg) + case BinaryOp(lhs, d.Boolean_&&, rhs) => binaryOpNode(ENode.Op.And, lhs, rhs) + case BinaryOp(lhs, d.Boolean_||, rhs) => binaryOpNode(ENode.Op.Or, lhs, rhs) + case BinaryOp(lhs, d.Int_+, rhs) => binaryOpNode(ENode.Op.IntSum, lhs, rhs) + case BinaryOp(lhs, d.Int_-, rhs) => binaryOpNode(ENode.Op.IntMinus, lhs, rhs) + case BinaryOp(lhs, d.Int_*, rhs) => binaryOpNode(ENode.Op.IntProduct, lhs, rhs) + case BinaryOp(lhs, d.Int_<, rhs) => binaryOpNode(ENode.Op.IntLessThan, lhs, rhs) + case BinaryOp(lhs, d.Int_<=, rhs) => binaryOpNode(ENode.Op.IntLessEqual, lhs, rhs) + case BinaryOp(lhs, d.Int_>, rhs) => binaryOpNode(ENode.Op.IntGreaterThan, lhs, rhs) + case BinaryOp(lhs, d.Int_>=, rhs) => binaryOpNode(ENode.Op.IntGreaterEqual, lhs, rhs) + case tpd.Apply(fun, args) => + for + funNode <- fromTree(fun, paramSyms, paramTps) + argsNodes <- args.map(fromTree(_, paramSyms, paramTps)).sequence + yield ENode.Apply(funNode, argsNodes) + case tpd.TypeApply(fun, args) => + for funNode <- fromTree(fun, paramSyms, paramTps) + yield ENode.TypeApply(funNode, args.map(tp => mapType(tp.tpe))) + case tpd.closureDef(defDef) => + defDef.symbol.info.dealias match + case mt: MethodType => + assert(defDef.termParamss.size == 1, "closures have a single parameter list, right?") + val myParamSyms: List[Symbol] = defDef.termParamss.head.map(_.symbol) + val myParamTps: ListBuffer[ArgRefType] = ListBuffer.empty + val paramTpsSize = paramTps.size + for myParamSym <- myParamSyms do + val underlying = mapType(myParamSym.info.subst(myParamSyms.take(myParamTps.size), myParamTps.toList)) + myParamTps += ArgRefType(paramTpsSize + myParamTps.size, underlying) + val myRetTp = mapType(defDef.tpt.tpe.subst(myParamSyms, myParamTps.toList)) + for body <- fromTree(defDef.rhs, myParamSyms ::: paramSyms, myParamTps.toList ::: paramTps) + yield ENode.Lambda(myParamTps.toList, myRetTp, body) + case _ => None + case _ => + None + + private def constructorNode(constr: Symbol)(using Context): Option[ENode.Constructor] = + val clazz = constr.owner + if hasCaseClassEquals(clazz) then + val isPrimaryConstructor = constr.denot.isPrimaryConstructor + val fieldsRaw = clazz.denot.asClass.paramAccessors.filter(isPrimaryConstructor && _.isStableMember) + val constrParams = constr.paramSymss.flatten.filter(_.isTerm) + val fields = constrParams.map(p => fieldsRaw.find(_.name == p.name).getOrElse(NoSymbol)) + Some(ENode.Constructor(constr)(fields)) + else + None + + private def hasCaseClassEquals(clazz: Symbol)(using Context): Boolean = + val equalsMethod = clazz.info.decls.lookup(nme.equals_) + val equalsNotOverriden = !equalsMethod.exists || equalsMethod.is(Flags.Synthetic) + clazz.isClass && clazz.is(Flags.Case) && equalsNotOverriden + + private def isCaseClassApply(meth: Symbol)(using Context): Boolean = + meth.name == nme.apply + && meth.flags.is(Flags.Synthetic) + && meth.owner.linkedClass.is(Flags.Case) + + def normalizeType(tp: Type)(using Context): Type = + tp match + case tp: TypeVar if tp.isPermanentlyInstantiated => + tp.permanentInst + case tp: NamedType => + if tp.symbol.isStatic then tp.symbol.termRef + else normalizeType(tp.prefix).select(tp.symbol) + case tp => tp + + def selfify(tree: tpd.Tree)(using Context): Option[ENode.Lambda] = + trace(i"ENode.selfify $tree", Printers.qualifiedTypes): + fromTree(tree) match + case Some(treeNode) => + val paramType = ArgRefType(0, tree.tpe) + Some(ENode.Lambda( + List(paramType), + defn.BooleanType, + OpApply(ENode.Op.Equal, List(treeNode, ENode.Atom(paramType))) + )) + case None => None + + + // ----------------------------------- + // Utils + // ----------------------------------- + + extension [T](xs: List[Option[T]]) + private def sequence: Option[List[T]] = + var result = List.newBuilder[T] + var current = xs + while current.nonEmpty do + current.head match + case Some(x) => + result += x + current = current.tail + case None => + return None + Some(result.result()) diff --git a/compiler/src/dotty/tools/dotc/qualified_types/QualifiedAnnotation.scala b/compiler/src/dotty/tools/dotc/qualified_types/QualifiedAnnotation.scala new file mode 100644 index 000000000000..38f130facf90 --- /dev/null +++ b/compiler/src/dotty/tools/dotc/qualified_types/QualifiedAnnotation.scala @@ -0,0 +1,58 @@ +package dotty.tools.dotc.qualified_types + +import dotty.tools.dotc.ast.tpd.Tree +import dotty.tools.dotc.core.Annotations.Annotation +import dotty.tools.dotc.core.Contexts.{ctx, Context} +import dotty.tools.dotc.core.Decorators.i +import dotty.tools.dotc.core.Symbols.defn +import dotty.tools.dotc.core.Types.{TermLambda, TermParamRef, Type, ConstantType, TypeMap} +import dotty.tools.dotc.printing.Printer +import dotty.tools.dotc.printing.Texts.Text +import dotty.tools.dotc.printing.Texts.stringToText +import dotty.tools.dotc.core.Constants.Constant +import dotty.tools.dotc.report + +case class QualifiedAnnotation(qualifier: ENode.Lambda) extends Annotation: + + override def tree(using Context): Tree = qualifier.toTree() + + override def symbol(using Context) = defn.QualifiedAnnot + + override def derivedAnnotation(tree: Tree)(using Context): Annotation = ??? + + private def derivedAnnotation(qualifier: ENode.Lambda)(using Context): Annotation = + if qualifier eq this.qualifier then this + else QualifiedAnnotation(qualifier) + + override def toText(printer: Printer): Text = + "with " ~ qualifier.body.toString() + + override def mapWith(tm: TypeMap)(using Context): Annotation = + derivedAnnotation(qualifier.mapTypes(tm).asInstanceOf[ENode.Lambda]) + + override def refersToParamOf(tl: TermLambda)(using Context): Boolean = + var res = false + qualifier.foreachType: tp => + tp.stripped match + case TermParamRef(tl1, _) if tl eq tl1 => res = true + case _ => () + res + +object QualifiedAnnotation: + def apply(annot: Annotation)(using Context): Annotation = + annot match + case QualifiedAnnotation(qualifier) => annot // Already a QualifiedAnnotation + case _ => + val arg = annot.arguments(0) + ENode.fromTree(arg) match + case Some(qualifier: ENode.Lambda) => QualifiedAnnotation(qualifier) + case _ => + report.error(i"Invalid qualifier: $arg", annot.tree.srcPos) + // Returns placeholder (arg0: Any) => true + QualifiedAnnotation( + ENode.Lambda( + List(ArgRefType(0, defn.AnyType)), + defn.BooleanType, + ENode.Atom(ConstantType(Constant(true))) + ): ENode.Lambda + ) diff --git a/compiler/src/dotty/tools/dotc/qualified_types/QualifiedType.scala b/compiler/src/dotty/tools/dotc/qualified_types/QualifiedType.scala new file mode 100644 index 000000000000..9f601b0558f5 --- /dev/null +++ b/compiler/src/dotty/tools/dotc/qualified_types/QualifiedType.scala @@ -0,0 +1,28 @@ +package dotty.tools.dotc.qualified_types + +import dotty.tools.dotc.ast.tpd +import dotty.tools.dotc.core.Annotations.Annotation +import dotty.tools.dotc.core.Contexts.{ctx, Context} +import dotty.tools.dotc.core.Types.{AnnotatedType, Type} + +/** A qualified type is internally represented as a type annotated with a + * `@qualified` annotation. + */ +object QualifiedType: + /** Extractor for qualified types. + * + * @param tp + * the type to deconstruct + * @return + * a pair containing the parent type and the qualifier tree (a lambda) on + * success, [[None]] otherwise + */ + def unapply(tp: Type)(using Context): Option[(Type, ENode.Lambda)] = + tp match + case AnnotatedType(parent, QualifiedAnnotation(qualifier)) => + Some((parent, qualifier)) + case _ => + None + + def apply(parent: Type, qualifier: ENode.Lambda)(using Context): Type = + AnnotatedType(parent, QualifiedAnnotation(qualifier)) diff --git a/compiler/src/dotty/tools/dotc/qualified_types/QualifiedTypes.scala b/compiler/src/dotty/tools/dotc/qualified_types/QualifiedTypes.scala new file mode 100644 index 000000000000..6942c8992bc0 --- /dev/null +++ b/compiler/src/dotty/tools/dotc/qualified_types/QualifiedTypes.scala @@ -0,0 +1,134 @@ +package dotty.tools.dotc.qualified_types + +import dotty.tools.dotc.ast.tpd +import dotty.tools.dotc.ast.tpd.{ + Apply, + Block, + EmptyTree, + Ident, + If, + Lambda, + Literal, + New, + Select, + SeqLiteral, + This, + Throw, + Tree, + TypeApply, + Typed, + given +} +import dotty.tools.dotc.config.Printers +import dotty.tools.dotc.core.Atoms +import dotty.tools.dotc.core.Constants.Constant +import dotty.tools.dotc.core.Contexts.{ctx, Context} +import dotty.tools.dotc.core.Decorators.{em, i, toTermName} +import dotty.tools.dotc.core.StdNames.nme +import dotty.tools.dotc.core.Symbols.{defn, Symbol} +import dotty.tools.dotc.core.Types.{ + AndType, + ConstantType, + ErrorType, + MethodType, + OrType, + SkolemType, + TermRef, + Type, + TypeProxy +} +import dotty.tools.dotc.util.SrcPos +import dotty.tools.dotc.report +import dotty.tools.dotc.reporting.trace + +object QualifiedTypes: + /** Does the type `tp1` imply the qualifier `qualifier2`? + * + * Used by [[dotty.tools.dotc.core.TypeComparer]] to compare qualified types. + * + * Note: the logic here is similar to [[Type#derivesAnnotWith]] but + * additionally handle comparisons with [[SingletonType]]s. + */ + def typeImplies(tp1: Type, qualifier2: ENode.Lambda)(using Context): Boolean = + val solver = QualifierSolver() + def trySelfifyType() = + val ENode.Lambda(List(paramTp), _, _) = qualifier2: @unchecked + ENode.selfify(tpd.singleton(tp1)) match + case Some(qualifier1) => solver.implies(qualifier1, qualifier2) + case None => false + trace(i"typeImplies $tp1 --> $qualifier2", Printers.qualifiedTypes): + tp1 match + case QualifiedType(parent1, qualifier1) => + solver.implies(qualifier1, qualifier2) + case tp1: TermRef => + typeImplies(tp1.underlying, qualifier2) || trySelfifyType() + case tp1: ConstantType => + trySelfifyType() + case tp1: TypeProxy => + typeImplies(tp1.underlying, qualifier2) + case AndType(tp11, tp12) => + typeImplies(tp11, qualifier2) || typeImplies(tp12, qualifier2) + case OrType(tp11, tp12) => + typeImplies(tp11, qualifier2) && typeImplies(tp12, qualifier2) + case _ => + false + + /** Try to adapt the tree to the given type `pt` + * + * Returns [[EmptyTree]] if `pt` does not contain qualifiers or if the tree + * cannot be adapted, or the adapted tree otherwise. + * + * Used by [[dotty.tools.dotc.core.Typer]]. + */ + def adapt(tree: Tree, pt: Type)(using Context): Tree = + if containsQualifier(pt) then + trace(i"adapt $tree to qualified type $pt", Printers.qualifiedTypes): + if tree.tpe.hasAnnotation(defn.RuntimeCheckedAnnot) then + if checkContainsSkolem(pt, tree.srcPos) then + tpd.evalOnce(tree): e => + If( + e.isInstance(pt), + e.asInstance(pt), + Throw(New(defn.IllegalArgumentExceptionType, List())) + ) + else + tree.withType(ErrorType(em"")) + else + ENode.selfify(tree) match + case Some(qualifier) => + val selfifiedTp = QualifiedType(tree.tpe, qualifier) + if selfifiedTp <:< pt then tree.cast(selfifiedTp) else EmptyTree + case None => + EmptyTree + else + EmptyTree + + def isSimple(tree: Tree)(using Context): Boolean = + tree match + case Apply(fn, args) => isSimple(fn) && args.forall(isSimple) + case TypeApply(fn, args) => isSimple(fn) + case SeqLiteral(elems, _) => elems.forall(isSimple) + case Typed(expr, _) => isSimple(expr) + case Block(Nil, expr) => isSimple(expr) + case _ => tpd.isIdempotentExpr(tree) + + def containsQualifier(tp: Type)(using Context): Boolean = + tp match + case QualifiedType(_, _) => true + case tp: TypeProxy => containsQualifier(tp.underlying) + case AndType(tp1, tp2) => containsQualifier(tp1) || containsQualifier(tp2) + case OrType(tp1, tp2) => containsQualifier(tp1) || containsQualifier(tp2) + case _ => false + + def checkContainsSkolem(tp: Type, pos: SrcPos)(using Context): Boolean = + var res = true + tp.foreachPart: + case QualifiedType(_, qualifier) => + qualifier.foreachType: rootTp => + rootTp.foreachPart: + case tp: SkolemType => + report.error(em"The qualified type $qualifier cannot be checked at runtime", pos) + res = false + case _ => () + case _ => () + res diff --git a/compiler/src/dotty/tools/dotc/qualified_types/QualifierEvaluator.scala b/compiler/src/dotty/tools/dotc/qualified_types/QualifierEvaluator.scala new file mode 100644 index 000000000000..ce20a38aee4a --- /dev/null +++ b/compiler/src/dotty/tools/dotc/qualified_types/QualifierEvaluator.scala @@ -0,0 +1,96 @@ +package dotty.tools.dotc.qualified_types + +import scala.annotation.tailrec + +import dotty.tools.dotc.ast.tpd.{ + Apply, + Block, + ConstantTree, + isIdempotentExpr, + EmptyTree, + Literal, + Ident, + Match, + Select, + This, + Tree, + TreeMap, + ValDef, + given +} +import dotty.tools.dotc.core.Constants.Constant +import dotty.tools.dotc.core.Contexts.Context +import dotty.tools.dotc.core.Decorators.i +import dotty.tools.dotc.core.Flags +import dotty.tools.dotc.core.Mode.Type +import dotty.tools.dotc.core.StdNames.nme +import dotty.tools.dotc.core.Symbols.{defn, NoSymbol, Symbol} +import dotty.tools.dotc.core.SymDenotations.given +import dotty.tools.dotc.core.Types.{ConstantType, NoPrefix, TermRef} +import dotty.tools.dotc.inlines.InlineReducer +import dotty.tools.dotc.transform.TreeExtractors.BinaryOp +import dotty.tools.dotc.transform.patmat.{Empty as EmptySpace, SpaceEngine} +import dotty.tools.dotc.typer.Typer +import scala.util.boundary +import scala.util.boundary.break + + +import dotty.tools.dotc.reporting.trace +import dotty.tools.dotc.config.Printers + +private[qualified_types] object QualifierEvaluator: + /** Reduces a tree by constant folding, simplification and unfolding of simple + * references. + * + * This is more aggressive than [[dotty.tools.dotc.transform.BetaReduce]] and + * [[dotty.tools.dotc.typer.ConstFold]] (which is used under the hood by + * `BetaReduce` through [[dotty.tools.dotc.ast.tpd.cpy]]), as it also unfolds + * non-constant expressions. + */ + def evaluate(tree: Tree, args: Map[Symbol, Tree] = Map.empty)(using Context): Tree = + trace(i"evaluate $tree", Printers.qualifiedTypes): + QualifierEvaluator(args).transform(tree) + +private class QualifierEvaluator(args: Map[Symbol, Tree]) extends TreeMap: + import QualifierEvaluator.* + + override def transform(tree: Tree)(using Context): Tree = + unfold(reduce(tree)) + + private def reduce(tree: Tree)(using Context): Tree = + tree match + case tree: Apply => + val treeTransformed = super.transform(tree) + constFold(treeTransformed).orElse(treeTransformed) + case tree: Select => + val treeTransformed = super.transform(tree) + constFold(treeTransformed).orElse(treeTransformed) + case Block(Nil, expr) => + transform(expr) + case tree => + super.transform(tree) + + private def constFold(tree: Tree)(using Context): Tree = + tree match + case ConstantTree(c: Constant) => Literal(c) + case _ => EmptyTree + + private def unfold(tree: Tree)(using Context): Tree = + args.get(tree.symbol) match + case Some(tree2) => + return transform(tree2) + case None => () + + tree match + case tree: Ident => + trace(s"unfold $tree", Printers.qualifiedTypes): + tree.symbol.defTree match + case valDef: ValDef + if !valDef.rhs.isEmpty + && !valDef.symbol.is(Flags.Lazy) + && QualifiedTypes.isSimple(valDef.rhs) => + transform(valDef.rhs) + case _ => + tree + case _ => + tree diff --git a/compiler/src/dotty/tools/dotc/qualified_types/QualifierSolver.scala b/compiler/src/dotty/tools/dotc/qualified_types/QualifierSolver.scala new file mode 100644 index 000000000000..6ecdc85c53a3 --- /dev/null +++ b/compiler/src/dotty/tools/dotc/qualified_types/QualifierSolver.scala @@ -0,0 +1,61 @@ +package dotty.tools.dotc.qualified_types + +import ENode.{Lambda, OpApply, Op} + +import dotty.tools.dotc.config.Printers +import dotty.tools.dotc.reporting.trace +import dotty.tools.dotc.core.Contexts.{ctx, Context} +import dotty.tools.dotc.core.Symbols.defn +import dotty.tools.dotc.core.Types.{Type, TypeVar, TypeMap} + +class QualifierSolver(using Context): + + def implies(node1: ENode.Lambda, node2: ENode.Lambda) = + trace(s"QualifierSolver.implies ${node1.body} -> ${node2.body}", Printers.qualifiedTypes): + val paramTp1 = node1.paramTps.head + val paramTp2 = node2.paramTps.head + if paramTp1.underlying frozen_<:< paramTp2.underlying then + impliesRec( + node1.body.mapTypes(SubstTypeMap(paramTp1, paramTp2)), + node2.body.mapTypes(InstantiateMap()) + ) + else if paramTp1.underlying frozen_<:< paramTp2.underlying then + impliesRec( + node1.body.mapTypes(InstantiateMap()), + node2.body.mapTypes(SubstTypeMap(paramTp2, paramTp1)) + ) + else + false + + /** Instantiates type variables when possible. + * + * See `TypeMap.mapOverTypeVar`. + */ + private class InstantiateMap extends TypeMap: + override def mapOverTypeVar(tp: TypeVar): Type = + val res = super.mapOverTypeVar(tp) + if res eq tp then res else ENode.normalizeType(res) + + def apply(tp: Type): Type = mapOver(tp) + + private class SubstTypeMap(from: Type, to: Type) extends InstantiateMap: + override def apply(tp: Type): Type = if tp eq from then to else mapOver(tp) + + private def impliesRec(node1: ENode, node2: ENode): Boolean = + node1 match + case OpApply(Op.Or, List(lhs, rhs)) => + return impliesRec(lhs, node2) && impliesRec(rhs, node2) + case _ => () + + node2 match + case OpApply(Op.And, List(lhs, rhs)) => + return impliesRec(node1, lhs) && impliesRec(node1, rhs) + case _ => () + + val egraph = EGraph(ctx) + val canonicalNode1 = egraph.canonicalize(node1) + val canonicalNode2 = egraph.canonicalize(node2) + trace(s"QualifierSolver.impliesRec $canonicalNode1 -> $canonicalNode2", Printers.qualifiedTypes): + egraph.merge(canonicalNode1, egraph.trueNode) + egraph.repair() + egraph.equiv(canonicalNode2, egraph.trueNode) diff --git a/compiler/src/dotty/tools/dotc/transform/BetaReduce.scala b/compiler/src/dotty/tools/dotc/transform/BetaReduce.scala index 16219055b8c0..30095b99b1c5 100644 --- a/compiler/src/dotty/tools/dotc/transform/BetaReduce.scala +++ b/compiler/src/dotty/tools/dotc/transform/BetaReduce.scala @@ -175,4 +175,14 @@ object BetaReduce: Some(expansion1) else None end reduceApplication -end BetaReduce \ No newline at end of file + + def reduceApplication(ddef: DefDef, argss: List[List[Tree]])(using Context): Option[Tree] = + val bindings = new ListBuffer[DefTree]() + reduceApplication(ddef, argss, bindings) match + case Some(expansion1) => + val bindings1 = bindings.result() + Some(seq(bindings1, expansion1)) + case None => + None + +end BetaReduce diff --git a/compiler/src/dotty/tools/dotc/transform/Erasure.scala b/compiler/src/dotty/tools/dotc/transform/Erasure.scala index 25239aee59cf..3d86b5560be0 100644 --- a/compiler/src/dotty/tools/dotc/transform/Erasure.scala +++ b/compiler/src/dotty/tools/dotc/transform/Erasure.scala @@ -614,7 +614,7 @@ object Erasure { } override def promote(tree: untpd.Tree)(using Context): tree.ThisTree[Type] = { - assert(tree.hasType) + assert(tree.hasType, i"promote called on tree without type: ${tree.show}") val erasedTp = erasedType(tree) report.log(s"promoting ${tree.show}: ${erasedTp.showWithUnderlying()}") tree.withType(erasedTp) diff --git a/compiler/src/dotty/tools/dotc/transform/InlinePatterns.scala b/compiler/src/dotty/tools/dotc/transform/InlinePatterns.scala index d2a72e10fcfc..aa470a0e9ea8 100644 --- a/compiler/src/dotty/tools/dotc/transform/InlinePatterns.scala +++ b/compiler/src/dotty/tools/dotc/transform/InlinePatterns.scala @@ -59,12 +59,7 @@ class InlinePatterns extends MiniPhase: case Block(TypeDef(_, template: Template) :: Nil, Apply(Select(New(_),_), Nil)) if template.constr.rhs.isEmpty => template.body match case List(ddef @ DefDef(`name`, _, _, _)) => - val bindings = new ListBuffer[DefTree]() - BetaReduce.reduceApplication(ddef, argss, bindings) match - case Some(expansion1) => - val bindings1 = bindings.result() - seq(bindings1, expansion1) - case None => tree + BetaReduce.reduceApplication(ddef, argss).getOrElse(tree) case _ => tree case _ => tree diff --git a/compiler/src/dotty/tools/dotc/transform/PostTyper.scala b/compiler/src/dotty/tools/dotc/transform/PostTyper.scala index f970e75177e3..861ad81cb990 100644 --- a/compiler/src/dotty/tools/dotc/transform/PostTyper.scala +++ b/compiler/src/dotty/tools/dotc/transform/PostTyper.scala @@ -22,6 +22,7 @@ import NameKinds.WildcardParamName import cc.* import dotty.tools.dotc.transform.MacroAnnotations.hasMacroAnnotation import dotty.tools.dotc.core.NameKinds.DefaultGetterName +import dotty.tools.dotc.qualified_types.QualifiedAnnotation object PostTyper { val name: String = "posttyper" @@ -201,11 +202,15 @@ class PostTyper extends MacroTransform with InfoTransformer { thisPhase => } private def transformAnnot(annot: Annotation)(using Context): Annotation = - val tree1 = - annot match - case _: BodyAnnotation => annot.tree - case _ => copySymbols(annot.tree) - annot.derivedAnnotation(transformAnnotTree(tree1)) + annot match + case _: QualifiedAnnotation => + annot + case _ => + val tree1 = + annot match + case _: BodyAnnotation => annot.tree + case _ => copySymbols(annot.tree) + annot.derivedAnnotation(transformAnnotTree(tree1)) /** Transforms all annotations in the given type. */ private def transformAnnotsIn(using Context) = diff --git a/compiler/src/dotty/tools/dotc/transform/TreeExtractors.scala b/compiler/src/dotty/tools/dotc/transform/TreeExtractors.scala index 8d5b7c28bbbc..9ec91c79abad 100644 --- a/compiler/src/dotty/tools/dotc/transform/TreeExtractors.scala +++ b/compiler/src/dotty/tools/dotc/transform/TreeExtractors.scala @@ -19,6 +19,15 @@ object TreeExtractors { } } + /** Match arg.op() and extract (arg, op.symbol) */ + object UnaryOp: + def unapply(t: Tree)(using Context): Option[(Symbol, Tree)] = + t match + case Apply(sel @ Select(arg, _), Nil) => + Some((sel.symbol, arg)) + case _ => + None + /** Match new C(args) and extract (C, args). * Also admit new C(args): T and {new C(args)}. */ diff --git a/compiler/src/dotty/tools/dotc/transform/TypeTestsCasts.scala b/compiler/src/dotty/tools/dotc/transform/TypeTestsCasts.scala index a8c8ec8ce1d8..26364298e41f 100644 --- a/compiler/src/dotty/tools/dotc/transform/TypeTestsCasts.scala +++ b/compiler/src/dotty/tools/dotc/transform/TypeTestsCasts.scala @@ -18,6 +18,8 @@ import config.Printers.{ transforms => debug } import patmat.Typ import dotty.tools.dotc.util.SrcPos +import qualified_types.QualifiedType + /** This transform normalizes type tests and type casts, * also replacing type tests with singleton argument type with reference equality check * Any remaining type tests @@ -323,7 +325,7 @@ object TypeTestsCasts { * The transform happens before erasure of `testType`, thus cannot be merged * with `transformIsInstanceOf`, which depends on erased type of `testType`. */ - def transformTypeTest(expr: Tree, testType: Type, flagUnrelated: Boolean): Tree = testType.dealias match { + def transformTypeTest(expr: Tree, testType: Type, flagUnrelated: Boolean): Tree = testType.dealiasKeepRefiningAnnots match { case tref: TermRef if tref.symbol == defn.EmptyTupleModule => ref(defn.RuntimeTuples_isInstanceOfEmptyTuple).appliedTo(expr) case _: SingletonType => @@ -352,6 +354,16 @@ object TypeTestsCasts { ref(defn.RuntimeTuples_isInstanceOfNonEmptyTuple).appliedTo(expr) case AppliedType(tref: TypeRef, _) if tref.symbol == defn.PairClass => ref(defn.RuntimeTuples_isInstanceOfNonEmptyTuple).appliedTo(expr) + case QualifiedType(parent, qualifier) => + qualifier.toTree() match + case closureDef(qualifierDef) => + evalOnce(expr): e => + // e.isInstanceOf[baseType] && qualifier(e.asInstanceOf[baseType]) + val arg = e.asInstance(parent) + val qualifierTest = BetaReduce.reduceApplication(qualifierDef, List(List(arg))).get + transformTypeTest(e, parent, flagUnrelated).and(qualifierTest) + case tree => + throw new IllegalStateException("Malformed qualifier tree: $tree, expected a closure definition") case _ => val testWidened = testType.widen defn.untestableClasses.find(testWidened.isRef(_)) match diff --git a/compiler/src/dotty/tools/dotc/typer/ConstFold.scala b/compiler/src/dotty/tools/dotc/typer/ConstFold.scala index bd726afe5bba..76c36bfe5c2c 100644 --- a/compiler/src/dotty/tools/dotc/typer/ConstFold.scala +++ b/compiler/src/dotty/tools/dotc/typer/ConstFold.scala @@ -61,17 +61,6 @@ object ConstFold: tree.withFoldedType(Constant(targ.tpe)) case _ => tree - private object ConstantTree: - def unapply(tree: Tree)(using Context): Option[Constant] = - tree match - case Inlined(_, Nil, expr) => unapply(expr) - case Typed(expr, _) => unapply(expr) - case Literal(c) if c.tag == Constants.NullTag => Some(c) - case _ => - tree.tpe.widenTermRefExpr.normalized.simplified match - case ConstantType(c) => Some(c) - case _ => None - extension [T <: Tree](tree: T)(using Context) private def withFoldedType(c: Constant | Null): T = if c == null then tree else tree.withType(ConstantType(c)).asInstanceOf[T] diff --git a/compiler/src/dotty/tools/dotc/typer/ImportInfo.scala b/compiler/src/dotty/tools/dotc/typer/ImportInfo.scala index 78cba674bfff..16f690790f80 100644 --- a/compiler/src/dotty/tools/dotc/typer/ImportInfo.scala +++ b/compiler/src/dotty/tools/dotc/typer/ImportInfo.scala @@ -206,7 +206,7 @@ class ImportInfo(symf: Context ?=> Symbol, /** Does this import clause or a preceding import clause enable `feature`? * - * @param feature a possibly quailified name, e.g. + * @param feature a possibly qualified name, e.g. * strictEquality * experimental.genericNumberLiterals * diff --git a/compiler/src/dotty/tools/dotc/typer/Typer.scala b/compiler/src/dotty/tools/dotc/typer/Typer.scala index 30d3add2529f..b8fd8d0786a9 100644 --- a/compiler/src/dotty/tools/dotc/typer/Typer.scala +++ b/compiler/src/dotty/tools/dotc/typer/Typer.scala @@ -47,6 +47,7 @@ import reporting.* import Nullables.* import NullOpsDecorator.* import cc.{CheckCaptures, isRetainsLike} +import qualified_types.QualifiedTypes import config.Config import config.MigrationVersion import transform.CheckUnused.OriginalName @@ -2267,9 +2268,10 @@ class Typer(@constructorOnly nestingLevel: Int = 0) extends Namer } /** Type a case. */ - def typedCase(tree: untpd.CaseDef, sel: Tree, wideSelType: Type, pt: Type)(using Context): CaseDef = { + def typedCase(tree0: untpd.CaseDef, sel: Tree, wideSelType: Type, pt: Type)(using Context): CaseDef = { val originalCtx = ctx val gadtCtx: Context = ctx.fresh.setFreshGADTBounds + val tree = desugar.caseDef(tree0) def caseRest(pat: Tree)(using Context) = { val pt1 = instantiateMatchTypeProto(pat, pt) match { @@ -2503,7 +2505,7 @@ class Typer(@constructorOnly nestingLevel: Int = 0) extends Namer // untyped tree is no longer accessed after all // accesses with typedTypeTree are done. case None => - errorTree(tree, em"Something's wrong: missing original symbol for type tree") + errorTree(tree, em"Something's wrong: missing original symbol for type tree ${tree}") } case _ => completeTypeTree(InferredTypeTree(), pt, tree) @@ -4719,7 +4721,7 @@ class Typer(@constructorOnly nestingLevel: Int = 0) extends Namer return readapt(tree.cast(captured)) // drop type if prototype is Unit - if (pt isRef defn.UnitClass) { + if (pt.isRef(defn.UnitClass, false)) { // local adaptation makes sure every adapted tree conforms to its pt // so will take the code path that decides on inlining val tree1 = adapt(tree, WildcardType, locked) @@ -4771,6 +4773,11 @@ class Typer(@constructorOnly nestingLevel: Int = 0) extends Namer case _ => case _ => + // Try to adapt to a qualified type + val adapted = QualifiedTypes.adapt(tree, pt) + if !adapted.isEmpty then + return readapt(adapted) + def recover(failure: SearchFailureType) = if canDefineFurther(wtp) || canDefineFurther(pt) then readapt(tree) else diff --git a/compiler/src/dotty/tools/dotc/util/EqHashMap.scala b/compiler/src/dotty/tools/dotc/util/EqHashMap.scala index 25d9fb2907b8..4bb079963f5e 100644 --- a/compiler/src/dotty/tools/dotc/util/EqHashMap.scala +++ b/compiler/src/dotty/tools/dotc/util/EqHashMap.scala @@ -93,4 +93,13 @@ extends GenericHashMap[Key, Value](initialCapacity, capacityMultiple): val key = oldTable(idx).asInstanceOf[Key] if key != null then addOld(key, oldTable(idx + 1).asInstanceOf[Value]) idx += 2 + + override def clone(): EqHashMap[Key, Value] = + // Inverse from the computation of `limit` in `allocate`. + val initialCapacity = if isDense then limit + 1 else limit * capacityMultiple + val cloned = EqHashMap[Key, Value](initialCapacity, capacityMultiple) + cloned.copyFrom(table) + cloned.used = used + cloned + end EqHashMap diff --git a/compiler/src/dotty/tools/dotc/util/HashMap.scala b/compiler/src/dotty/tools/dotc/util/HashMap.scala index eec3a604b5e2..4918e4371191 100644 --- a/compiler/src/dotty/tools/dotc/util/HashMap.scala +++ b/compiler/src/dotty/tools/dotc/util/HashMap.scala @@ -98,4 +98,13 @@ extends GenericHashMap[Key, Value](initialCapacity, capacityMultiple): val key = oldTable(idx).asInstanceOf[Key] if key != null then addOld(key, oldTable(idx + 1).asInstanceOf[Value]) idx += 2 + + override def clone(): HashMap[Key, Value] = + // Inverse from the computation of `limit` in `allocate`. + val initialCapacity = if isDense then limit + 1 else limit * capacityMultiple + val cloned = HashMap[Key, Value](initialCapacity, capacityMultiple) + cloned.copyFrom(table) + cloned.used = used + cloned + end HashMap diff --git a/compiler/test/dotty/tools/dotc/CompilationTests.scala b/compiler/test/dotty/tools/dotc/CompilationTests.scala index 2d2f01388374..afed7b83ba5b 100644 --- a/compiler/test/dotty/tools/dotc/CompilationTests.scala +++ b/compiler/test/dotty/tools/dotc/CompilationTests.scala @@ -38,6 +38,7 @@ class CompilationTests { compileFile("tests/pos-special/sourcepath/outer/nested/Test4.scala", defaultOptions.and("-sourcepath", "tests/pos-special/sourcepath")), compileFilesInDir("tests/pos-scala2", defaultOptions.and("-source", "3.0-migration")), compileFilesInDir("tests/pos-custom-args/captures", defaultOptions.and("-language:experimental.captureChecking", "-source", "3.8")), + compileFilesInDir("tests/pos-custom-args/qualified-types", defaultOptions.and("-language:experimental.qualifiedTypes")), compileFile("tests/pos-special/utf8encoded.scala", defaultOptions.and("-encoding", "UTF8")), compileFile("tests/pos-special/utf16encoded.scala", defaultOptions.and("-encoding", "UTF16")), compileDir("tests/pos-special/i18589", defaultOptions.and("-Wsafe-init").without("-Ycheck:all")), @@ -147,6 +148,7 @@ class CompilationTests { compileFilesInDir("tests/neg", defaultOptions, FileFilter.exclude(TestSources.negScala2LibraryTastyExcludelisted)), compileFilesInDir("tests/neg-deep-subtype", allowDeepSubtypes), compileFilesInDir("tests/neg-custom-args/captures", defaultOptions.and("-language:experimental.captureChecking", "-source", "3.8")), + compileFilesInDir("tests/neg-custom-args/qualified-types", defaultOptions.and("-language:experimental.qualifiedTypes")), compileFile("tests/neg-custom-args/sourcepath/outer/nested/Test1.scala", defaultOptions.and("-sourcepath", "tests/neg-custom-args/sourcepath")), compileDir("tests/neg-custom-args/sourcepath2/hi", defaultOptions.and("-sourcepath", "tests/neg-custom-args/sourcepath2", "-Xfatal-warnings")), compileList("duplicate source", List( @@ -170,6 +172,7 @@ class CompilationTests { compileFilesInDir("tests/run", defaultOptions.and("-Wsafe-init")), compileFilesInDir("tests/run-deep-subtype", allowDeepSubtypes), compileFilesInDir("tests/run-custom-args/captures", allowDeepSubtypes.and("-language:experimental.captureChecking", "-source", "3.8")), + compileFilesInDir("tests/run-custom-args/qualified-types", defaultOptions.and("-language:experimental.qualifiedTypes")), // Run tests for legacy lazy vals. compileFilesInDir("tests/run", defaultOptions.and("-Wsafe-init", "-Ylegacy-lazy-vals", "-Ycheck-constraint-deps"), FileFilter.include(TestSources.runLazyValsAllowlist)), ).checkRuns() diff --git a/library/src/scala/annotation/qualified.scala b/library/src/scala/annotation/qualified.scala new file mode 100644 index 000000000000..0c0b6532dd43 --- /dev/null +++ b/library/src/scala/annotation/qualified.scala @@ -0,0 +1,4 @@ +package scala.annotation + +/** Annotation for qualified types. */ +@experimental class qualified[T](predicate: T => Boolean) extends RefiningAnnotation diff --git a/library/src/scala/runtime/stdLibPatches/language.scala b/library/src/scala/runtime/stdLibPatches/language.scala index 0f5e904e29bb..269df32fd599 100644 --- a/library/src/scala/runtime/stdLibPatches/language.scala +++ b/library/src/scala/runtime/stdLibPatches/language.scala @@ -84,6 +84,10 @@ object language: @compileTimeOnly("`captureChecking` can only be used at compile time in import statements") object captureChecking + /** Experimental support for qualified types */ + @compileTimeOnly("`qualifiedTypes` is only be used at compile time") + object qualifiedTypes + /** Experimental support for automatic conversions of arguments, without requiring * a language import `import scala.language.implicitConversions`. * diff --git a/project/MiMaFilters.scala b/project/MiMaFilters.scala index c57136b262dd..92bbaabb16ce 100644 --- a/project/MiMaFilters.scala +++ b/project/MiMaFilters.scala @@ -19,6 +19,9 @@ object MiMaFilters { ProblemFilters.exclude[DirectMissingMethodProblem]("scala.Conversion.underlying"), ProblemFilters.exclude[MissingClassProblem]("scala.Conversion$"), + + ProblemFilters.exclude[MissingFieldProblem]("scala.runtime.stdLibPatches.language#experimental.qualifiedTypes"), + ProblemFilters.exclude[MissingClassProblem]("scala.runtime.stdLibPatches.language$experimental$qualifiedTypes$"), ), // Additions since last LTS diff --git a/tests/neg-custom-args/qualified-types/adapt_neg.scala b/tests/neg-custom-args/qualified-types/adapt_neg.scala new file mode 100644 index 000000000000..8722ffcbb7b8 --- /dev/null +++ b/tests/neg-custom-args/qualified-types/adapt_neg.scala @@ -0,0 +1,22 @@ +def f(x: Int): Int = ??? +case class IntBox(x: Int) +case class Box[T](x: T) + +def test: Unit = + val x: Int = ??? + val y: Int = ??? + def g(x: Int): Int = ??? + + val v1: {v: Int with v == 1} = 2 // error + val v2: {v: Int with v == x} = y // error + val v3: {v: Int with v == x + 1} = x + 2 // error + val v4: {v: Int with v == f(x)} = g(x) // error + val v5: {v: Int with v == g(x)} = f(x) // error + val v6: {v: IntBox with v == IntBox(x)} = IntBox(x + 1) // error + val v7: {v: Box[Int] with v == Box(x)} = Box(x + 1) // error + val v8: {v: Int with v == x + f(x)} = x + g(x) // error + val v9: {v: Int with v == x + g(x)} = x + f(x) // error + val v10: {v: Int with v == f(x + 1)} = f(x + 2) // error + val v11: {v: Int with v == g(x + 1)} = g(x + 2) // error + val v12: {v: IntBox with v == IntBox(x + 1)} = IntBox(x) // error + val v13: {v: Box[Int] with v == Box(x + 1)} = Box(x) // error diff --git a/tests/neg-custom-args/qualified-types/runtimeChecked_dependent_neg.scala b/tests/neg-custom-args/qualified-types/runtimeChecked_dependent_neg.scala new file mode 100644 index 000000000000..f8fcc5c576f0 --- /dev/null +++ b/tests/neg-custom-args/qualified-types/runtimeChecked_dependent_neg.scala @@ -0,0 +1,8 @@ +def foo(x: Int, y: {v: Int with v > x}): y.type = y + +def getInt(): Int = + println("getInt called") + 42 + +@main def Test = + val res = foo(getInt(), 2.runtimeChecked) // error diff --git a/tests/neg-custom-args/qualified-types/subtyping_egraph_state.scala b/tests/neg-custom-args/qualified-types/subtyping_egraph_state.scala new file mode 100644 index 000000000000..a40f6db5331c --- /dev/null +++ b/tests/neg-custom-args/qualified-types/subtyping_egraph_state.scala @@ -0,0 +1,7 @@ +def test: Unit = + val b: Boolean = ??? + val b2: Boolean = ??? + summon[{u: Unit with b && b2} <:< {u: Unit with b}] + // Checks that E-Graph state is reset after the implication check: b is no + // longer true + summon[{u: Unit with true} <:< {u: Unit with b}] // error diff --git a/tests/neg-custom-args/qualified-types/subtyping_lambdas_neg.scala.scala b/tests/neg-custom-args/qualified-types/subtyping_lambdas_neg.scala.scala new file mode 100644 index 000000000000..c36d13c0941f --- /dev/null +++ b/tests/neg-custom-args/qualified-types/subtyping_lambdas_neg.scala.scala @@ -0,0 +1,9 @@ +def toBool[T](x: T): Boolean = ??? +def tp[T](): Any = ??? + +def test: Unit = + val x: {l: List[Int] with toBool((x: String, y: x.type) => x.length > 0)} = ??? // error: cannot turn method type into closure because it has internal parameter dependencies + summon[{l: List[Int] with toBool((x: String, y: String) => tp[x.type]())} =:= {l: List[Int] with toBool((x: String, y: String) => tp[y.type]())}] // error + + summon[{l: List[Int] with toBool((x: Double) => (y: Int) => x == y)} =:= {l: List[Int] with toBool((a: Double) => (b: Int) => a == a)}] // error + summon[{l: List[Int] with toBool((x: Int) => (y: Int) => x == y)} =:= {l: List[Int] with toBool((a: Int) => (b: Int) => a == a)}] // error diff --git a/tests/neg-custom-args/qualified-types/subtyping_objects_neg.scala b/tests/neg-custom-args/qualified-types/subtyping_objects_neg.scala new file mode 100644 index 000000000000..b1283f4e807b --- /dev/null +++ b/tests/neg-custom-args/qualified-types/subtyping_objects_neg.scala @@ -0,0 +1,52 @@ +class Box[T](val x: T) + +class BoxMutable[T](var x: T) + +class Foo(val id: String): + def this(x: Int) = this(x.toString) + +class Person(val name: String, val age: Int) + +class PersonCurried(val name: String)(val age: Int) + +class PersonMutable(val name: String, var age: Int) + +case class PersonCaseMutable(name: String, var age: Int) + +case class PersonCaseSecondary(name: String, age: Int): + def this(name: String) = this(name, 0) + +case class PersonCaseEqualsOverriden(name: String, age: Int): + override def equals(that: Object): Boolean = this eq that + +def test: Unit = + summon[{b: Box[Int] with b == Box(1)} =:= {b: Box[Int] with b == Box(1)}] // error + + summon[{b: BoxMutable[Int] with b == BoxMutable(1)} =:= {b: BoxMutable[Int] with b == BoxMutable(1)}] // error + // TODO(mbovel): restrict selection to stable members + //summon[{b: BoxMutable[Int] with b.x == 3} =:= {b: BoxMutable[Int] with b.x == 3}] + + summon[{f: Foo with f == Foo("hello")} =:= {f: Foo with f == Foo("hello")}] // error + summon[{f: Foo with f == Foo(1)} =:= {f: Foo with f == Foo(1)}] // error + summon[{s: String with Foo("hello").id == s} =:= {s: String with s == "hello"}] // error + + summon[{p: Person with p == Person("Alice", 30)} =:= {p: Person with p == Person("Alice", 30)}] // error + summon[{s: String with Person("Alice", 30).name == s} =:= {s: String with s == "Alice"}] // error + summon[{n: Int with Person("Alice", 30).age == n} =:= {n: Int with n == 30}] // error + + summon[{p: PersonCurried with p == PersonCurried("Alice")(30)} =:= {p: PersonCurried with p == PersonCurried("Alice")(30)}] // error + summon[{s: String with PersonCurried("Alice")(30).name == s} =:= {s: String with s == "Alice"}] // error + summon[{n: Int with PersonCurried("Alice")(30).age == n} =:= {n: Int with n == 30}] // error + + summon[{p: PersonMutable with p == PersonMutable("Alice", 30)} =:= {p: PersonMutable with p == PersonMutable("Alice", 30)}] // error + summon[{s: String with PersonMutable("Alice", 30).name == s} =:= {s: String with s == "Alice"}] // error + summon[{n: Int with PersonMutable("Alice", 30).age == n} =:= {n: Int with n == 30}] // error + + summon[{n: Int with PersonCaseMutable("Alice", 30).age == n} =:= {n: Int with n == 30}] // error + + summon[{s: String with new PersonCaseSecondary("Alice").name == s} =:= {s: String with s == "Alice"}] // error + summon[{n: Int with new PersonCaseSecondary("Alice").age == n} =:= {n: Int with n == 0}] // error + + summon[{p: PersonCaseEqualsOverriden with PersonCaseEqualsOverriden("Alice", 30) == p} =:= {p: PersonCaseEqualsOverriden with p == PersonCaseEqualsOverriden("Alice", 30)}] // error + summon[{s: String with PersonCaseEqualsOverriden("Alice", 30).name == s} =:= {s: String with s == "Alice"}] // error + summon[{n: Int with PersonCaseEqualsOverriden("Alice", 30).age == n} =:= {n: Int with n == 30}] // error diff --git a/tests/neg-custom-args/qualified-types/subtyping_singletons_neg.scala b/tests/neg-custom-args/qualified-types/subtyping_singletons_neg.scala new file mode 100644 index 000000000000..421cca0789b2 --- /dev/null +++ b/tests/neg-custom-args/qualified-types/subtyping_singletons_neg.scala @@ -0,0 +1,8 @@ +def f(x: Int): Int = ??? + +def test: Unit = + val x: Int = ??? + val y: Int = ??? + summon[2 <:< {v: Int with v == 1}] // error + summon[x.type <:< {v: Int with v == 1}] // error + //summon[y.type <:< {v: Int with v == x}] // FIXME diff --git a/tests/neg-custom-args/qualified-types/subtyping_unfolding_neg.scala b/tests/neg-custom-args/qualified-types/subtyping_unfolding_neg.scala new file mode 100644 index 000000000000..2af09733af41 --- /dev/null +++ b/tests/neg-custom-args/qualified-types/subtyping_unfolding_neg.scala @@ -0,0 +1,18 @@ +def tp[T](): T = ??? + +abstract class C: + type T + val x: T + +def test: Unit = + val x: Int = ??? + val z: Int = ??? + val c1: C = ??? + val c2: C = ??? + + summon[{v: Int with v == x} <:< {v: Int with v == z}] // error + summon[{v: C with v == c1} <:< {v: C with v == c2}] // error + + // summon[{v: Int with v == (??? : Int)} <:< {v: Int with v == (??? : Int)}] // TODO(mbovel): should not compare some impure applications? + + summon[{v: Int with v == tp[c1.T]()} <:< {v: Int with v == tp[c2.T]()}] // error diff --git a/tests/neg-custom-args/qualified-types/syntax_unnamed_neg.scala b/tests/neg-custom-args/qualified-types/syntax_unnamed_neg.scala new file mode 100644 index 000000000000..9191cf5db2db --- /dev/null +++ b/tests/neg-custom-args/qualified-types/syntax_unnamed_neg.scala @@ -0,0 +1,7 @@ +case class Box[T](x: T) +def id[T](x: T): T = x + +abstract class Test: + val v1: Box[Int with v1 > 0] // error: Cyclic reference + val v2: {v: Int with id[Int with v2 > 0](???) > 0} // error: Cyclic reference + val v3: {v: Int with (??? : Int with v3 == 2) > 0} // error: Cyclic reference diff --git a/tests/pos-custom-args/qualified-types/adapt.scala b/tests/pos-custom-args/qualified-types/adapt.scala new file mode 100644 index 000000000000..597c6906b19d --- /dev/null +++ b/tests/pos-custom-args/qualified-types/adapt.scala @@ -0,0 +1,21 @@ +def f(x: Int): Int = ??? +case class IntBox(x: Int) +case class Box[T](x: T) + +def f(x: Int, y: Int): {r: Int with r == x + y} = x + y + +def test: Unit = + val x: Int = ??? + def g(x: Int): Int = ??? + + val v1: {v: Int with v == x + 1} = x + 1 + val v2: {v: Int with v == f(x)} = f(x) + val v3: {v: Int with v == g(x)} = g(x) + val v4: {v: IntBox with v == IntBox(x)} = IntBox(x) + val v5: {v: Box[Int] with v == Box(x)} = Box(x) + val v6: {v: Int with v == x + f(x)} = x + f(x) + val v7: {v: Int with v == x + g(x)} = x + g(x) + val v8: {v: Int with v == f(x + 1)} = f(x + 1) + val v9: {v: Int with v == g(x + 1)} = g(x + 1) + val v12: {v: IntBox with v == IntBox(x + 1)} = IntBox(x + 1) + val v13: {v: Box[Int] with v == Box(x + 1)} = Box(x + 1) diff --git a/tests/pos-custom-args/qualified-types/avoidance.scala b/tests/pos-custom-args/qualified-types/avoidance.scala new file mode 100644 index 000000000000..bff6decb61a2 --- /dev/null +++ b/tests/pos-custom-args/qualified-types/avoidance.scala @@ -0,0 +1,6 @@ +def Test = () + + //val x = + // val y: Int = ??? + // y: {v: Int with v == y} + // TODO(mbovel): proper avoidance for qualified types diff --git a/tests/pos-custom-args/qualified-types/class_constraints.scala b/tests/pos-custom-args/qualified-types/class_constraints.scala new file mode 100644 index 000000000000..71085de63d44 --- /dev/null +++ b/tests/pos-custom-args/qualified-types/class_constraints.scala @@ -0,0 +1,3 @@ +/*class foo(elem: Int with elem > 0)*/ + +@main def Test = () diff --git a/tests/pos-custom-args/qualified-types/sized_lists.scala b/tests/pos-custom-args/qualified-types/sized_lists.scala new file mode 100644 index 000000000000..385434c253f7 --- /dev/null +++ b/tests/pos-custom-args/qualified-types/sized_lists.scala @@ -0,0 +1,15 @@ +def size(v: Vec): Int = ??? +type Vec + +def vec(s: Int): {v: Vec with size(v) == s} = ??? +def concat(v1: Vec, v2: Vec): {v: Vec with size(v) == size(v1) + size(v2)} = ??? +def sum(v1: Vec, v2: Vec with size(v1) == size(v2)): {v: Vec with size(v) == size(v1)} = ??? + +@main def Test = + + val v3: {v: Vec with size(v) == 3} = vec(3) + val v4: {v: Vec with size(v) == 4} = vec(4) + /* + val v7: {v: Vec with size(v) == 7} = concat(v3, v4) + */ + // TODO(mbovel): need constraints of referred term refs diff --git a/tests/pos-custom-args/qualified-types/sized_lists2.scala b/tests/pos-custom-args/qualified-types/sized_lists2.scala new file mode 100644 index 000000000000..9cdcd934f1f1 --- /dev/null +++ b/tests/pos-custom-args/qualified-types/sized_lists2.scala @@ -0,0 +1,22 @@ +type Vec[T] +object Vec: + def fill[T](n: Int, v: T): + {r: Vec[T] with r.len == n} + = ??? +extension [T](a: Vec[T]) + def len: {r: Int with r >= 0} = ??? + def concat(b: Vec[T]): + {r: Vec[T] with r.len == a.len + b.len} + = ??? + def zip[S](b: Vec[S] with b.len == a.len): + {r: Vec[(T, S)] with r.len == a.len} + = ??? + +@main def Test = + val n: Int with n >= 0 = ??? + val m: Int with m >= 0 = ??? + val v1 = Vec.fill(n, 0) + val v2 = Vec.fill(m, 1) + val v3 = v1.concat(v2) + // TODO(mbovel) + //v1.concat(v2).zip(Vec.fill(m + n, "")) diff --git a/tests/pos-custom-args/qualified-types/subtyping_equality.scala b/tests/pos-custom-args/qualified-types/subtyping_equality.scala new file mode 100644 index 000000000000..d4e3201472a7 --- /dev/null +++ b/tests/pos-custom-args/qualified-types/subtyping_equality.scala @@ -0,0 +1,49 @@ +def f(x: Int): Int = ??? +def g(x: Int): Int = ??? +def f2(x: Int, y: Int): Int = ??? +def g2(x: Int, y: Int): Int = ??? + +case class IntBox(x: Int) +case class Box[T](x: T) + +def test: Unit = + val a: Int = ??? + val b: Int = ??? + val c: Int = ??? + val d: Int = ??? + /* + summon[{v: Int with v == 2} <:< {v: Int with v == 2}] + summon[{v: Int with v == f(a)} <:< {v: Int with v == f(a)}] + + // Equality is reflexive, symmetric and transitive + summon[{v: Int with v == v} <:< {v: Int with true}] + summon[{v: Int with a == b} <:< {v: Int with true}] + summon[{v: Int with v == a} <:< {v: Int with v == a}] + summon[{v: Int with v == a} <:< {v: Int with a == v}] + summon[{v: Int with a == b} <:< {v: Int with b == a}] + summon[{v: Int with v == a && a > 3} <:< {v: Int with v > 3}] + summon[{v: Int with v == a && a == b} <:< {v: Int with v == b}] + summon[{v: Int with a == b && b == c} <:< {v: Int with a == c}] + summon[{v: Int with a == b && c == b} <:< {v: Int with a == c}] + summon[{v: Int with a == b && c == d && b == d} <:< {v: Int with b == d}] + summon[{v: Int with a == b && c == d && b == d} <:< {v: Int with a == c}] + + // Equality is congruent over functions + summon[{v: Int with a == b} <:< {v: Int with f(a) == f(b)}] + summon[{v: Int with a == b} <:< {v: Int with f(f(a)) == f(f(b))}] + summon[{v: Int with c == f(a) && d == f(b) && a == b} <:< {v: Int with c == d}] + // the two first equalities in the premises are just used to test the behavior + // of the e-graph when `f(a)` and `f(b)` are inserted before `a == b`. + summon[{v: Int with c == f(a) && d == f(b) && a == b} <:< {v: Int with f(a) == f(b)}] + summon[{v: Int with c == f(a) && d == f(b) && a == b} <:< {v: Int with f(f(a)) == f(f(b))}] + */ + + // Equality is supported on Strings + summon[{v: String with v == "hello"} <:< {v: String with v == "hello"}] + summon[{v: String with v == "hello"} <:< {v: String with "hello" == v}] + + // Equality is supported on case classes + summon[{v: IntBox with v == IntBox(3)} <:< {v: IntBox with v == IntBox(3)}] + summon[{v: IntBox with v == IntBox(3)} <:< {v: IntBox with IntBox(3) == v}] + summon[{v: Box[Int] with v == Box(3)} <:< {v: Box[Int] with v == Box(3)}] + summon[{v: Box[Int] with v == Box(3)} <:< {v: Box[Int] with Box(3) == v}] diff --git a/tests/pos-custom-args/qualified-types/subtyping_lambdas.scala b/tests/pos-custom-args/qualified-types/subtyping_lambdas.scala new file mode 100644 index 000000000000..204dde4ac55e --- /dev/null +++ b/tests/pos-custom-args/qualified-types/subtyping_lambdas.scala @@ -0,0 +1,17 @@ +def toBool[T](x: T): Boolean = ??? +def tp[T](): Any = ??? + + +def test: Unit = + summon[{l: List[Int] with l.forall(x => x > 0)} =:= {l: List[Int] with l.forall(x => x > 0)}] + summon[{l: List[Int] with l.forall(x => x > 0)} =:= {l: List[Int] with l.forall(y => y > 0)}] + summon[{l: List[Int] with l.forall(x => x > 0)} =:= {l: List[Int] with l.forall(_ > 0)}] + + summon[{l: List[Int] with toBool((x: String) => x.length > 0)} =:= {l: List[Int] with toBool((y: String) => y.length > 0)}] + + summon[{l: List[Int] with toBool((x: String) => tp[x.type]())} =:= {l: List[Int] with toBool((y: String) => tp[y.type]())}] + summon[{l: List[Int] with toBool((x: String, y: String) => tp[x.type]())} =:= {l: List[Int] with toBool((x: String, y: String) => tp[x.type]())}] + summon[{l: List[Int] with toBool((x: String) => tp[x.type]())} =:= {l: List[Int] with toBool((y: String) => tp[y.type]())}] + summon[{l: List[Int] with toBool((x: String, y: String) => tp[y.type]())} =:= {l: List[Int] with toBool((x: String, y: String) => tp[y.type]())}] + + summon[{l: List[Int] with toBool((x: String) => (y: String) => x == y)} =:= {l: List[Int] with toBool((a: String) => (b: String) => a == b)}] diff --git a/tests/pos-custom-args/qualified-types/subtyping_normalization.scala b/tests/pos-custom-args/qualified-types/subtyping_normalization.scala new file mode 100644 index 000000000000..6fc9dd98783f --- /dev/null +++ b/tests/pos-custom-args/qualified-types/subtyping_normalization.scala @@ -0,0 +1,5 @@ +def test: Unit = + val x: Int = ??? + val y: Int = ??? + val z: Int = ??? + summon[{v: Int with v == 2 + (x * y * y * z)} <:< {v: Int with v == (x * y * z * y) + 2}] diff --git a/tests/pos-custom-args/qualified-types/subtyping_objects.scala b/tests/pos-custom-args/qualified-types/subtyping_objects.scala new file mode 100644 index 000000000000..6916e1fba5a1 --- /dev/null +++ b/tests/pos-custom-args/qualified-types/subtyping_objects.scala @@ -0,0 +1,59 @@ +class Box[T](val x: T) + +class Foo(val id: String): + def this(x: Int) = this(x.toString) + +case class PersonCase(name: String, age: Int) + +case class PersonCaseCurried(name: String)(val age: Int) + +case class PersonCaseMutable(name: String, var age: Int) + +case class PersonCaseSecondary(name: String, age: Int): + def this(name: String) = this(name, 0) + +def test: Unit = + summon[{b: Box[Int] with 3 == b.x} =:= {b: Box[Int] with b.x == 3}] + + // TODO(mbovel): firgure out why f.id.== is Any.== and not String.== + //summon[{f: Foo with f.id == "hello"} =:= {f: Foo with "hello" == f.id}] + + // new PersonCase + summon[{p: PersonCase with p == new PersonCase("Alice", 30)} =:= {p: PersonCase with p == new PersonCase("Alice", 30)}] + summon[{s: String with new PersonCase("Alice", 30).name == s} =:= {s: String with s == "Alice"}] + summon[{n: Int with new PersonCase("Alice", 30).age == n} =:= {n: Int with n == 30}] + + // PersonCase + summon[{p: PersonCase with p == PersonCase("Alice", 30)} =:= {p: PersonCase with p == PersonCase("Alice", 30)}] + summon[{s: String with PersonCase("Alice", 30).name == s} =:= {s: String with s == "Alice"}] + summon[{n: Int with PersonCase("Alice", 30).age == n} =:= {n: Int with n == 30}] + + // new PersonCaseCurried + summon[{p: PersonCaseCurried with p == new PersonCaseCurried("Alice")(30)} =:= {p: PersonCaseCurried with p == new PersonCaseCurried("Alice")(30)}] + summon[{s: String with new PersonCaseCurried("Alice")(30).name == s} =:= {s: String with s == "Alice"}] + summon[{n: Int with new PersonCaseCurried("Alice")(30).age == n} =:= {n: Int with n == 30}] + + // PersonCaseCurried + summon[{p: PersonCaseCurried with p == PersonCaseCurried("Alice")(30)} =:= {p: PersonCaseCurried with p == PersonCaseCurried("Alice")(30)}] + summon[{s: String with PersonCaseCurried("Alice")(30).name == s} =:= {s: String with s == "Alice"}] + summon[{n: Int with PersonCaseCurried("Alice")(30).age == n} =:= {n: Int with n == 30}] + + // new PersonCaseMutable + summon[{p: PersonCaseMutable with p == new PersonCaseMutable("Alice", 30)} =:= {p: PersonCaseMutable with p == new PersonCaseMutable("Alice", 30)}] + summon[{s: String with new PersonCaseMutable("Alice", 30).name == s} =:= {s: String with s == "Alice"}] + //summon[{n: Int with new PersonCaseMutable("Alice", 30).age == n} =:= {n: Int with n == 30}] // error + + // PersonCaseMutable + summon[{p: PersonCaseMutable with p == PersonCaseMutable("Alice", 30)} =:= {p: PersonCaseMutable with p == PersonCaseMutable("Alice", 30)}] + summon[{s: String with PersonCaseMutable("Alice", 30).name == s} =:= {s: String with s == "Alice"}] + //summon[{n: Int with PersonCaseMutable("Alice", 30).age == n} =:= {n: Int with n == 30}] // error + + // new PersonCaseSecondary + summon[{p: PersonCaseSecondary with p == new PersonCaseSecondary("Alice")} =:= {p: PersonCaseSecondary with p == new PersonCaseSecondary("Alice")}] + //summon[{s: String with new PersonCaseSecondary("Alice").name == s} =:= {s: String with s == "Alice"}] // error + //summon[{n: Int with new PersonCaseSecondary("Alice").age == n} =:= {n: Int with n == 0}] // error + + // PersonCaseSecondary + summon[{p: PersonCaseSecondary with p == PersonCaseSecondary("Alice", 30)} =:= {p: PersonCaseSecondary with p == PersonCaseSecondary("Alice", 30)}] + //summon[{s: String with PersonCaseSecondary("Alice", 30).name == s} =:= {s: String with s == "Alice"}] // error + //summon[{n: Int with PersonCaseSecondary("Alice", 30).age == n} =:= {n: Int with n == 30}] // error diff --git a/tests/pos-custom-args/qualified-types/subtyping_paths.scala b/tests/pos-custom-args/qualified-types/subtyping_paths.scala new file mode 100644 index 000000000000..153dfb2bee64 --- /dev/null +++ b/tests/pos-custom-args/qualified-types/subtyping_paths.scala @@ -0,0 +1,18 @@ +def tp[T](): Boolean = ??? + +class Outer: + class Inner: + type D + summon[{v: Boolean with tp[Inner.this.D]()} =:= {v: Boolean with tp[D]()}] + +object OuterO: + object InnerO: + type D + summon[{v: Boolean with tp[InnerO.this.D]()} =:= {v: Boolean with tp[D]()}] + + // Before normalization: + // lhs: .this.OuterO$.this.InnerO$.this.D + // rhs: .this.OuterO$.this.InnerO.D + summon[{v: Boolean with tp[InnerO.D]()} =:= {v: Boolean with tp[D]()}] + + summon[{v: Boolean with tp[OuterO.InnerO.D]()} =:= {v: Boolean with tp[D]()}] diff --git a/tests/pos-custom-args/qualified-types/subtyping_reflectivity.scala b/tests/pos-custom-args/qualified-types/subtyping_reflectivity.scala new file mode 100644 index 000000000000..8cace392c1f6 --- /dev/null +++ b/tests/pos-custom-args/qualified-types/subtyping_reflectivity.scala @@ -0,0 +1,4 @@ +@main def Test = + val n: Int = ??? + summon[{v: Int with v == 2} <:< {v: Int with v == 2}] + summon[{v: Int with v == n} <:< {v: Int with v == n}] diff --git a/tests/pos-custom-args/qualified-types/subtyping_singletons.scala b/tests/pos-custom-args/qualified-types/subtyping_singletons.scala new file mode 100644 index 000000000000..f2f83e8dd8c4 --- /dev/null +++ b/tests/pos-custom-args/qualified-types/subtyping_singletons.scala @@ -0,0 +1,8 @@ +type Pos = {v: Int with v > 0} + +def test: Unit = + val x: Int = ??? + summon[1 <:< {v: Int with v == 1}] + summon[1 <:< {v: Int with v > 0}] + summon[1 <:< Pos] + summon[x.type <:< {v: Int with v == x}] diff --git a/tests/pos-custom-args/qualified-types/subtyping_unfolding.scala.disabled b/tests/pos-custom-args/qualified-types/subtyping_unfolding.scala.disabled new file mode 100644 index 000000000000..99ced156cdf5 --- /dev/null +++ b/tests/pos-custom-args/qualified-types/subtyping_unfolding.scala.disabled @@ -0,0 +1,54 @@ +def id[T](x: T): T = x + +abstract class C: + type T + val x: T + +def test: Unit = + val x: Int = ??? + val x2: Int = x + val x3: Int = x2 + val y: Int = x + 1 + val c1: C = ??? + val c2: C = ??? + + summon[{v: Int with v == x} <:< {v: Int with v == x2}] + summon[{v: Int with v == x2} <:< {v: Int with v == x}] + summon[{v: Int with v == x} <:< {v: Int with v == x3}] + summon[{v: Int with v == x3} <:< {v: Int with v == x}] + summon[{v: Int with v == x2} <:< {v: Int with v == x3}] + summon[{v: Int with v == x3} <:< {v: Int with v == x2}] + + summon[{v: Int with v == y} <:< {v: Int with v == x + 1}] + summon[{v: Int with v == x + 1} <:< {v: Int with v == y}] + + summon[{v: Int with v == id(x)} <:< {v: Int with v == id(x2)}] + summon[{v: Int with v == id(x2)} <:< {v: Int with v == id(x)}] + summon[{v: Int with v == id(y)} <:< {v: Int with v == id(x + 1)}] + summon[{v: Int with v == id(x + 1)} <:< {v: Int with v == id(y)}] + + summon[{v: Int with v == y + 2} <:< {v: Int with v == x + 1 + 2}] + summon[{v: Int with v == x + 1 + 2} <:< {v: Int with v == y + 2}] + + summon[{v: Int with v == id[c1.T](c1.x)} <:< {v: Int with v == id[c1.T](c1.x)}] + + def innerScope() = + summon[{v: Int with v == x} <:< {v: Int with v == x2}] + summon[{v: Int with v == x2} <:< {v: Int with v == x}] + summon[{v: Int with v == x} <:< {v: Int with v == x3}] + summon[{v: Int with v == x3} <:< {v: Int with v == x}] + summon[{v: Int with v == x2} <:< {v: Int with v == x3}] + summon[{v: Int with v == x3} <:< {v: Int with v == x2}] + + summon[{v: Int with v == y} <:< {v: Int with v == x + 1}] + summon[{v: Int with v == x + 1} <:< {v: Int with v == y}] + + summon[{v: Int with v == id(x)} <:< {v: Int with v == id(x2)}] + summon[{v: Int with v == id(x2)} <:< {v: Int with v == id(x)}] + summon[{v: Int with v == id(y)} <:< {v: Int with v == id(x + 1)}] + summon[{v: Int with v == id(x + 1)} <:< {v: Int with v == id(y)}] + + summon[{v: Int with v == y + 2} <:< {v: Int with v == x + 1 + 2}] + summon[{v: Int with v == x + 1 + 2} <:< {v: Int with v == y + 2}] + + summon[{v: Int with v == id[c1.T](c1.x)} <:< {v: Int with v == id[c1.T](c1.x)}] diff --git a/tests/pos-custom-args/qualified-types/syntax_basics.scala b/tests/pos-custom-args/qualified-types/syntax_basics.scala new file mode 100644 index 000000000000..8395c5bf670c --- /dev/null +++ b/tests/pos-custom-args/qualified-types/syntax_basics.scala @@ -0,0 +1,55 @@ +abstract class BoolExprs: + val c: Boolean + def f(b: Boolean): Boolean + val v01: {b: Boolean with true} + val v02: {b: Boolean with false} + val v03: {b: Boolean with !b} + val v04: {b: Boolean with b && b} + val v05: {b: Boolean with b || b} + val v06: {b: Boolean with c} + val v07: {b: Boolean with !c} + val v08: {b: Boolean with b && c} + val v09: {b: Boolean with b || c} + val v10: {b: Boolean with f(b)} + val w01 = v01 + val w02 = v02 + val w03 = v03 + val w04 = v04 + val w05 = v05 + val w06 = v06 + val w07 = v07 + val w08 = v08 + val w09 = v09 + val w10 = v10 + +abstract class IntExprs: + val c: Int + def f(n: Int): Int + val v01: {n: Int with n == 0} + val v02: {n: Int with -n == 0} + val v03: {n: Int with n != 0} + val v04: {n: Int with n > 0} + val v05: {n: Int with n >= 0} + val v06: {n: Int with n < 0} + val v07: {n: Int with n <= 0} + val v08: {n: Int with n == c} + val v09: {n: Int with n != c} + val v10: {n: Int with n > c} + val v11: {n: Int with n >= c} + val v12: {n: Int with n < c} + val v13: {n: Int with n <= c} + val v14: {n: Int with n == f(n)} + val w01 = v01 + val w02 = v02 + val w03 = v03 + val w04 = v04 + val w05 = v05 + val w06 = v06 + val w07 = v07 + val w08 = v08 + val w09 = v09 + val w10 = v10 + val w11 = v11 + val w12 = v12 + val w13 = v13 + val w14 = v14 diff --git a/tests/printing/qualified-types.check b/tests/printing/qualified-types.check new file mode 100644 index 000000000000..2c8605bad750 --- /dev/null +++ b/tests/printing/qualified-types.check @@ -0,0 +1,160 @@ +[[syntax trees at end of typer]] // tests/printing/qualified-types.scala +package example { + class Foo() extends Object() { + val x: Int @qualified[Int]((x: Int) => x > 0) = 1 + } + trait A() extends Object {} + final lazy module val qualified-types$package: example.qualified-types$package + = new example.qualified-types$package() + final module class qualified-types$package() extends Object() { + this: example.qualified-types$package.type => + type Neg = Int @qualified[Int]((x: Int) => x < 0) + type Pos = Int @qualified[Int]((x: Int) => x > 0) + type Pos2 = Int @qualified[Int]((x: Int) => x > 0) + type Pos3 = Int @qualified[Int]((x: Int) => x > 0) + type Pos4 = Int @qualified[Int]((x: Int) => x > 0) + type Pos5 = + Int @qualified[Int]((x: Int) => + { + val res: Boolean = x > 0 + res:Boolean + } + ) + type UninhabitedInt = Int @qualified[Int]((_: Int) => false) + type Nested = + Int @qualified[Int]((x: Int) => + { + val y: Int @qualified[Int]((z: Int) => z > 0) = ??? + x > y + } + ) + type Intersection = Int & Int @qualified[Int]((x: Int) => x > 0) + type ValRefinement = + Object + { + val x: Int @qualified[Int]((x: Int) => x > 0) + } + def id[T >: Nothing <: Any](x: T): T = x + def test(): Unit = + { + val x: example.Pos = 1 + val x2: Int @qualified[Int]((x: Int) => x > 0) = 1 + val x3: Int @qualified[Int]((x: Int) => x > 0) = 1 + val x4: Int @qualified[Int]((x: Int) => x > 0) = 1 + val x5: Int @qualified[Int]((x5: Int) => x > 0) = 1 + val x6: Int = + example.id[Int @qualified[Int]((x: Int) => x < 0)](-1) + + example.id[example.Neg](-1) + () + } + def implicitArgumentName(): Unit = + { + val x0: + Int @qualified[Int]((x0: Int) => x0 > 0) | + String @qualified[String]((x0: String) => x0 == "foo") + = ??? + val x1: Int @qualified[Int]((x1: Int) => x1 > 0) = ??? + val x2: Int @qualified[Int]((x2: Int) => x2 > 0) = ??? + val x3: + Int @qualified[Int]((x3: Int) => x3 > 0) & + Int @qualified[Int]((x3: Int) => x3 < 10) + = ??? + val x4: + (Int @qualified[Int]((x4: Int) => x4 > 0) & Int) @qualified[ + Int @qualified[Int]((x4: Int) => x4 > 0) & Int](( + x4: Int @qualified[Int]((x4: Int) => x4 > 0) & Int) => x4 < 10) + = ??? + val x5: + (Int & String) @qualified[Int & String]((x5: Int & String) => false) + = ??? + val x6: + (Int @qualified[Int]((x6: Int) => x6 > 0) & Int) @qualified[ + Int @qualified[Int]((x6: Int) => x6 > 0) & Int](( + x6: Int @qualified[Int]((x6: Int) => x6 > 0) & Int) => x5 < 10) + = ??? + val x7: + Int @qualified[Int]((x7: Int) => x7 > 0) @qualified[ + Int @qualified[Int]((x7: Int) => x7 > 0)](( + x7: Int @qualified[Int]((x7: Int) => x7 > 0)) => x6 < 10) + = ??? + val x8: + Int @qualified[Int]((x8: Int) => x8 > 0) @qualified[ + Int @qualified[Int]((x8: Int) => x8 > 0)](( + x8: Int @qualified[Int]((x8: Int) => x8 > 0)) => x7 < 10) + = ??? + val x9: Any = 42 + x9 match + { + case y @ _:Int @qualified[Int]((y: Int) => y > 0) => + println( + _root_.scala.StringContext.apply([""," is positive" : String]*). + s([y : Any]*) + ) + case _ => + () + } + x9 match + { + case + Tuple2.unapply[Any, Any]( + y @ _:Int @qualified[Int]((y: Int) => y > 0), + z @ _:Int @qualified[Int]((z: Int) => z > 0)):(Any, Any) + => + println( + _root_.scala.StringContext.apply( + [""," and "," are both positive" : String]*).s([y,z : Any]*) + ) + case _ => + () + } + } + def bar(x: Int @qualified[Int]((x: Int) => x > 0)): Nothing = ??? + def secondGreater1(x: Int, y: Int)(z: Int @qualified[Int]((w: Int) => x > y) + ): Nothing = ??? + def secondGreater2(x: Int, y: Int)(z: Int @qualified[Int]((z: Int) => x > y) + ): Nothing = ??? + final lazy module given val given_A: example.given_A = new example.given_A() + final module class given_A() extends Object(), example.A { + this: example.given_A.type => + val b: Boolean = false + example.id[Boolean](true) + } + type T1 = + Object + { + val x: Int + } + type T2 = + Object + { + val x: Int + } + type T3 = + Object + { + type T = Int + } + type T4 = + Object + { + def x: Int + } + type T5 = + Object + { + def x: Int + def x_=(x$1: Int): _root_.scala.Unit + } + type T6 = + Object + { + val x: Int + } + type T7 = + Object + { + val x: Int + } + } +} + diff --git a/tests/printing/qualified-types.flags b/tests/printing/qualified-types.flags new file mode 100644 index 000000000000..0a6f384436cd --- /dev/null +++ b/tests/printing/qualified-types.flags @@ -0,0 +1 @@ +-language:experimental.qualifiedTypes diff --git a/tests/printing/qualified-types.scala b/tests/printing/qualified-types.scala new file mode 100644 index 000000000000..9d231d1fbf31 --- /dev/null +++ b/tests/printing/qualified-types.scala @@ -0,0 +1,86 @@ +package example + +type Neg = {x: Int with x < 0} +type Pos = {x: Int with x > 0} +type Pos2 = {x: Int + with x > 0 +} +type Pos3 = {x: Int with + x > 0 +} +type Pos4 = + {x: Int with x > 0} +type Pos5 = {x: Int with + val res = x > 0 + res +} + +type UninhabitedInt = Int with false + +type Nested = {x: Int with { val y: {z: Int with z > 0} = ??? ; x > y }} +type Intersection = Int & {x: Int with x > 0} +type ValRefinement = {val x: Int with x > 0} + +def id[T](x: T): T = x + +def test() = + val x: Pos = 1 + val x2: {x: Int with x > 0} = 1 + val x3: { + x: Int with x > 0 + } = 1 + val x4: {x: Int with + x > 0 + } = 1 + val x5: Int with x > 0 = 1 + val x6: Int = id[{x: Int with x < 0}](-1) + id[Neg](-1) + +// `val x: Int with x > 0` is desugared to `val x: {x: Int with x > 0}`: if the +// name of a qualifier argument is not specified, it is assumed to be the same +// as the parent `val` definition. +def implicitArgumentName() = + val x0: (Int with x0 > 0) | (String with x0 == "foo") = ??? + val x1: Int with x1 > 0 = ??? + val x2: (Int with x2 > 0) = ??? + val x3: (Int with x3 > 0) & (Int with x3 < 10) = ??? + val x4: (Int with x4 > 0) & Int with x4 < 10 = ??? + val x5: Int & String with false = ??? + val x6: ((Int with x6 > 0) & Int) with x5 < 10 = ??? + val x7: (Int with x7 > 0) with x6 < 10 = ??? + val x8: ((Int with x8 > 0) with x7 < 10) = ??? + + val x9: Any = 42 + x9 match + case y: Int with y > 0 => + println(s"$y is positive") + case _ => () + + x9 match + case (y: Int with y > 0, z: Int with z > 0) => + println(s"$y and $z are both positive") + case _ => () + +def bar(x: Int with x > 0) = ??? +def secondGreater1(x: Int, y: Int)(z: {w: Int with x > y}) = ??? +def secondGreater2(x: Int, y: Int)(z: Int with x > y) = ??? + +class Foo: + val x: Int with x > 0 = 1 + +trait A +// Not a qualified type: +given A with + val b = false + id(true) + +// Also not qualified types: +type T1 = {val x: Int} +type T2 = { + val x: Int +} +type T3 = {type T = Int} +type T4 = {def x: Int} +type T5 = {var x: Int} +type T6 = Object {val x: Int} +type T7 = Object: + val x: Int diff --git a/tests/run-custom-args/qualified-types/evalOnce.check b/tests/run-custom-args/qualified-types/evalOnce.check new file mode 100644 index 000000000000..520fc9973c80 --- /dev/null +++ b/tests/run-custom-args/qualified-types/evalOnce.check @@ -0,0 +1,4 @@ +Hello +succeed +65 +Hello diff --git a/tests/run-custom-args/qualified-types/evalOnce.scala b/tests/run-custom-args/qualified-types/evalOnce.scala new file mode 100644 index 000000000000..fd050e1f5399 --- /dev/null +++ b/tests/run-custom-args/qualified-types/evalOnce.scala @@ -0,0 +1,15 @@ +@main def Test: Unit = + + ({println ("Hello"); 4}).isInstanceOf[{ y : Int with y > 0}] + + class Person(val name: String, var age: Int) + def incAge(p: Person): Int = + p.age += 1 + p.age + + val p = new Person("Alice", 64) + + if incAge(p).isInstanceOf[{v:Int with v == 65}] then println("succeed") + println(p.age) + + ({println ("Hello"); 4}).isInstanceOf[Int & { y : Int with y > 0}] diff --git a/tests/run-custom-args/qualified-types/isInstanceOf.check b/tests/run-custom-args/qualified-types/isInstanceOf.check new file mode 100644 index 000000000000..bba4b7cd4694 --- /dev/null +++ b/tests/run-custom-args/qualified-types/isInstanceOf.check @@ -0,0 +1,320 @@ +call id +-1 is instance of Pos: false +call id +-1 is instance of {v: Int with v == 2}: false +call id +-1 is instance of NonEmptyString: false +call id +-1 is instance of PoliteString: false +call id +-1 is instance of Pos & Int: false +call id +-1 is instance of Pos | Int: true +call id +-1 is instance of Pos & String: false +call id +-1 is instance of Pos | String: false +call id +-1 is instance of {v: Int with v == 2} & Int: false +call id +-1 is instance of {v: Int with v == 2} | Int: true +call id +-1 is instance of {v: Int with v == 2} & String: false +call id +-1 is instance of {v: Int with v == 2} | String: false +call id +-1 is instance of NonEmptyString & String: false +call id +-1 is instance of NonEmptyString | String: false +call id +-1 is instance of NonEmptyString & Int: false +call id +-1 is instance of NonEmptyString | Int: true +call id +-1 is instance of PoliteString & Int: false +call id +-1 is instance of PoliteString | Int: true +call id +-1 is instance of PoliteString & String: false +call id +-1 is instance of PoliteString | String: false +call id +1 is instance of Pos: true +call id +1 is instance of {v: Int with v == 2}: false +call id +1 is instance of NonEmptyString: false +call id +1 is instance of PoliteString: false +call id +1 is instance of Pos & Int: true +call id +1 is instance of Pos | Int: true +call id +1 is instance of Pos & String: false +call id +1 is instance of Pos | String: true +call id +1 is instance of {v: Int with v == 2} & Int: false +call id +1 is instance of {v: Int with v == 2} | Int: true +call id +1 is instance of {v: Int with v == 2} & String: false +call id +1 is instance of {v: Int with v == 2} | String: false +call id +1 is instance of NonEmptyString & String: false +call id +1 is instance of NonEmptyString | String: false +call id +1 is instance of NonEmptyString & Int: false +call id +1 is instance of NonEmptyString | Int: true +call id +1 is instance of PoliteString & Int: false +call id +1 is instance of PoliteString | Int: true +call id +1 is instance of PoliteString & String: false +call id +1 is instance of PoliteString | String: false +call id +2 is instance of Pos: true +call id +2 is instance of {v: Int with v == 2}: true +call id +2 is instance of NonEmptyString: false +call id +2 is instance of PoliteString: false +call id +2 is instance of Pos & Int: true +call id +2 is instance of Pos | Int: true +call id +2 is instance of Pos & String: false +call id +2 is instance of Pos | String: true +call id +2 is instance of {v: Int with v == 2} & Int: true +call id +2 is instance of {v: Int with v == 2} | Int: true +call id +2 is instance of {v: Int with v == 2} & String: false +call id +2 is instance of {v: Int with v == 2} | String: true +call id +2 is instance of NonEmptyString & String: false +call id +2 is instance of NonEmptyString | String: false +call id +2 is instance of NonEmptyString & Int: false +call id +2 is instance of NonEmptyString | Int: true +call id +2 is instance of PoliteString & Int: false +call id +2 is instance of PoliteString | Int: true +call id +2 is instance of PoliteString & String: false +call id +2 is instance of PoliteString | String: false +call id +"" is instance of Pos: false +call id +"" is instance of {v: Int with v == 2}: false +call id +"" is instance of NonEmptyString: false +call id +"" is instance of PoliteString: false +call id +"" is instance of Pos & Int: false +call id +"" is instance of Pos | Int: false +call id +"" is instance of Pos & String: false +call id +"" is instance of Pos | String: true +call id +"" is instance of {v: Int with v == 2} & Int: false +call id +"" is instance of {v: Int with v == 2} | Int: false +call id +"" is instance of {v: Int with v == 2} & String: false +call id +"" is instance of {v: Int with v == 2} | String: true +call id +"" is instance of NonEmptyString & String: false +call id +"" is instance of NonEmptyString | String: true +call id +"" is instance of NonEmptyString & Int: false +call id +"" is instance of NonEmptyString | Int: false +call id +"" is instance of PoliteString & Int: false +call id +"" is instance of PoliteString | Int: false +call id +"" is instance of PoliteString & String: false +call id +"" is instance of PoliteString | String: true +call id +"Do it please" is instance of Pos: false +call id +"Do it please" is instance of {v: Int with v == 2}: false +call id +"Do it please" is instance of NonEmptyString: true +call id +"Do it please" is instance of PoliteString: true +call id +"Do it please" is instance of Pos & Int: false +call id +"Do it please" is instance of Pos | Int: false +call id +"Do it please" is instance of Pos & String: false +call id +"Do it please" is instance of Pos | String: true +call id +"Do it please" is instance of {v: Int with v == 2} & Int: false +call id +"Do it please" is instance of {v: Int with v == 2} | Int: false +call id +"Do it please" is instance of {v: Int with v == 2} & String: false +call id +"Do it please" is instance of {v: Int with v == 2} | String: true +call id +"Do it please" is instance of NonEmptyString & String: true +call id +"Do it please" is instance of NonEmptyString | String: true +call id +"Do it please" is instance of NonEmptyString & Int: false +call id +"Do it please" is instance of NonEmptyString | Int: true +call id +"Do it please" is instance of PoliteString & Int: false +call id +"Do it please" is instance of PoliteString | Int: true +call id +"Do it please" is instance of PoliteString & String: true +call id +"Do it please" is instance of PoliteString | String: true +call id +"do it already" is instance of Pos: false +call id +"do it already" is instance of {v: Int with v == 2}: false +call id +"do it already" is instance of NonEmptyString: true +call id +"do it already" is instance of PoliteString: false +call id +"do it already" is instance of Pos & Int: false +call id +"do it already" is instance of Pos | Int: false +call id +"do it already" is instance of Pos & String: false +call id +"do it already" is instance of Pos | String: true +call id +"do it already" is instance of {v: Int with v == 2} & Int: false +call id +"do it already" is instance of {v: Int with v == 2} | Int: false +call id +"do it already" is instance of {v: Int with v == 2} & String: false +call id +"do it already" is instance of {v: Int with v == 2} | String: true +call id +"do it already" is instance of NonEmptyString & String: true +call id +"do it already" is instance of NonEmptyString | String: true +call id +"do it already" is instance of NonEmptyString & Int: false +call id +"do it already" is instance of NonEmptyString | Int: true +call id +"do it already" is instance of PoliteString & Int: false +call id +"do it already" is instance of PoliteString | Int: false +call id +"do it already" is instance of PoliteString & String: false +call id +"do it already" is instance of PoliteString | String: true +call id +false is instance of Pos: false +call id +false is instance of {v: Int with v == 2}: false +call id +false is instance of NonEmptyString: false +call id +false is instance of PoliteString: false +call id +false is instance of Pos & Int: false +call id +false is instance of Pos | Int: false +call id +false is instance of Pos & String: false +call id +false is instance of Pos | String: false +call id +false is instance of {v: Int with v == 2} & Int: false +call id +false is instance of {v: Int with v == 2} | Int: false +call id +false is instance of {v: Int with v == 2} & String: false +call id +false is instance of {v: Int with v == 2} | String: false +call id +false is instance of NonEmptyString & String: false +call id +false is instance of NonEmptyString | String: false +call id +false is instance of NonEmptyString & Int: false +call id +false is instance of NonEmptyString | Int: false +call id +false is instance of PoliteString & Int: false +call id +false is instance of PoliteString | Int: false +call id +false is instance of PoliteString & String: false +call id +false is instance of PoliteString | String: false +call id +null is instance of Pos: false +call id +null is instance of {v: Int with v == 2}: false +call id +null is instance of NonEmptyString: false +call id +null is instance of PoliteString: false +call id +null is instance of Pos & Int: false +call id +null is instance of Pos | Int: false +call id +null is instance of Pos & String: false +call id +null is instance of Pos | String: false +call id +null is instance of {v: Int with v == 2} & Int: false +call id +null is instance of {v: Int with v == 2} | Int: false +call id +null is instance of {v: Int with v == 2} & String: false +call id +null is instance of {v: Int with v == 2} | String: false +call id +null is instance of NonEmptyString & String: false +call id +null is instance of NonEmptyString | String: false +call id +null is instance of NonEmptyString & Int: false +call id +null is instance of NonEmptyString | Int: false +call id +null is instance of PoliteString & Int: false +call id +null is instance of PoliteString | Int: false +call id +null is instance of PoliteString & String: false +call id +null is instance of PoliteString | String: false diff --git a/tests/run-custom-args/qualified-types/isInstanceOf.scala b/tests/run-custom-args/qualified-types/isInstanceOf.scala new file mode 100644 index 000000000000..a893a5b7a3f0 --- /dev/null +++ b/tests/run-custom-args/qualified-types/isInstanceOf.scala @@ -0,0 +1,36 @@ +type Pos = {x: Int with x > 0} + +type NonEmptyString = {s: String with !s.isEmpty} +type PoliteString = {s: NonEmptyString with s.head.isUpper && s.takeRight(6) == "please"} + +def id[T](x: T): T = + println("call id") + x + +@main +def Test = + for v <- List[Any](-1, 1, 2, "", "Do it please", "do it already", false, null) do + val vStr = + if v.isInstanceOf[String] then s""""$v"""" + else if v == null then "null" + else v.toString + println(s"$vStr is instance of Pos: ${id(v).isInstanceOf[Pos]}") + println(s"$vStr is instance of {v: Int with v == 2}: ${id(v).isInstanceOf[{v: Int with v == 2}]}") + println(s"$vStr is instance of NonEmptyString: ${id(v).isInstanceOf[NonEmptyString]}") + println(s"$vStr is instance of PoliteString: ${id(v).isInstanceOf[PoliteString]}") + println(s"$vStr is instance of Pos & Int: ${id(v).isInstanceOf[Pos & Int]}") + println(s"$vStr is instance of Pos | Int: ${id(v).isInstanceOf[Pos | Int]}") + println(s"$vStr is instance of Pos & String: ${id(v).isInstanceOf[Pos & String]}") + println(s"$vStr is instance of Pos | String: ${id(v).isInstanceOf[Pos | String]}") + println(s"$vStr is instance of {v: Int with v == 2} & Int: ${id(v).isInstanceOf[{v: Int with v == 2} & Int]}") + println(s"$vStr is instance of {v: Int with v == 2} | Int: ${id(v).isInstanceOf[{v: Int with v == 2} | Int]}") + println(s"$vStr is instance of {v: Int with v == 2} & String: ${id(v).isInstanceOf[{v: Int with v == 2} & String]}") + println(s"$vStr is instance of {v: Int with v == 2} | String: ${id(v).isInstanceOf[{v: Int with v == 2} | String]}") + println(s"$vStr is instance of NonEmptyString & String: ${id(v).isInstanceOf[NonEmptyString & String]}") + println(s"$vStr is instance of NonEmptyString | String: ${id(v).isInstanceOf[NonEmptyString | String]}") + println(s"$vStr is instance of NonEmptyString & Int: ${id(v).isInstanceOf[NonEmptyString & Int]}") + println(s"$vStr is instance of NonEmptyString | Int: ${id(v).isInstanceOf[NonEmptyString | Int]}") + println(s"$vStr is instance of PoliteString & Int: ${id(v).isInstanceOf[PoliteString & Int]}") + println(s"$vStr is instance of PoliteString | Int: ${id(v).isInstanceOf[PoliteString | Int]}") + println(s"$vStr is instance of PoliteString & String: ${id(v).isInstanceOf[PoliteString & String]}") + println(s"$vStr is instance of PoliteString | String: ${id(v).isInstanceOf[PoliteString | String]}") diff --git a/tests/run-custom-args/qualified-types/pattern_matching.check b/tests/run-custom-args/qualified-types/pattern_matching.check new file mode 100644 index 000000000000..612472cd9501 --- /dev/null +++ b/tests/run-custom-args/qualified-types/pattern_matching.check @@ -0,0 +1,8 @@ +-1 is none of the above +1 is Pos +2 is {v: Int with v == 2} +"" is none of the above +"Do it please" is PoliteString +"do it already" is NonEmptyString +false is none of the above +null is none of the above diff --git a/tests/run-custom-args/qualified-types/pattern_matching.scala b/tests/run-custom-args/qualified-types/pattern_matching.scala new file mode 100644 index 000000000000..43dc87b9dbe9 --- /dev/null +++ b/tests/run-custom-args/qualified-types/pattern_matching.scala @@ -0,0 +1,31 @@ +type Pos = {x: Int with x > 0} + +type NonEmptyString = {s: String with !s.isEmpty} +type PoliteString = {s: NonEmptyString with s.head.isUpper && s.takeRight(6) == "please"} + +def id[T](x: T): T = + println("call id") + x + +def rec(x: NonEmptyString): List[Char] = + val rest = + x.tail match + case xs: NonEmptyString => rec(xs) + case _ => Nil + + x.head :: rest + +@main def Test = + for v <- List[Any](-1, 1, 2, "", "Do it please", "do it already", false, null) do + val vStr = + if v.isInstanceOf[String] then s""""$v"""" + else if v == null then "null" + else v.toString + + v match + case _: {v: Int with v == 2} => println(s"$vStr is {v: Int with v == 2}") + case _: Pos => println(s"$vStr is Pos") + case _: PoliteString => println(s"$vStr is PoliteString") + case _: NonEmptyString => println(s"$vStr is NonEmptyString") + case _ => println(s"$vStr is none of the above") + diff --git a/tests/run-custom-args/qualified-types/pattern_matching_alternative.check b/tests/run-custom-args/qualified-types/pattern_matching_alternative.check new file mode 100644 index 000000000000..fddaa098404b --- /dev/null +++ b/tests/run-custom-args/qualified-types/pattern_matching_alternative.check @@ -0,0 +1,2 @@ +Hello is an Int or a non-empty String +Hello is an Int or a non-empty String diff --git a/tests/run-custom-args/qualified-types/pattern_matching_alternative.scala b/tests/run-custom-args/qualified-types/pattern_matching_alternative.scala new file mode 100644 index 000000000000..5f3feed1ed4b --- /dev/null +++ b/tests/run-custom-args/qualified-types/pattern_matching_alternative.scala @@ -0,0 +1,14 @@ +@main def Test = + val x: Any = "Hello" + + x match + case (_: Int) | (_: {s: String with s.nonEmpty}) => + println(s"$x is an Int or a non-empty String") + case _ => + () + + x match + case _: (Int | {s: String with s.nonEmpty}) => + println(s"$x is an Int or a non-empty String") + case _ => + () diff --git a/tests/run-custom-args/qualified-types/runtimeChecked.scala b/tests/run-custom-args/qualified-types/runtimeChecked.scala new file mode 100644 index 000000000000..a709a0089c09 --- /dev/null +++ b/tests/run-custom-args/qualified-types/runtimeChecked.scala @@ -0,0 +1,18 @@ + +def getInt(): Int = 1 + +@main def Test = + val x: {v: Int with v == 1} = getInt().runtimeChecked + + assertThrows[IllegalArgumentException]: + val v1: {v: Int with v == 2} = x.runtimeChecked + +def assertThrows[T <: Throwable](block: => Unit): Unit = + try + block + catch + case e: Throwable if e.isInstanceOf[T] => + return + case _ => + throw new AssertionError("Unexpected exception") + throw new AssertionError("Expected exception not thrown") diff --git a/tests/run-custom-args/qualified-types/runtimeChecked_dependent.scala b/tests/run-custom-args/qualified-types/runtimeChecked_dependent.scala new file mode 100644 index 000000000000..26ed21311a89 --- /dev/null +++ b/tests/run-custom-args/qualified-types/runtimeChecked_dependent.scala @@ -0,0 +1,14 @@ +def foo(x: Int, y: {v: Int with v > x}): y.type = y + +def getInt(): Int = 1 + +type Pos = {v: Int with v > 0} +type Neg = {v: Int with v < 0} + +import scala.reflect.TypeTest + +@main def Test = + val v1= foo(1, 2.runtimeChecked) + + val p: Int = 1 + val v2 = foo(p, 2.runtimeChecked) diff --git a/tests/run-tasty-inspector/stdlibExperimentalDefinitions.scala b/tests/run-tasty-inspector/stdlibExperimentalDefinitions.scala index 6a867233b49b..23e426986f97 100644 --- a/tests/run-tasty-inspector/stdlibExperimentalDefinitions.scala +++ b/tests/run-tasty-inspector/stdlibExperimentalDefinitions.scala @@ -49,6 +49,9 @@ val experimentalDefinitionInLibrary = Set( "scala.caps.unsafe$", "scala.caps.use", + // Experimental feature: qualified types + "scala.annotation.qualified", + //// New feature: into "scala.Conversion$.into", "scala.Conversion$.underlying",