diff --git a/compiler/src/dotty/tools/dotc/Compiler.scala b/compiler/src/dotty/tools/dotc/Compiler.scala index ce4ed2d4e4e8..46d36c4412c7 100644 --- a/compiler/src/dotty/tools/dotc/Compiler.scala +++ b/compiler/src/dotty/tools/dotc/Compiler.scala @@ -4,6 +4,7 @@ package dotc import core._ import Contexts._ import typer.{TyperPhase, RefChecks} +import cc.CheckCaptures import parsing.Parser import Phases.Phase import transform._ @@ -78,6 +79,10 @@ class Compiler { new SpecializeApplyMethods, // Adds specialized methods to FunctionN new TryCatchPatterns, // Compile cases in try/catch new PatternMatcher) :: // Compile pattern matches + List(new TestRecheck.Pre) :: // Test only: run rechecker, enabled under -Yrecheck-test + List(new TestRecheck) :: // Test only: run rechecker, enabled under -Yrecheck-test + List(new CheckCaptures.Pre) :: // Preparations for check captures phase, enabled under -Ycc + List(new CheckCaptures) :: // Check captures, enabled under -Ycc List(new ElimOpaque, // Turn opaque into normal aliases new sjs.ExplicitJSClasses, // Make all JS classes explicit (Scala.js only) new ExplicitOuter, // Add accessors to outer classes from nested ones. diff --git a/compiler/src/dotty/tools/dotc/Run.scala b/compiler/src/dotty/tools/dotc/Run.scala index 8f0bc395879e..f9152e8294c6 100644 --- a/compiler/src/dotty/tools/dotc/Run.scala +++ b/compiler/src/dotty/tools/dotc/Run.scala @@ -19,7 +19,6 @@ import reporting.{Suppression, Action, Profile, ActiveProfile, NoProfile} import reporting.Diagnostic import reporting.Diagnostic.Warning import rewrites.Rewrites - import profile.Profiler import printing.XprintMode import typer.ImplicitRunInfo @@ -294,7 +293,7 @@ class Run(comp: Compiler, ictx: Context) extends ImplicitRunInfo with Constraint val fusedPhase = ctx.phase.prevMega val echoHeader = f"[[syntax trees at end of $fusedPhase%25s]] // ${unit.source}" val tree = if ctx.isAfterTyper then unit.tpdTree else unit.untpdTree - val treeString = tree.show(using ctx.withProperty(XprintMode, Some(()))) + val treeString = fusedPhase.show(tree) last match { case SomePrintedTree(phase, lastTreeString) if lastTreeString == treeString => diff --git a/compiler/src/dotty/tools/dotc/ast/Desugar.scala b/compiler/src/dotty/tools/dotc/ast/Desugar.scala index 10d4fed7f058..1e1db19bcf25 100644 --- a/compiler/src/dotty/tools/dotc/ast/Desugar.scala +++ b/compiler/src/dotty/tools/dotc/ast/Desugar.scala @@ -468,7 +468,7 @@ object desugar { if mods.is(Trait) then for vparams <- originalVparamss; vparam <- vparams do - if vparam.tpt.isInstanceOf[ByNameTypeTree] then + if isByNameType(vparam.tpt) then report.error(em"implementation restriction: traits cannot have by name parameters", vparam.srcPos) // Annotations on class _type_ parameters are set on the derived parameters @@ -576,9 +576,8 @@ object desugar { appliedTypeTree(tycon, targs) } - def isRepeated(tree: Tree): Boolean = tree match { + def isRepeated(tree: Tree): Boolean = stripByNameType(tree) match { case PostfixOp(_, Ident(tpnme.raw.STAR)) => true - case ByNameTypeTree(tree1) => isRepeated(tree1) case _ => false } @@ -1810,6 +1809,16 @@ object desugar { flatTree(pats1 map (makePatDef(tree, mods, _, rhs))) case ext: ExtMethods => Block(List(ext), Literal(Constant(())).withSpan(ext.span)) + case CapturingTypeTree(refs, parent) => + // convert `{refs} T` to `T @retains refs` + // `{refs}-> T` to `-> (T @retainsByName refs)` + def annotate(annotName: TypeName, tp: Tree) = + Annotated(tp, New(scalaAnnotationDot(annotName), List(refs))) + parent match + case ByNameTypeTree(restpt) => + cpy.ByNameTypeTree(parent)(annotate(tpnme.retainsByName, restpt)) + case _ => + annotate(tpnme.retains, parent) } desugared.withSpan(tree.span) } @@ -1946,6 +1955,8 @@ object desugar { case _ => traverseChildren(tree) } }.traverse(expr) + case CapturingTypeTree(refs, parent) => + collect(parent) case _ => } collect(tree) diff --git a/compiler/src/dotty/tools/dotc/ast/TreeInfo.scala b/compiler/src/dotty/tools/dotc/ast/TreeInfo.scala index 553a73a6f4c6..083a92b26d11 100644 --- a/compiler/src/dotty/tools/dotc/ast/TreeInfo.scala +++ b/compiler/src/dotty/tools/dotc/ast/TreeInfo.scala @@ -178,8 +178,7 @@ trait TreeInfo[T >: Untyped <: Type] { self: Trees.Instance[T] => } /** Is tpt a vararg type of the form T* or => T*? */ - def isRepeatedParamType(tpt: Tree)(using Context): Boolean = tpt match { - case ByNameTypeTree(tpt1) => isRepeatedParamType(tpt1) + def isRepeatedParamType(tpt: Tree)(using Context): Boolean = stripByNameType(tpt) match { case tpt: TypeTree => tpt.typeOpt.isRepeatedParam case AppliedTypeTree(Select(_, tpnme.REPEATED_PARAM_CLASS), _) => true case _ => false @@ -196,6 +195,18 @@ trait TreeInfo[T >: Untyped <: Type] { self: Trees.Instance[T] => case arg => arg.typeOpt.widen.isRepeatedParam } + /** Is tree a type tree of the form `=> T` or (under -Ycc) `{refs}-> T`? */ + def isByNameType(tree: Tree)(using Context): Boolean = + stripByNameType(tree) ne tree + + /** Strip `=> T` to `T` and (under -Ycc) `{refs}-> T` to `T` */ + def stripByNameType(tree: Tree)(using Context): Tree = unsplice(tree) match + case ByNameTypeTree(t1) => t1 + case untpd.CapturingTypeTree(_, parent) => + val parent1 = stripByNameType(parent) + if parent1 eq parent then tree else parent1 + case _ => tree + /** All type and value parameter symbols of this DefDef */ def allParamSyms(ddef: DefDef)(using Context): List[Symbol] = ddef.paramss.flatten.map(_.symbol) @@ -388,6 +399,22 @@ trait UntypedTreeInfo extends TreeInfo[Untyped] { self: Trees.Instance[Untyped] case _ => None } } + + /** Under -Ycc: A builder and extractor for `=> T`, which is an alias for `{*}-> T`. + * Only trees of the form `=> T` are matched; trees written directly as `{*}-> T` + * are ignored by the extractor. + */ + object ImpureByNameTypeTree: + + def apply(tp: ByNameTypeTree)(using Context): untpd.CapturingTypeTree = + untpd.CapturingTypeTree( + Ident(nme.CAPTURE_ROOT).withSpan(tp.span.startPos) :: Nil, tp) + + def unapply(tp: Tree)(using Context): Option[ByNameTypeTree] = tp match + case untpd.CapturingTypeTree(id @ Ident(nme.CAPTURE_ROOT) :: Nil, bntp: ByNameTypeTree) + if id.span == bntp.span.startPos => Some(bntp) + case _ => None + end ImpureByNameTypeTree } trait TypedTreeInfo extends TreeInfo[Type] { self: Trees.Instance[Type] => diff --git a/compiler/src/dotty/tools/dotc/ast/Trees.scala b/compiler/src/dotty/tools/dotc/ast/Trees.scala index 41077637d92e..1159d13d5aef 100644 --- a/compiler/src/dotty/tools/dotc/ast/Trees.scala +++ b/compiler/src/dotty/tools/dotc/ast/Trees.scala @@ -253,16 +253,10 @@ object Trees { /** Tree's denotation can be derived from its type */ abstract class DenotingTree[-T >: Untyped](implicit @constructorOnly src: SourceFile) extends Tree[T] { type ThisTree[-T >: Untyped] <: DenotingTree[T] - override def denot(using Context): Denotation = typeOpt match { + override def denot(using Context): Denotation = typeOpt.stripped match case tpe: NamedType => tpe.denot case tpe: ThisType => tpe.cls.denot - case tpe: AnnotatedType => tpe.stripAnnots match { - case tpe: NamedType => tpe.denot - case tpe: ThisType => tpe.cls.denot - case _ => NoDenotation - } case _ => NoDenotation - } } /** Tree's denot/isType/isTerm properties come from a subtree diff --git a/compiler/src/dotty/tools/dotc/ast/tpd.scala b/compiler/src/dotty/tools/dotc/ast/tpd.scala index 53ad330eea35..52325e36037d 100644 --- a/compiler/src/dotty/tools/dotc/ast/tpd.scala +++ b/compiler/src/dotty/tools/dotc/ast/tpd.scala @@ -164,8 +164,8 @@ object tpd extends Trees.Instance[Type] with TypedTreeInfo { def Inlined(call: Tree, bindings: List[MemberDef], expansion: Tree)(using Context): Inlined = ta.assignType(untpd.Inlined(call, bindings, expansion), bindings, expansion) - def TypeTree(tp: Type)(using Context): TypeTree = - untpd.TypeTree().withType(tp) + def TypeTree(tp: Type, inferred: Boolean = false)(using Context): TypeTree = + (if inferred then untpd.InferredTypeTree() else untpd.TypeTree()).withType(tp) def SingletonTypeTree(ref: Tree)(using Context): SingletonTypeTree = ta.assignType(untpd.SingletonTypeTree(ref), ref) @@ -203,8 +203,8 @@ object tpd extends Trees.Instance[Type] with TypedTreeInfo { ta.assignType(untpd.UnApply(fun, implicits, patterns), proto) } - def ValDef(sym: TermSymbol, rhs: LazyTree = EmptyTree)(using Context): ValDef = - ta.assignType(untpd.ValDef(sym.name, TypeTree(sym.info), rhs), sym) + def ValDef(sym: TermSymbol, rhs: LazyTree = EmptyTree, inferred: Boolean = false)(using Context): ValDef = + ta.assignType(untpd.ValDef(sym.name, TypeTree(sym.info, inferred), rhs), sym) def SyntheticValDef(name: TermName, rhs: Tree, flags: FlagSet = EmptyFlags)(using Context): ValDef = ValDef(newSymbol(ctx.owner, name, Synthetic | flags, rhs.tpe.widen, coord = rhs.span), rhs) diff --git a/compiler/src/dotty/tools/dotc/ast/untpd.scala b/compiler/src/dotty/tools/dotc/ast/untpd.scala index a769df537f1a..6f3f134f9342 100644 --- a/compiler/src/dotty/tools/dotc/ast/untpd.scala +++ b/compiler/src/dotty/tools/dotc/ast/untpd.scala @@ -69,13 +69,13 @@ object untpd extends Trees.Instance[Untyped] with UntypedTreeInfo { case class InterpolatedString(id: TermName, segments: List[Tree])(implicit @constructorOnly src: SourceFile) extends TermTree - /** A function type */ + /** A function type or closure */ case class Function(args: List[Tree], body: Tree)(implicit @constructorOnly src: SourceFile) extends Tree { override def isTerm: Boolean = body.isTerm override def isType: Boolean = body.isType } - /** A function type with `implicit`, `erased`, or `given` modifiers */ + /** A function type or closure with `implicit`, `erased`, or `given` modifiers */ class FunctionWithMods(args: List[Tree], body: Tree, val mods: Modifiers)(implicit @constructorOnly src: SourceFile) extends Function(args, body) @@ -145,6 +145,9 @@ object untpd extends Trees.Instance[Untyped] with UntypedTreeInfo { case Floating } + /** {x1, ..., xN} T (only relevant under -Ycc) */ + case class CapturingTypeTree(refs: List[Tree], parent: Tree)(implicit @constructorOnly src: SourceFile) extends TypTree + /** Short-lived usage in typer, does not need copy/transform/fold infrastructure */ case class DependentTypeTree(tp: List[Symbol] => Type)(implicit @constructorOnly src: SourceFile) extends Tree @@ -213,6 +216,9 @@ object untpd extends Trees.Instance[Untyped] with UntypedTreeInfo { case class Transparent()(implicit @constructorOnly src: SourceFile) extends Mod(Flags.Transparent) case class Infix()(implicit @constructorOnly src: SourceFile) extends Mod(Flags.Infix) + + /** Used under -Ycc to mark impure function types `A => B` in `FunctionWithMods` */ + case class Impure()(implicit @constructorOnly src: SourceFile) extends Mod(Flags.Impure) } /** Modifiers and annotations for definitions @@ -390,6 +396,7 @@ object untpd extends Trees.Instance[Untyped] with UntypedTreeInfo { def JavaSeqLiteral(elems: List[Tree], elemtpt: Tree)(implicit src: SourceFile): JavaSeqLiteral = new JavaSeqLiteral(elems, elemtpt) def Inlined(call: tpd.Tree, bindings: List[MemberDef], expansion: Tree)(implicit src: SourceFile): Inlined = new Inlined(call, bindings, expansion) def TypeTree()(implicit src: SourceFile): TypeTree = new TypeTree() + def InferredTypeTree()(implicit src: SourceFile): TypeTree = new InferredTypeTree() def SingletonTypeTree(ref: Tree)(implicit src: SourceFile): SingletonTypeTree = new SingletonTypeTree(ref) def RefinedTypeTree(tpt: Tree, refinements: List[Tree])(implicit src: SourceFile): RefinedTypeTree = new RefinedTypeTree(tpt, refinements) def AppliedTypeTree(tpt: Tree, args: List[Tree])(implicit src: SourceFile): AppliedTypeTree = new AppliedTypeTree(tpt, args) @@ -647,6 +654,10 @@ object untpd extends Trees.Instance[Untyped] with UntypedTreeInfo { case tree: Number if (digits == tree.digits) && (kind == tree.kind) => tree case _ => finalize(tree, untpd.Number(digits, kind)) } + def CapturingTypeTree(tree: Tree)(refs: List[Tree], parent: Tree)(using Context): Tree = tree match + case tree: CapturingTypeTree if (refs eq tree.refs) && (parent eq tree.parent) => tree + case _ => finalize(tree, untpd.CapturingTypeTree(refs, parent)) + 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)) @@ -710,6 +721,8 @@ object untpd extends Trees.Instance[Untyped] with UntypedTreeInfo { tree case MacroTree(expr) => cpy.MacroTree(tree)(transform(expr)) + case CapturingTypeTree(refs, parent) => + cpy.CapturingTypeTree(tree)(transform(refs), transform(parent)) case _ => super.transformMoreCases(tree) } @@ -769,6 +782,8 @@ object untpd extends Trees.Instance[Untyped] with UntypedTreeInfo { this(x, splice) case MacroTree(expr) => this(x, expr) + case CapturingTypeTree(refs, parent) => + this(this(x, refs), parent) case _ => super.foldMoreCases(x, tree) } diff --git a/compiler/src/dotty/tools/dotc/cc/BoxedTypeCache.scala b/compiler/src/dotty/tools/dotc/cc/BoxedTypeCache.scala new file mode 100644 index 000000000000..56b3f5ba5047 --- /dev/null +++ b/compiler/src/dotty/tools/dotc/cc/BoxedTypeCache.scala @@ -0,0 +1,19 @@ +package dotty.tools +package dotc +package cc + +import core.* +import Types.*, Symbols.*, Contexts.* + +/** A one-element cache for the boxed version of an unboxed capturing type */ +class BoxedTypeCache: + private var boxed: Type = compiletime.uninitialized + private var unboxed: Type = NoType + + def apply(tp: AnnotatedType)(using Context): Type = + if tp ne unboxed then + unboxed = tp + val CapturingType(parent, refs) = tp: @unchecked + boxed = CapturingType(parent, refs, boxed = true) + boxed +end BoxedTypeCache \ No newline at end of file diff --git a/compiler/src/dotty/tools/dotc/cc/CaptureAnnotation.scala b/compiler/src/dotty/tools/dotc/cc/CaptureAnnotation.scala new file mode 100644 index 000000000000..0fd96fe2462c --- /dev/null +++ b/compiler/src/dotty/tools/dotc/cc/CaptureAnnotation.scala @@ -0,0 +1,77 @@ +package dotty.tools +package dotc +package cc + +import core.* +import Types.*, Symbols.*, Contexts.*, Annotations.* +import ast.Trees.* +import ast.{tpd, untpd} +import Decorators.* +import config.Printers.capt +import printing.Printer +import printing.Texts.Text + +/** An annotation representing a capture set and whether it is boxed. + * It simulates a normal @retains annotation except that it is more efficient, + * supports variables as capture sets, and adds a `boxed` flag. + * These annotations are created during capture checking. Before that + * there are only regular @retains and @retainsByName annotations. + * @param refs the capture set + * @param boxed whether the type carrying the annotation is boxed + * @param cls the underlying class (either annotation.retains or annotation.retainsByName) + */ +case class CaptureAnnotation(refs: CaptureSet, boxed: Boolean)(cls: Symbol) extends Annotation: + import CaptureAnnotation.* + import tpd.* + + /** A cache for boxed version of a capturing type with this annotation */ + val boxedType = BoxedTypeCache() + + /** Reconstitute annotation tree from capture set */ + override def tree(using Context) = + val elems = refs.elems.toList.map { + case cr: TermRef => ref(cr) + case cr: TermParamRef => untpd.Ident(cr.paramName).withType(cr) + case cr: ThisType => This(cr.cls) + } + val arg = repeated(elems, TypeTree(defn.AnyType)) + New(symbol.typeRef, arg :: Nil) + + override def symbol(using Context) = cls + + override def derivedAnnotation(tree: Tree)(using Context): Annotation = + unsupported(i"derivedAnnotation(Tree), $tree, $refs") + + def derivedAnnotation(refs: CaptureSet, boxed: Boolean)(using Context): Annotation = + if (this.refs eq refs) && (this.boxed == boxed) then this + else CaptureAnnotation(refs, boxed)(cls) + + override def sameAnnotation(that: Annotation)(using Context): Boolean = that match + case CaptureAnnotation(refs, boxed) => + this.refs == refs && this.boxed == boxed && this.symbol == that.symbol + case _ => false + + override def mapWith(tm: TypeMap)(using Context) = + val elems = refs.elems.toList + val elems1 = elems.mapConserve(tm) + if elems1 eq elems then this + else if elems1.forall(_.isInstanceOf[CaptureRef]) + then derivedAnnotation(CaptureSet(elems1.asInstanceOf[List[CaptureRef]]*), boxed) + else EmptyAnnotation + + override def refersToParamOf(tl: TermLambda)(using Context): Boolean = + refs.elems.exists { + case TermParamRef(tl1, _) => tl eq tl1 + case _ => false + } + + override def toText(printer: Printer): Text = refs.toText(printer) + + override def hash: Int = + (refs.hashCode << 1) | (if boxed then 1 else 0) + + override def eql(that: Annotation) = that match + case that: CaptureAnnotation => (this.refs eq that.refs) && (this.boxed == that.boxed) + case _ => false + +end CaptureAnnotation diff --git a/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala b/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala new file mode 100644 index 000000000000..0ebf7c1c01e9 --- /dev/null +++ b/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala @@ -0,0 +1,168 @@ +package dotty.tools +package dotc +package cc + +import core.* +import Types.*, Symbols.*, Contexts.*, Annotations.*, Flags.* +import ast.{tpd, untpd} +import Decorators.*, NameOps.* +import config.Printers.capt +import util.Property.Key +import tpd.* + +private val Captures: Key[CaptureSet] = Key() +private val BoxedType: Key[BoxedTypeCache] = Key() + +/** The arguments of a @retains or @retainsByName annotation */ +private[cc] def retainedElems(tree: Tree)(using Context): List[Tree] = tree match + case Apply(_, Typed(SeqLiteral(elems, _), _) :: Nil) => elems + case _ => Nil + +/** An exception thrown if a @retains argument is not syntactically a CaptureRef */ +class IllegalCaptureRef(tpe: Type) extends Exception + +extension (tree: Tree) + + /** Map tree with CaptureRef type to its type, throw IllegalCaptureRef otherwise */ + def toCaptureRef(using Context): CaptureRef = tree.tpe match + case ref: CaptureRef => ref + case tpe => throw IllegalCaptureRef(tpe) + + /** Convert a @retains or @retainsByName annotation tree to the capture set it represents. + * For efficience, the result is cached as an Attachment on the tree. + */ + def toCaptureSet(using Context): CaptureSet = + tree.getAttachment(Captures) match + case Some(refs) => refs + case None => + val refs = CaptureSet(retainedElems(tree).map(_.toCaptureRef)*) + .showing(i"toCaptureSet $tree --> $result", capt) + tree.putAttachment(Captures, refs) + refs + +extension (tp: Type) + + /** @pre `tp` is a CapturingType */ + def derivedCapturingType(parent: Type, refs: CaptureSet)(using Context): Type = tp match + case tp @ CapturingType(p, r) => + if (parent eq p) && (refs eq r) then tp + else CapturingType(parent, refs, tp.isBoxed) + + /** If this is a unboxed capturing type with nonempty capture set, its boxed version. + * Or, if type is a TypeBounds of capturing types, the version where the bounds are boxed. + * The identity for all other types. + */ + def boxed(using Context): Type = tp.dealias match + case tp @ CapturingType(parent, refs) if !tp.isBoxed && !refs.isAlwaysEmpty => + tp.annot match + case ann: CaptureAnnotation => + ann.boxedType(tp) + case ann => + ann.tree.getAttachment(BoxedType) match + case None => ann.tree.putAttachment(BoxedType, BoxedTypeCache()) + case _ => + ann.tree.attachment(BoxedType)(tp) + case tp: RealTypeBounds => + tp.derivedTypeBounds(tp.lo.boxed, tp.hi.boxed) + case _ => + tp + + /** If `sym` is a type parameter, the boxed version of `tp`, otherwise `tp` */ + def boxedIfTypeParam(sym: Symbol)(using Context) = + if sym.is(TypeParam) then tp.boxed else tp + + /** The boxed version of `tp`, unless `tycon` is a function symbol */ + def boxedUnlessFun(tycon: Type)(using Context) = + if ctx.phase != Phases.checkCapturesPhase || defn.isFunctionSymbol(tycon.typeSymbol) + then tp + else tp.boxed + + /** The capture set consisting of all top-level captures of `tp` that appear under a box. + * Unlike for `boxed` this also considers parents of capture types, unions and + * intersections, and type proxies other than abstract types. + */ + def boxedCaptureSet(using Context): CaptureSet = + def getBoxed(tp: Type): CaptureSet = tp match + case tp @ CapturingType(parent, refs) => + val pcs = getBoxed(parent) + if tp.isBoxed then refs ++ pcs else pcs + case tp: TypeRef if tp.symbol.isAbstractType => CaptureSet.empty + case tp: TypeProxy => getBoxed(tp.superType) + case tp: AndType => getBoxed(tp.tp1) ** getBoxed(tp.tp2) + case tp: OrType => getBoxed(tp.tp1) ++ getBoxed(tp.tp2) + case _ => CaptureSet.empty + getBoxed(tp) + + /** Is the boxedCaptureSet of this type nonempty? */ + def isBoxedCapturing(using Context) = !tp.boxedCaptureSet.isAlwaysEmpty + + /** Map capturing type to their parents. Capturing types accessible + * via dealising are also stripped. + */ + def stripCapturing(using Context): Type = tp.dealiasKeepAnnots match + case CapturingType(parent, _) => + parent.stripCapturing + case atd @ AnnotatedType(parent, annot) => + atd.derivedAnnotatedType(parent.stripCapturing, annot) + case _ => + tp + + /** Under -Ycc, map regular function type to impure function type + */ + def adaptFunctionTypeUnderCC(using Context): Type = tp match + case AppliedType(fn, args) + if ctx.settings.Ycc.value && defn.isFunctionClass(fn.typeSymbol) => + val fname = fn.typeSymbol.name + defn.FunctionType( + fname.functionArity, + isContextual = fname.isContextFunction, + isErased = fname.isErasedFunction, + isImpure = true).appliedTo(args) + case _ => + tp + +extension (sym: Symbol) + + /** Does this symbol allow results carrying the universal capability? + * Currently this is true only for function type applies (since their + * results are unboxed) and `erasedValue` since this function is magic in + * that is allows to conjure global capabilies from nothing (aside: can we find a + * more controlled way to achieve this?). + * But it could be generalized to other functions that so that they can take capability + * classes as arguments. + */ + def allowsRootCapture(using Context): Boolean = + sym == defn.Compiletime_erasedValue + || defn.isFunctionClass(sym.maybeOwner) + + /** When applying `sym`, would the result type be unboxed? + * This is the case if the result type contains a top-level reference to an enclosing + * class or method type parameter and the method does not allow root capture. + * If the type parameter is instantiated to a boxed type, that type would + * have to be unboxed in the method's result. + */ + def unboxesResult(using Context): Boolean = + def containsEnclTypeParam(tp: Type): Boolean = tp.strippedDealias match + case tp @ TypeRef(pre: ThisType, _) => tp.symbol.is(Param) + case tp: TypeParamRef => true + case tp: AndOrType => containsEnclTypeParam(tp.tp1) || containsEnclTypeParam(tp.tp2) + case tp: RefinedType => containsEnclTypeParam(tp.parent) || containsEnclTypeParam(tp.refinedInfo) + case _ => false + containsEnclTypeParam(sym.info.finalResultType) + && !sym.allowsRootCapture + +extension (tp: AnnotatedType) + /** Is this a boxed capturing type? */ + def isBoxed(using Context): Boolean = tp.annot match + case ann: CaptureAnnotation => ann.boxed + case _ => false + +extension (ts: List[Type]) + /** Equivalent to ts.mapconserve(_.boxedUnlessFun(tycon)) but more efficient where + * it is the identity. + */ + def boxedUnlessFun(tycon: Type)(using Context) = + if ctx.phase != Phases.checkCapturesPhase || defn.isFunctionClass(tycon.typeSymbol) + then ts + else ts.mapconserve(_.boxed) + diff --git a/compiler/src/dotty/tools/dotc/cc/CaptureSet.scala b/compiler/src/dotty/tools/dotc/cc/CaptureSet.scala new file mode 100644 index 000000000000..fb726a73c486 --- /dev/null +++ b/compiler/src/dotty/tools/dotc/cc/CaptureSet.scala @@ -0,0 +1,898 @@ +package dotty.tools +package dotc +package cc + +import core.* +import Types.*, Symbols.*, Flags.*, Contexts.*, Decorators.* +import config.Printers.capt +import Annotations.Annotation +import annotation.threadUnsafe +import annotation.constructorOnly +import annotation.internal.sharable +import reporting.trace +import printing.{Showable, Printer} +import printing.Texts.* +import util.{SimpleIdentitySet, Property} +import util.common.alwaysTrue +import scala.collection.mutable +import config.Config.ccAllowUnsoundMaps + +/** A class for capture sets. Capture sets can be constants or variables. + * Capture sets support inclusion constraints <:< where <:< is subcapturing. + * + * They also allow + * - mapping with functions from elements to capture sets + * - filtering with predicates on elements + * - intersecting wo capture sets + * + * That is, constraints can be of the forms + * + * cs1 <:< cs2 + * cs1 = ∪ {f(x) | x ∈ cs2} where f is a function from capture references to capture sets. + * cs1 = ∪ {x | x ∈ cs2, p(x)} where p is a predicate on capture references + * cs1 = cs2 ∩ cs2 + * + * We call the resulting constraint system "monadic set constraints". + * To support capture propagation across maps, mappings are supported only + * if the mapped function is either a bijection or if it is idempotent + * on capture references (c.f. doc comment on `map` below). + */ +sealed abstract class CaptureSet extends Showable: + import CaptureSet.* + + /** The elements of this capture set. For capture variables, + * the elements known so far. + */ + def elems: Refs + + /** Is this capture set constant (i.e. not an unsolved capture variable)? + * Solved capture variables count as constant. + */ + def isConst: Boolean + + /** Is this capture set always empty? For unsolved capture veriables, returns + * always false. + */ + def isAlwaysEmpty: Boolean + + /** Is this capture set definitely non-empty? */ + final def isNotEmpty: Boolean = !elems.isEmpty + + /** Convert to Const. @pre: isConst */ + def asConst: Const = this match + case c: Const => c + case v: Var => + assert(v.isConst) + Const(v.elems) + + /** Cast to variable. @pre: !isConst */ + def asVar: Var = + assert(!isConst) + asInstanceOf[Var] + + /** Does this capture set contain the root reference `*` as element? */ + final def isUniversal(using Context) = + elems.exists { + case ref: TermRef => ref.symbol == defn.captureRoot + case _ => false + } + + /** Add new elements to this capture set if allowed. + * @pre `newElems` is not empty and does not overlap with `this.elems`. + * Constant capture sets never allow to add new elements. + * Variables allow it if and only if the new elements can be included + * in all their dependent sets. + * @param origin The set where the elements come from, or `empty` if not known. + * @return CompareResult.OK if elements were added, or a conflicting + * capture set that prevents addition otherwise. + */ + protected def addNewElems(newElems: Refs, origin: CaptureSet)(using Context, VarState): CompareResult + + /** If this is a variable, add `cs` as a dependent set */ + protected def addDependent(cs: CaptureSet)(using Context, VarState): CompareResult + + /** If `cs` is a variable, add this capture set as one of its dependent sets */ + protected def addAsDependentTo(cs: CaptureSet)(using Context): this.type = + cs.addDependent(this)(using ctx, UnrecordedState) + this + + /** Try to include all references of `elems` that are not yet accounted for by this + * capture set. Inclusion is via `addNewElems`. + * @param origin The set where the elements come from, or `empty` if not known. + * @return CompareResult.OK if all unaccounted elements could be added, + * capture set that prevents addition otherwise. + */ + protected final def tryInclude(elems: Refs, origin: CaptureSet)(using Context, VarState): CompareResult = + val unaccounted = elems.filter(!accountsFor(_)) + if unaccounted.isEmpty then CompareResult.OK + else addNewElems(unaccounted, origin) + + /** Equivalent to `tryInclude({elem}, origin)`, but more efficient */ + protected final def tryInclude(elem: CaptureRef, origin: CaptureSet)(using Context, VarState): CompareResult = + if accountsFor(elem) then CompareResult.OK + else addNewElems(elem.singletonCaptureSet.elems, origin) + + /* x subsumes y if x is the same as y, or x is a this reference and y refers to a field of x */ + extension (x: CaptureRef) private def subsumes(y: CaptureRef) = + (x eq y) + || y.match + case y: TermRef => y.prefix eq x + case _ => false + + /** {x} <:< this where <:< is subcapturing, but treating all variables + * as frozen. + */ + def accountsFor(x: CaptureRef)(using Context): Boolean = + reporting.trace(i"$this accountsFor $x, ${x.captureSetOfInfo}?", show = true) { + elems.exists(_.subsumes(x)) + || !x.isRootCapability && x.captureSetOfInfo.subCaptures(this, frozen = true).isOK + } + + /** A more optimistic version of accountsFor, which does not take variable supersets + * of the `x` reference into account. A set might account for `x` if it accounts + * for `x` in a state where we assume all supersets of `x` have just the elements + * known at this point. On the other hand if x's capture set has no known elements, + * a set `cs` might account for `x` only if it subsumes `x` or it contains the + * root capability `*`. + */ + def mightAccountFor(x: CaptureRef)(using Context): Boolean = + reporting.trace(i"$this mightAccountFor $x, ${x.captureSetOfInfo}?", show = true) { + elems.exists(elem => elem.subsumes(x) || elem.isRootCapability) + || !x.isRootCapability + && { + val elems = x.captureSetOfInfo.elems + !elems.isEmpty && elems.forall(mightAccountFor) + } + } + + /** A more optimistic version of subCaptures used to choose one of two typing rules + * for selections and applications. `cs1 mightSubcapture cs2` if `cs2` might account for + * every element currently known to be in `cs1`. + */ + def mightSubcapture(that: CaptureSet)(using Context): Boolean = + elems.forall(that.mightAccountFor) + + /** The subcapturing test. + * @param frozen if true, no new variables or dependent sets are allowed to + * be added when making this test. An attempt to add either + * will result in failure. + */ + final def subCaptures(that: CaptureSet, frozen: Boolean)(using Context): CompareResult = + subCaptures(that)(using ctx, if frozen then FrozenState else VarState()) + + /** The subcapturing test, using a given VarState */ + private def subCaptures(that: CaptureSet)(using Context, VarState): CompareResult = + def recur(elems: List[CaptureRef]): CompareResult = elems match + case elem :: elems1 => + var result = that.tryInclude(elem, this) + if !result.isOK && !elem.isRootCapability && summon[VarState] != FrozenState then + result = elem.captureSetOfInfo.subCaptures(that) + if result.isOK then + recur(elems1) + else + varState.rollBack() + result + case Nil => + addDependent(that) + recur(elems.toList) + .showing(i"subcaptures $this <:< $that = ${result.show}", capt) + + /** Two capture sets are considered =:= equal if they mutually subcapture each other + * in a frozen state. + */ + def =:= (that: CaptureSet)(using Context): Boolean = + this.subCaptures(that, frozen = true).isOK + && that.subCaptures(this, frozen = true).isOK + + /** The smallest capture set (via <:<) that is a superset of both + * `this` and `that` + */ + def ++ (that: CaptureSet)(using Context): CaptureSet = + if this.subCaptures(that, frozen = true).isOK then that + else if that.subCaptures(this, frozen = true).isOK then this + else if this.isConst && that.isConst then Const(this.elems ++ that.elems) + else Var(this.elems ++ that.elems).addAsDependentTo(this).addAsDependentTo(that) + + /** The smallest superset (via <:<) of this capture set that also contains `ref`. + */ + def + (ref: CaptureRef)(using Context): CaptureSet = + this ++ ref.singletonCaptureSet + + /** The largest capture set (via <:<) that is a subset of both `this` and `that` + */ + def **(that: CaptureSet)(using Context): CaptureSet = + if this.subCaptures(that, frozen = true).isOK then this + else if that.subCaptures(this, frozen = true).isOK then that + else if this.isConst && that.isConst then Const(elemIntersection(this, that)) + else Intersected(this, that) + + /** The largest subset (via <:<) of this capture set that does not account for + * any of the elements in the constant capture set `that` + */ + def -- (that: CaptureSet.Const)(using Context): CaptureSet = + val elems1 = elems.filter(!that.accountsFor(_)) + if elems1.size == elems.size then this + else if this.isConst then Const(elems1) + else Diff(asVar, that) + + /** The largest subset (via <:<) of this capture set that does not account for `ref` */ + def - (ref: CaptureRef)(using Context): CaptureSet = + this -- ref.singletonCaptureSet + + /** The largest subset (via <:<) of this capture set that only contains elements + * for which `p` is true. + */ + def filter(p: CaptureRef => Boolean)(using Context): CaptureSet = + if this.isConst then + val elems1 = elems.filter(p) + if elems1 == elems then this + else Const(elems.filter(p)) + else Filtered(asVar, p) + + /** Capture set obtained by applying `tm` to all elements of the current capture set + * and joining the results. If the current capture set is a variable, the same + * transformation is applied to all future additions of new elements. + * + * Note: We have a problem how we handle the situation where we have a mapped set + * + * cs2 = tm(cs1) + * + * and then the propagation solver adds a new element `x` to `cs2`. What do we + * know in this case about `cs1`? We can answer this question in a sound way only + * if `tm` is a bijection on capture references or it is idempotent on capture references. + * (see definition in IdempotentCapRefMap). + * If `tm` is a bijection we know that `tm^-1(x)` must be in `cs1`. If `tm` is idempotent + * one possible solution is that `x` is in `cs1`, which is what we assume in this case. + * That strategy is sound but not complete. + * + * If `tm` is some other map, we don't know how to handle this case. For now, + * we simply refuse to handle other maps. If they do need to be handled, + * `OtherMapped` provides some approximation to a solution, but it is neither + * sound nor complete. + */ + def map(tm: TypeMap)(using Context): CaptureSet = tm match + case tm: BiTypeMap => + val mappedElems = elems.map(tm.forward) + if isConst then + if mappedElems == elems then this + else Const(mappedElems) + else BiMapped(asVar, tm, mappedElems) + case tm: IdentityCaptRefMap => + this + case _ => + val mapped = mapRefs(elems, tm, tm.variance) + if isConst then + if mapped.isConst && mapped.elems == elems then this + else mapped + else Mapped(asVar, tm, tm.variance, mapped) + + /** A mapping resulting from substituting parameters of a BindingType to a list of types */ + def substParams(tl: BindingType, to: List[Type])(using Context) = + map(Substituters.SubstParamsMap(tl, to)) + + /** Invoke handler if this set has (or later aquires) the root capability `*` */ + def disallowRootCapability(handler: () => Unit)(using Context): this.type = + if isUniversal then handler() + this + + /** An upper approximation of this capture set, i.e. a constant set that is + * subcaptured by this set. If the current set is a variable + * it is the intersection of all upper approximations of known supersets + * of the variable. + * The upper approximation is meaningful only if it is constant. If not, + * `upperApprox` can return an arbitrary capture set variable. + * `upperApprox` is used in `solve`. + */ + protected def upperApprox(origin: CaptureSet)(using Context): CaptureSet + + /** Assuming set this set dependds on was just solved to be constant, propagate this info + * to this set. This might result in the set being solved to be constant + * itself. + */ + protected def propagateSolved()(using Context): Unit = () + + /** This capture set with a description that tells where it comes from */ + def withDescription(description: String): CaptureSet + + /** The provided description (using `withDescription`) for this capture set or else "" */ + def description: String + + /** A regular @retains or @retainsByName annotation with the elements of this set as arguments. */ + def toRegularAnnotation(cls: Symbol)(using Context): Annotation = + Annotation(CaptureAnnotation(this, boxed = false)(cls).tree) + + override def toText(printer: Printer): Text = + Str("{") ~ Text(elems.toList.map(printer.toTextCaptureRef), ", ") ~ Str("}") ~~ description + +object CaptureSet: + type Refs = SimpleIdentitySet[CaptureRef] + type Vars = SimpleIdentitySet[Var] + type Deps = SimpleIdentitySet[CaptureSet] + + @sharable private var varId = 0 + + /** If set to `true`, capture stack traces that tell us where sets are created */ + private final val debugSets = false + + private val emptySet = SimpleIdentitySet.empty + + /** The empty capture set `{}` */ + val empty: CaptureSet.Const = Const(emptySet) + + /** The universal capture set `{*}` */ + def universal(using Context): CaptureSet = + defn.captureRoot.termRef.singletonCaptureSet + + /** Used as a recursion brake */ + @sharable private[dotc] val Pending = Const(SimpleIdentitySet.empty) + + def apply(elems: CaptureRef*)(using Context): CaptureSet.Const = + if elems.isEmpty then empty + else Const(SimpleIdentitySet(elems.map(_.normalizedRef)*)) + + def apply(elems: Refs)(using Context): CaptureSet.Const = + if elems.isEmpty then empty else Const(elems) + + /** The subclass of constant capture sets with given elements `elems` */ + class Const private[CaptureSet] (val elems: Refs, val description: String = "") extends CaptureSet: + def isConst = true + def isAlwaysEmpty = elems.isEmpty + + def addNewElems(elems: Refs, origin: CaptureSet)(using Context, VarState): CompareResult = + CompareResult.fail(this) + + def addDependent(cs: CaptureSet)(using Context, VarState) = CompareResult.OK + + def upperApprox(origin: CaptureSet)(using Context): CaptureSet = this + + def withDescription(description: String): Const = Const(elems, description) + + override def toString = elems.toString + end Const + + /** The subclass of captureset variables with given initial elements */ + class Var(initialElems: Refs = emptySet) extends CaptureSet: + + /** A unique identification number for diagnostics */ + val id = + varId += 1 + varId + + /** A variable is solved if it is aproximated to a from-then-on constant set. */ + private var isSolved: Boolean = false + + /** The elements currently known to be in the set */ + var elems: Refs = initialElems + + /** The sets currently known to be dependent sets (i.e. new additions to this set + * are propagated to these dependent sets.) + */ + var deps: Deps = emptySet + + def isConst = isSolved + def isAlwaysEmpty = false + + /** A handler to be invoked if the root reference `*` is added to this set */ + var addRootHandler: () => Unit = () => () + + var description: String = "" + + /** Record current elements in given VarState provided it does not yet + * contain an entry for this variable. + */ + private def recordElemsState()(using VarState): Boolean = + varState.getElems(this) match + case None => varState.putElems(this, elems) + case _ => true + + /** Record current dependent sets in given VarState provided it does not yet + * contain an entry for this variable. + */ + private[CaptureSet] def recordDepsState()(using VarState): Boolean = + varState.getDeps(this) match + case None => varState.putDeps(this, deps) + case _ => true + + /** Reset elements to what was recorded in `state` */ + def resetElems()(using state: VarState): Unit = + elems = state.elems(this) + + /** Reset dependent sets to what was recorded in `state` */ + def resetDeps()(using state: VarState): Unit = + deps = state.deps(this) + + def addNewElems(newElems: Refs, origin: CaptureSet)(using Context, VarState): CompareResult = + if !isConst && recordElemsState() then + elems ++= newElems + if isUniversal then addRootHandler() + // assert(id != 2 || elems.size != 2, this) + (CompareResult.OK /: deps) { (r, dep) => + r.andAlso(dep.tryInclude(newElems, this)) + } + else // fail if variable is solved or given VarState is frozen + CompareResult.fail(this) + + def addDependent(cs: CaptureSet)(using Context, VarState): CompareResult = + if (cs eq this) || cs.isUniversal || isConst then + CompareResult.OK + else if recordDepsState() then + deps += cs + CompareResult.OK + else + CompareResult.fail(this) + + override def disallowRootCapability(handler: () => Unit)(using Context): this.type = + addRootHandler = handler + super.disallowRootCapability(handler) + + private var computingApprox = false + + /** Roughly: the intersection of all constant known supersets of this set. + * The aim is to find an as-good-as-possible constant set that is a superset + * of this set. The universal set {*} is a sound fallback. + */ + final def upperApprox(origin: CaptureSet)(using Context): CaptureSet = + if computingApprox then universal + else if isConst then this + else + computingApprox = true + try computeApprox(origin).ensuring(_.isConst) + finally computingApprox = false + + /** The intersection of all upper approximations of dependent sets */ + protected def computeApprox(origin: CaptureSet)(using Context): CaptureSet = + (universal /: deps) { (acc, sup) => acc ** sup.upperApprox(this) } + + /** Widen the variable's elements to its upper approximation and + * mark it as constant from now on. This is used for contra-variant type variables + * in the results of defs and vals. + */ + def solve()(using Context): Unit = + if !isConst then + val approx = upperApprox(empty) + //println(i"solving var $this $approx ${approx.isConst} deps = ${deps.toList}") + val newElems = approx.elems -- elems + if newElems.isEmpty || addNewElems(newElems, empty)(using ctx, VarState()).isOK then + markSolved() + + /** Mark set as solved and propagate this info to all dependent sets */ + def markSolved()(using Context): Unit = + isSolved = true + deps.foreach(_.propagateSolved()) + + def withDescription(description: String): this.type = + this.description = + if this.description.isEmpty then description + else s"${this.description} and $description" + this + + /** Used for diagnostics and debugging: A string that traces the creation + * history of a variable by following source links. Each variable on the + * path is characterized by the variable's id and the first letter of the + * variable's class name. The path ends in a plain variable with letter `V` that + * is not derived from some other variable. + */ + protected def ids(using Context): String = + val trail = this.match + case dv: DerivedVar => dv.source.ids + case _ => "" + s"$id${getClass.getSimpleName.nn.take(1)}$trail" + + /** Adds variables to the ShownVars context property if that exists, which + * establishes a record of all variables printed in an error message. + * Prints variables wih ids under -Ycc-debug. + */ + override def toText(printer: Printer): Text = inContext(printer.printerContext) { + for vars <- ctx.property(ShownVars) do vars += this + super.toText(printer) ~ (Str(ids) provided !isConst && ctx.settings.YccDebug.value) + } + + override def toString = s"Var$id$elems" + end Var + + /** A variable that is derived from some other variable via a map or filter. */ + abstract class DerivedVar(initialElems: Refs)(using @constructorOnly ctx: Context) + extends Var(initialElems): + + // For debugging: A trace where a set was created. Note that logically it would make more + // sense to place this variable in Mapped, but that runs afoul of the initializatuon checker. + val stack = if debugSets && this.isInstanceOf[Mapped] then (new Throwable).getStackTrace().nn.take(20) else null + + /** The variable from which this variable is derived */ + def source: Var + + addAsDependentTo(source) + + override def propagateSolved()(using Context) = + if source.isConst && !isConst then markSolved() + end DerivedVar + + /** A variable that changes when `source` changes, where all additional new elements are mapped + * using ∪ { tm(x) | x <- source.elems }. + * @param source the original set that is mapped + * @param tm the type map, which is assumed to be idempotent on capture refs + * (except if ccUnsoundMaps is enabled) + * @param variance the assumed variance with which types with capturesets of size >= 2 are approximated + * (i.e. co: full capture set, contra: empty set, nonvariant is not allowed.) + * @param initial The initial mappings of source's elements at the point the Mapped set is created. + */ + class Mapped private[CaptureSet] + (val source: Var, tm: TypeMap, variance: Int, initial: CaptureSet)(using @constructorOnly ctx: Context) + extends DerivedVar(initial.elems): + addAsDependentTo(initial) // initial mappings could change by propagation + + private def mapIsIdempotent = tm.isInstanceOf[IdempotentCaptRefMap] + + assert(ccAllowUnsoundMaps || mapIsIdempotent) + + private def whereCreated(using Context): String = + if stack == null then "" + else i""" + |Stack trace of variable creation:" + |${stack.mkString("\n")}""" + + override def addNewElems(newElems: Refs, origin: CaptureSet)(using Context, VarState): CompareResult = + val added = + if origin eq source then // elements have to be mapped + mapRefs(newElems, tm, variance) + else + // elements are added by subcapturing propagation with this Mapped set + // as superset; no mapping is necessary or allowed. + Const(newElems) + super.addNewElems(added.elems, origin) + .andAlso { + if added.isConst then CompareResult.OK + else if added.asVar.recordDepsState() then { addAsDependentTo(added); CompareResult.OK } + else CompareResult.fail(this) + } + .andAlso { + if (origin ne source) && mapIsIdempotent then + // `tm` is idempotent, propagate back elems from image set. + // This is sound, since we know that for `r in newElems: tm(r) = r`, hence + // `r` is _one_ possible solution in `source` that would make an `r` appear in this set. + // It's not necessarily the only possible solution, so the scheme is incomplete. + source.tryInclude(newElems, this) + else if !mapIsIdempotent && variance <= 0 && !origin.isConst && (origin ne initial) && (origin ne source) then + // The map is neither a BiTypeMap nor an idempotent type map. + // In that case there's no much we can do. + // The scheme then does not propagate added elements back to source and rejects adding + // elements from variable sources in contra- and non-variant positions. In essence, + // we approximate types resulting from such maps by returning a possible super type + // from the actual type. But this is neither sound nor complete. + report.warning(i"trying to add elems ${CaptureSet(newElems)} from unrecognized source $origin of mapped set $this$whereCreated") + CompareResult.fail(this) + else + CompareResult.OK + } + + override def computeApprox(origin: CaptureSet)(using Context): CaptureSet = + if source eq origin then + // it's a mapping of origin, so not a superset of `origin`, + // therefore don't contribute to the intersection. + universal + else + source.upperApprox(this).map(tm) + + override def propagateSolved()(using Context) = + if initial.isConst then super.propagateSolved() + + override def toString = s"Mapped$id($source, elems = $elems)" + end Mapped + + /** A mapping where the type map is required to be a bijection. + * Parameters as in Mapped. + */ + final class BiMapped private[CaptureSet] + (val source: Var, bimap: BiTypeMap, initialElems: Refs)(using @constructorOnly ctx: Context) + extends DerivedVar(initialElems): + + override def addNewElems(newElems: Refs, origin: CaptureSet)(using Context, VarState): CompareResult = + if origin eq source then + super.addNewElems(newElems.map(bimap.forward), origin) + else + super.addNewElems(newElems, origin) + .andAlso { + source.tryInclude(newElems.map(bimap.backward), this) + .showing(i"propagating new elems ${CaptureSet(newElems)} backward from $this to $source", capt) + } + + /** For a BiTypeMap, supertypes of the mapped type also constrain + * the source via the inverse type mapping and vice versa. That is, if + * B = f(A) and B <: C, then A <: f^-1(C), so C should flow into + * the upper approximation of A. + * Conversely if A <: C2, then we also know that B <: f(C2). + * These situations are modeled by the two branches of the conditional below. + */ + override def computeApprox(origin: CaptureSet)(using Context): CaptureSet = + val supApprox = super.computeApprox(this) + if source eq origin then supApprox.map(bimap.inverseTypeMap) + else source.upperApprox(this).map(bimap) ** supApprox + + override def toString = s"BiMapped$id($source, elems = $elems)" + end BiMapped + + /** A variable with elements given at any time as { x <- source.elems | p(x) } */ + class Filtered private[CaptureSet] + (val source: Var, p: CaptureRef => Boolean)(using @constructorOnly ctx: Context) + extends DerivedVar(source.elems.filter(p)): + + override def addNewElems(newElems: Refs, origin: CaptureSet)(using Context, VarState): CompareResult = + val filtered = newElems.filter(p) + if origin eq source then + super.addNewElems(filtered, origin) + else + // Filtered elements have to be back-propagated to source. + // Elements that don't satisfy `p` are not allowed. + super.addNewElems(newElems, origin) + .andAlso { + if filtered.size == newElems.size then source.tryInclude(newElems, this) + else CompareResult.fail(this) + } + + override def computeApprox(origin: CaptureSet)(using Context): CaptureSet = + if source eq origin then + // it's a filter of origin, so not a superset of `origin`, + // therefore don't contribute to the intersection. + universal + else + source.upperApprox(this).filter(p) + + override def toString = s"${getClass.getSimpleName}$id($source, elems = $elems)" + end Filtered + + /** A variable with elements given at any time as { x <- source.elems | !other.accountsFor(x) } */ + class Diff(source: Var, other: Const)(using Context) + extends Filtered(source, !other.accountsFor(_)) + + class Intersected(cs1: CaptureSet, cs2: CaptureSet)(using Context) + extends Var(elemIntersection(cs1, cs2)): + addAsDependentTo(cs1) + addAsDependentTo(cs2) + deps += cs1 + deps += cs2 + + override def addNewElems(newElems: Refs, origin: CaptureSet)(using Context, VarState): CompareResult = + val added = + if origin eq cs1 then newElems.filter(cs2.accountsFor) + else if origin eq cs2 then newElems.filter(cs1.accountsFor) + else newElems + // If origin is not cs1 or cs2, then newElems will be propagated to + // cs1, cs2 since they are in deps. + super.addNewElems(added, origin) + + override def computeApprox(origin: CaptureSet)(using Context): CaptureSet = + if (origin eq cs1) || (origin eq cs2) then + // it's a combination of origin with some other set, so not a superset of `origin`, + // therefore don't contribute to the intersection. + universal + else + CaptureSet(elemIntersection(cs1.upperApprox(this), cs2.upperApprox(this))) + + override def propagateSolved()(using Context) = + if cs1.isConst && cs2.isConst && !isConst then markSolved() + end Intersected + + def elemIntersection(cs1: CaptureSet, cs2: CaptureSet)(using Context): Refs = + cs1.elems.filter(cs2.mightAccountFor) ++ cs2.elems.filter(cs1.mightAccountFor) + + /** Extrapolate tm(r) according to `variance`. Let r1 be the result of tm(r). + * - If r1 is a tracked CaptureRef, return {r1} + * - If r1 has an empty capture set, return {} + * - Otherwise, + * - if the variance is covariant, return r1's capture set + * - if the variance is contravariant, return {} + * - Otherwise assertion failure + */ + def extrapolateCaptureRef(r: CaptureRef, tm: TypeMap, variance: Int)(using Context): CaptureSet = + val r1 = tm(r) + val upper = r1.captureSet + def isExact = + upper.isAlwaysEmpty || upper.isConst && upper.elems.size == 1 && upper.elems.contains(r1) + if variance > 0 || isExact then upper + else if variance < 0 then CaptureSet.empty + else assert(false, i"trying to add $upper from $r via ${tm.getClass} in a non-variant setting") + + /** Apply `f` to each element in `xs`, and join result sets with `++` */ + def mapRefs(xs: Refs, f: CaptureRef => CaptureSet)(using Context): CaptureSet = + ((empty: CaptureSet) /: xs)((cs, x) => cs ++ f(x)) + + /** Apply extrapolated `tm` to each element in `xs`, and join result sets with `++` */ + def mapRefs(xs: Refs, tm: TypeMap, variance: Int)(using Context): CaptureSet = + mapRefs(xs, extrapolateCaptureRef(_, tm, variance)) + + /** Return true iff + * - arg1 is a TypeBounds >: CL T <: CH T of two capturing types with equal parents. + * - arg2 is a capturing type CA U + * - CH <: CA <: CL + * In other words, we can unify CL, CH and CA. + */ + def subCapturesRange(arg1: TypeBounds, arg2: Type)(using Context): Boolean = arg1 match + case TypeBounds(CapturingType(lo, loRefs), CapturingType(hi, hiRefs)) if lo =:= hi => + given VarState = VarState() + val cs2 = arg2.captureSet + hiRefs.subCaptures(cs2).isOK && cs2.subCaptures(loRefs).isOK + case _ => + false + + /** A TypeMap with the property that every capture reference in the image + * of the map is mapped to itself. I.e. for all capture references r1, r2, + * if M(r1) == r2 then M(r2) == r2. + */ + trait IdempotentCaptRefMap extends TypeMap + + /** A TypeMap that is the identity on capture references */ + trait IdentityCaptRefMap extends TypeMap + + type CompareResult = CompareResult.TYPE + + /** The result of subcapturing comparisons is an opaque type CompareResult.TYPE. + * This is either OK, indicating success, or + * another capture set, indicating failure. The failure capture set + * is the one that did not allow propagaton of elements into it. + */ + object CompareResult: + opaque type TYPE = CaptureSet + val OK: TYPE = Const(emptySet) + def fail(cs: CaptureSet): TYPE = cs + + extension (result: TYPE) + /** The result is OK */ + def isOK: Boolean = result eq OK + /** If not isOK, the blocking capture set */ + def blocking: CaptureSet = result + inline def andAlso(op: Context ?=> TYPE)(using Context): TYPE = if result.isOK then op else result + def show(using Context): String = if result.isOK then "OK" else i"$result" + end CompareResult + + /** A VarState serves as a snapshot mechanism that can undo + * additions of elements or super sets if an operation fails + */ + class VarState: + + /** A map from captureset variables to their elements at the time of the snapshot. */ + private val elemsMap: util.EqHashMap[Var, Refs] = new util.EqHashMap + + /** A map from captureset variables to their dependent sets at the time of the snapshot. */ + private val depsMap: util.EqHashMap[Var, Deps] = new util.EqHashMap + + /** The recorded elements of `v` (it's required that a recording was made) */ + def elems(v: Var): Refs = elemsMap(v) + + /** Optionally the recorded elements of `v`, None if nothing was recorded for `v` */ + def getElems(v: Var): Option[Refs] = elemsMap.get(v) + + /** Record elements, return whether this was allowed. + * By default, recording is allowed but the special state FrozenState + * overrides this. + */ + def putElems(v: Var, elems: Refs): Boolean = { elemsMap(v) = elems; true } + + /** The recorded dependent sets of `v` (it's required that a recording was made) */ + def deps(v: Var): Deps = depsMap(v) + + /** Optionally the recorded dependent sets of `v`, None if nothing was recorded for `v` */ + def getDeps(v: Var): Option[Deps] = depsMap.get(v) + + /** Record dependent sets, return whether this was allowed. + * By default, recording is allowed but the special state FrozenState + * overrides this. + */ + def putDeps(v: Var, deps: Deps): Boolean = { depsMap(v) = deps; true } + + /** Roll back global state to what was recorded in this VarState */ + def rollBack(): Unit = + elemsMap.keysIterator.foreach(_.resetElems()(using this)) + depsMap.keysIterator.foreach(_.resetDeps()(using this)) + end VarState + + /** A special state that does not allow to record elements or dependent sets. + * In effect this means that no new elements or dependent sets can be added + * in this state (since the previous state cannot be recorded in a snapshot) + */ + @sharable + object FrozenState extends VarState: + override def putElems(v: Var, refs: Refs) = false + override def putDeps(v: Var, deps: Deps) = false + override def rollBack(): Unit = () + + @sharable + /** A special state that turns off recording of elements. Used only + * in `addSub` to prevent cycles in recordings. + */ + private object UnrecordedState extends VarState: + override def putElems(v: Var, refs: Refs) = true + override def putDeps(v: Var, deps: Deps) = true + override def rollBack(): Unit = () + + /** The current VarState, as passed by the implicit context */ + def varState(using state: VarState): VarState = state + + /* Not needed: + def ofClass(cinfo: ClassInfo, argTypes: List[Type])(using Context): CaptureSet = + CaptureSet.empty + def captureSetOf(tp: Type): CaptureSet = tp match + case tp: TypeRef if tp.symbol.is(ParamAccessor) => + def mapArg(accs: List[Symbol], tps: List[Type]): CaptureSet = accs match + case acc :: accs1 if tps.nonEmpty => + if acc == tp.symbol then tps.head.captureSet + else mapArg(accs1, tps.tail) + case _ => + empty + mapArg(cinfo.cls.paramAccessors, argTypes) + case _ => + tp.captureSet + val css = + for + parent <- cinfo.parents if parent.classSymbol == defn.RetainingClass + arg <- parent.argInfos + yield captureSetOf(arg) + css.foldLeft(empty)(_ ++ _) + */ + + /** The capture set of the type underlying a CaptureRef */ + def ofInfo(ref: CaptureRef)(using Context): CaptureSet = ref match + case ref: TermRef if ref.isRootCapability => ref.singletonCaptureSet + case _ => ofType(ref.underlying) + + /** Capture set of a type */ + def ofType(tp: Type)(using Context): CaptureSet = + def recur(tp: Type): CaptureSet = tp.dealias match + case tp: TermRef => + tp.captureSet + case tp: TermParamRef => + tp.captureSet + case _: TypeRef => + if tp.classSymbol.hasAnnotation(defn.CapabilityAnnot) then universal else empty + case _: TypeParamRef => + empty + case CapturingType(parent, refs) => + recur(parent) ++ refs + case AppliedType(tycon, args) => + val cs = recur(tycon) + tycon.typeParams match + case tparams @ (LambdaParam(tl, _) :: _) => cs.substParams(tl, args) + case _ => cs + case tp: TypeProxy => + recur(tp.underlying) + case AndType(tp1, tp2) => + recur(tp1) ** recur(tp2) + case OrType(tp1, tp2) => + recur(tp1) ++ recur(tp2) + case _ => + empty + recur(tp) + .showing(i"capture set of $tp = $result", capt) + + private val ShownVars: Property.Key[mutable.Set[Var]] = Property.Key() + + /** Perform `op`. Under -Ycc-debug, collect and print info about all variables reachable + * via `(_.deps)*` from the variables that were shown in `op`. + */ + def withCaptureSetsExplained[T](op: Context ?=> T)(using ctx: Context): T = + if ctx.settings.YccDebug.value then + val shownVars = mutable.Set[Var]() + inContext(ctx.withProperty(ShownVars, Some(shownVars))) { + try op + finally + val reachable = mutable.Set[Var]() + val todo = mutable.Queue[Var]() ++= shownVars + def incl(cv: Var): Unit = + if !reachable.contains(cv) then todo += cv + while todo.nonEmpty do + val cv = todo.dequeue() + if !reachable.contains(cv) then + reachable += cv + cv.deps.foreach { + case cv: Var => incl(cv) + case _ => + } + cv match + case cv: DerivedVar => incl(cv.source) + case _ => + val allVars = reachable.toArray.sortBy(_.id) + println(i"Capture set dependencies:") + for cv <- allVars do + println(i" ${cv.show.padTo(20, ' ')} :: ${cv.deps.toList}%, %") + } + else op +end CaptureSet diff --git a/compiler/src/dotty/tools/dotc/cc/CapturingType.scala b/compiler/src/dotty/tools/dotc/cc/CapturingType.scala new file mode 100644 index 000000000000..05e813793a63 --- /dev/null +++ b/compiler/src/dotty/tools/dotc/cc/CapturingType.scala @@ -0,0 +1,69 @@ +package dotty.tools +package dotc +package cc + +import core.* +import Types.*, Symbols.*, Contexts.* + +/** A (possibly boxed) capturing type. This is internally represented as an annotated type with a @retains + * or @retainsByName annotation, but the extractor will succeed only at phase CheckCaptures. + * That way, we can ignore caturing information until phase CheckCaptures since it is + * wrapped in a plain annotation. + * + * The same trick does not work for the boxing information. Boxing is context dependent, so + * we have to add that information in the Setup step preceding CheckCaptures. Boxes are + * added for all type arguments of methods. For type arguments of applied types a different + * strategy is used where we box arguments of applied types that are not functions when + * accessing the argument. + * + * An alternative strategy would add boxes also to arguments of applied types during setup. + * But this would have to be done for all possibly accessibly types from the compiled units + * as well as their dependencies. It's difficult to do this in a DenotationTransformer without + * accidentally forcing symbol infos. That's why this alternative was not implemented. + * If we would go back on this it would make sense to also treat captuyring types different + * from annotations and to generate them all during Setup and in DenotationTransformers. + */ +object CapturingType: + + /** Smart constructor that drops empty capture sets and fuses compatible capturiong types. + * An outer type capturing type A can be fused with an inner capturing type B if their + * boxing status is the same or if A is boxed. + */ + def apply(parent: Type, refs: CaptureSet, boxed: Boolean = false)(using Context): Type = + if refs.isAlwaysEmpty then parent + else parent match + case parent @ CapturingType(parent1, refs1) if boxed || !parent.isBoxed => + apply(parent1, refs ++ refs1, boxed) + case _ => + AnnotatedType(parent, CaptureAnnotation(refs, boxed)(defn.RetainsAnnot)) + + /** An extractor that succeeds only during CheckCapturingPhase. Boxing statis is + * returned separately by CaptureOps.isBoxed. + */ + def unapply(tp: AnnotatedType)(using Context): Option[(Type, CaptureSet)] = + if ctx.phase == Phases.checkCapturesPhase && tp.annot.symbol == defn.RetainsAnnot then + EventuallyCapturingType.unapply(tp) + else None + +end CapturingType + +/** An extractor for types that will be capturing types at phase CheckCaptures. Also + * included are types that indicate captures on enclosing call-by-name parameters + * before phase ElimByName. + */ +object EventuallyCapturingType: + + def unapply(tp: AnnotatedType)(using Context): Option[(Type, CaptureSet)] = + val sym = tp.annot.symbol + if sym == defn.RetainsAnnot || sym == defn.RetainsByNameAnnot then + tp.annot match + case ann: CaptureAnnotation => + Some((tp.parent, ann.refs)) + case ann => + try Some((tp.parent, ann.tree.toCaptureSet)) + catch case ex: IllegalCaptureRef => None + else None + +end EventuallyCapturingType + + diff --git a/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala b/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala new file mode 100644 index 000000000000..fe22f9f49e13 --- /dev/null +++ b/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala @@ -0,0 +1,761 @@ +package dotty.tools +package dotc +package cc + +import core.* +import Phases.*, DenotTransformers.*, SymDenotations.* +import Contexts.*, Names.*, Flags.*, Symbols.*, Decorators.* +import Types.*, StdNames.* +import config.Printers.{capt, recheckr} +import config.Config +import ast.{tpd, untpd, Trees} +import Trees.* +import typer.RefChecks.{checkAllOverrides, checkParents} +import util.{SimpleIdentitySet, EqHashMap, SrcPos} +import transform.SymUtils.* +import transform.{Recheck, PreRecheck} +import Recheck.* +import scala.collection.mutable +import CaptureSet.{withCaptureSetsExplained, IdempotentCaptRefMap} +import StdNames.nme +import reporting.trace + +/** The capture checker */ +object CheckCaptures: + import ast.tpd.* + + class Pre extends PreRecheck, SymTransformer: + + override def isEnabled(using Context) = ctx.settings.Ycc.value + + /** Reset `private` flags of parameter accessors so that we can refine them + * in Setup if they have non-empty capture sets. Special handling of some + * symbols defined for case classes. + */ + def transformSym(sym: SymDenotation)(using Context): SymDenotation = + if sym.isAllOf(PrivateParamAccessor) && !sym.hasAnnotation(defn.ConstructorOnlyAnnot) then + sym.copySymDenotation(initFlags = sym.flags &~ Private | Recheck.ResetPrivate) + else if Synthetics.needsTransform(sym) then + Synthetics.transformToCC(sym) + else + sym + end Pre + + /** A class describing environments. + * @param owner the current owner + * @param captured the caputure set containing all references to tracked free variables outside of boxes + * @param isBoxed true if the environment is inside a box (in which case references are not counted) + * @param outer0 the next enclosing environment + */ + case class Env(owner: Symbol, captured: CaptureSet, isBoxed: Boolean, outer0: Env | Null): + def outer = outer0.nn + + def isOutermost = outer0 == null + + /** If an environment is open it tracks free references */ + def isOpen = !captured.isAlwaysEmpty && !isBoxed + end Env + + /** Similar normal substParams, but this is an approximating type map that + * maps parameters in contravariant capture sets to the empty set. + * TODO: check what happens with non-variant. + */ + final class SubstParamsMap(from: BindingType, to: List[Type])(using Context) + extends ApproximatingTypeMap, IdempotentCaptRefMap: + def apply(tp: Type): Type = tp match + case tp: ParamRef => + if tp.binder == from then to(tp.paramNum) else tp + case tp: NamedType => + if tp.prefix `eq` NoPrefix then tp + else tp.derivedSelect(apply(tp.prefix)) + case _: ThisType => + tp + case _ => + mapOver(tp) + + /** Check that a @retains annotation only mentions references that can be tracked. + * This check is performed at Typer. + */ + def checkWellformed(ann: Tree)(using Context): Unit = + for elem <- retainedElems(ann) do + elem.tpe match + case ref: CaptureRef => + if !ref.canBeTracked then + report.error(em"$elem cannot be tracked since it is not a parameter or a local variable", elem.srcPos) + case tpe => + report.error(em"$elem: $tpe is not a legal element of a capture set", elem.srcPos) + + /** If `tp` is a capturing type, check that all references it mentions have non-empty + * capture sets. Also: warn about redundant capture annotations. + * This check is performed after capture sets are computed in phase cc. + */ + def checkWellformedPost(tp: Type, pos: SrcPos)(using Context): Unit = tp match + case CapturingType(parent, refs) => + for ref <- refs.elems do + if ref.captureSetOfInfo.elems.isEmpty then + report.error(em"$ref cannot be tracked since its capture set is empty", pos) + else if parent.captureSet.accountsFor(ref) then + report.warning(em"redundant capture: $parent already accounts for $ref", pos) + case _ => + + /** Warn if `ann`, which is a tree of a @retains annotation, defines some elements that + * are already accounted for by other elements of the same annotation. + * Note: We need to perform the check on the original annotation rather than its + * capture set since the conversion to a capture set already eliminates redundant elements. + */ + def warnIfRedundantCaptureSet(ann: Tree)(using Context): Unit = + // The lists `elems(i) :: prev.reverse :: elems(0),...,elems(i-1),elems(i+1),elems(n)` + // where `n == elems.length-1`, i <- 0..n`. + // I.e. + // choices(Nil, elems) = [[elems(i), elems(0), ..., elems(i-1), elems(i+1), .... elems(n)] | i <- 0..n] + def choices(prev: List[Tree], elems: List[Tree]): List[List[Tree]] = elems match + case Nil => Nil + case elem :: elems => + List(elem :: (prev reverse_::: elems)) ++ choices(elem :: prev, elems) + for case first :: others <- choices(Nil, retainedElems(ann)) do + val firstRef = first.toCaptureRef + val remaining = CaptureSet(others.map(_.toCaptureRef)*) + if remaining.accountsFor(firstRef) then + report.warning(em"redundant capture: $remaining already accounts for $firstRef", ann.srcPos) + +class CheckCaptures extends Recheck, SymTransformer: + thisPhase => + + import ast.tpd.* + import CheckCaptures.* + + def phaseName: String = "cc" + override def isEnabled(using Context) = ctx.settings.Ycc.value + + def newRechecker()(using Context) = CaptureChecker(ctx) + + override def run(using Context): Unit = + checkOverrides.traverse(ctx.compilationUnit.tpdTree) + super.run + + override def transformSym(sym: SymDenotation)(using Context): SymDenotation = + if Synthetics.needsTransform(sym) then Synthetics.transformFromCC(sym) + else super.transformSym(sym) + + /** Check overrides again, taking capture sets into account. + * TODO: Can we avoid doing overrides checks twice? + * We need to do them here since only at this phase CaptureTypes are relevant + * But maybe we can then elide the check during the RefChecks phase if -Ycc is set? + */ + def checkOverrides = new TreeTraverser: + def traverse(t: Tree)(using Context) = + t match + case t: Template => checkAllOverrides(ctx.owner.asClass) + case _ => + traverseChildren(t) + + class CaptureChecker(ictx: Context) extends Rechecker(ictx): + import ast.tpd.* + + override def keepType(tree: Tree) = + super.keepType(tree) + || tree.isInstanceOf[Try] // type of `try` needs tp be checked for * escapes + + /** Instantiate capture set variables appearing contra-variantly to their + * upper approximation. + */ + private def interpolator(startingVariance: Int = 1)(using Context) = new TypeTraverser: + variance = startingVariance + override def traverse(t: Type) = + t match + case CapturingType(parent, refs: CaptureSet.Var) => + if variance < 0 then + capt.println(i"solving $t") + refs.solve() + traverse(parent) + case t @ RefinedType(_, nme.apply, rinfo) if defn.isFunctionOrPolyType(t) => + traverse(rinfo) + case tp: TypeVar => + case tp: TypeRef => + traverse(tp.prefix) + case _ => + traverseChildren(t) + + /** If `tpt` is an inferred type, interpolate capture set variables appearing contra- + * variantly in it. + */ + private def interpolateVarsIn(tpt: Tree)(using Context): Unit = + if tpt.isInstanceOf[InferredTypeTree] then + interpolator().traverse(tpt.knownType) + .showing(i"solved vars in ${tpt.knownType}", capt) + + /** Assert subcapturing `cs1 <: cs2` */ + def assertSub(cs1: CaptureSet, cs2: CaptureSet)(using Context) = + assert(cs1.subCaptures(cs2, frozen = false).isOK, i"$cs1 is not a subset of $cs2") + + /** Check subcapturing `{elem} <: cs`, report error on failure */ + def checkElem(elem: CaptureRef, cs: CaptureSet, pos: SrcPos)(using Context) = + val res = elem.singletonCaptureSet.subCaptures(cs, frozen = false) + if !res.isOK then + report.error(i"$elem cannot be referenced here; it is not included in the allowed capture set ${res.blocking}", pos) + + /** Check subcapturing `cs1 <: cs2`, report error on failure */ + def checkSubset(cs1: CaptureSet, cs2: CaptureSet, pos: SrcPos)(using Context) = + val res = cs1.subCaptures(cs2, frozen = false) + if !res.isOK then + def header = + if cs1.elems.size == 1 then i"reference ${cs1.elems.toList}%, % is not" + else i"references $cs1 are not all" + report.error(i"$header included in allowed capture set ${res.blocking}", pos) + + /** The current environment */ + private var curEnv: Env = Env(NoSymbol, CaptureSet.empty, isBoxed = false, null) + + private val myCapturedVars: util.EqHashMap[Symbol, CaptureSet] = EqHashMap() + + /** If `sym` is a class or method nested inside a term, a capture set variable representing + * the captured variables of the environment associated with `sym`. + */ + def capturedVars(sym: Symbol)(using Context) = + myCapturedVars.getOrElseUpdate(sym, + if sym.ownersIterator.exists(_.isTerm) then CaptureSet.Var() + else CaptureSet.empty) + + /** For all nested environments up to `limit` perform `op` */ + def forallOuterEnvsUpTo(limit: Symbol)(op: Env => Unit)(using Context): Unit = + def recur(env: Env): Unit = + if env.isOpen && env.owner != limit then + op(env) + if !env.isOutermost then + var nextEnv = env.outer + if env.owner.isConstructor then + if nextEnv.owner != limit && !nextEnv.isOutermost then + recur(nextEnv.outer) + else recur(nextEnv) + recur(curEnv) + + /** Include `sym` in the capture sets of all enclosing environments nested in the + * the environment in which `sym` is defined. + */ + def markFree(sym: Symbol, pos: SrcPos)(using Context): Unit = + if sym.exists then + val ref = sym.termRef + if ref.isTracked then + forallOuterEnvsUpTo(sym.enclosure) { env => + capt.println(i"Mark $sym with cs ${ref.captureSet} free in ${env.owner}") + checkElem(ref, env.captured, pos) + } + + /** Make sure (projected) `cs` is a subset of the capture sets of all enclosing + * environments. At each stage, only include references from `cs` that are outside + * the environment's owner + */ + def markFree(cs: CaptureSet, pos: SrcPos)(using Context): Unit = + if !cs.isAlwaysEmpty then + forallOuterEnvsUpTo(ctx.owner.topLevelClass) { env => + val included = cs.filter { + case ref: TermRef => env.owner.isProperlyContainedIn(ref.symbol.owner) + case ref: ThisType => env.owner.isProperlyContainedIn(ref.cls) + case _ => false + } + capt.println(i"Include call capture $included in ${env.owner}") + checkSubset(included, env.captured, pos) + } + + /** Include references captured by the called method in the current environment stack */ + def includeCallCaptures(sym: Symbol, pos: SrcPos)(using Context): Unit = + if sym.exists && curEnv.isOpen then markFree(capturedVars(sym), pos) + + override def recheckIdent(tree: Ident)(using Context): Type = + if tree.symbol.is(Method) then includeCallCaptures(tree.symbol, tree.srcPos) + else markFree(tree.symbol, tree.srcPos) + super.recheckIdent(tree) + + /** A specialized implementation of the selection rule. + * + * E |- f: Cf f { m: Cr R } + * ------------------------ + * E |- f.m: C R + * + * The implementation picks as `C` one of `{f}` or `Cr`, depending on the + * outcome of a `mightSubcapture` test. It picks `{f}` if this might subcapture Cr + * and Cr otherwise. + */ + override def recheckSelection(tree: Select, qualType: Type, name: Name)(using Context) = { + val selType = super.recheckSelection(tree, qualType, name) + val selCs = selType.widen.captureSet + if selCs.isAlwaysEmpty || selType.widen.isBoxedCapturing || qualType.isBoxedCapturing then + selType + else + val qualCs = qualType.captureSet + capt.println(i"intersect $qualType, ${selType.widen}, $qualCs, $selCs in $tree") + if qualCs.mightSubcapture(selCs) then + selType.widen.stripCapturing.capturing(qualCs) + else + selType + }//.showing(i"recheck sel $tree, $qualType = $result") + + /** A specialized implementation of the apply rule. + * + * E |- f: Cf (Ra -> Cr Rr) + * E |- a: Ca Ra + * ------------------------ + * E |- f a: C Rr + * + * The implementation picks as `C` one of `{f, a}` or `Cr`, depending on the + * outcome of a `mightSubcapture` test. It picks `{f, a}` if this might subcapture Cr + * and Cr otherwise. + */ + override def recheckApply(tree: Apply, pt: Type)(using Context): Type = + includeCallCaptures(tree.symbol, tree.srcPos) + super.recheckApply(tree, pt) match + case appType @ CapturingType(appType1, refs) => + tree.fun match + case Select(qual, _) + if !tree.fun.symbol.isConstructor + && !qual.tpe.isBoxedCapturing + && !tree.args.exists(_.tpe.isBoxedCapturing) + && qual.tpe.captureSet.mightSubcapture(refs) + && tree.args.forall(_.tpe.captureSet.mightSubcapture(refs)) + => + val callCaptures = tree.args.foldLeft(qual.tpe.captureSet)((cs, arg) => + cs ++ arg.tpe.captureSet) + appType.derivedCapturingType(appType1, callCaptures) + .showing(i"narrow $tree: $appType, refs = $refs, qual = ${qual.tpe.captureSet} --> $result", capt) + case _ => appType + case appType => appType + + /** Handle an application of method `sym` with type `mt` to arguments of types `argTypes`. + * This means: + * - Instantiate result type with actual arguments + * - If call is to a constructor: + * - remember types of arguments corresponding to tracked + * parameters in refinements. + * - add capture set of instantiated class to capture set of result type. + */ + override def instantiate(mt: MethodType, argTypes: List[Type], sym: Symbol)(using Context): Type = + val ownType = + if mt.isResultDependent then SubstParamsMap(mt, argTypes)(mt.resType) + else mt.resType + + if sym.isConstructor then + val cls = sym.owner.asClass + + /** First half of result pair: + * Refine the type of a constructor call `new C(t_1, ..., t_n)` + * to C{val x_1: T_1, ..., x_m: T_m} where x_1, ..., x_m are the tracked + * parameters of C and T_1, ..., T_m are the types of the corresponding arguments. + * + * Second half: union of all capture sets of arguments to tracked parameters. + */ + def addParamArgRefinements(core: Type, initCs: CaptureSet): (Type, CaptureSet) = + mt.paramNames.lazyZip(argTypes).foldLeft((core, initCs)) { (acc, refine) => + val (core, allCaptures) = acc + val (getterName, argType) = refine + val getter = cls.info.member(getterName).suchThat(_.is(ParamAccessor)).symbol + if getter.termRef.isTracked && !getter.is(Private) + then (RefinedType(core, getterName, argType), allCaptures ++ argType.captureSet) + else (core, allCaptures) + } + + def augmentConstructorType(core: Type, initCs: CaptureSet): Type = core match + case core: MethodType => + // more parameters to follow; augment result type + core.derivedLambdaType(resType = augmentConstructorType(core.resType, initCs)) + case CapturingType(parent, refs) => + // can happen for curried constructors if instantiate of a previous step + // added capture set to result. + augmentConstructorType(parent, initCs ++ refs) + case _ => + val (refined, cs) = addParamArgRefinements(core, initCs) + refined.capturing(cs) + + augmentConstructorType(ownType, CaptureSet.empty) match + case augmented: MethodType => + augmented + case augmented => + // add capture sets of class and constructor to final result of constructor call + augmented.capturing(capturedVars(cls) ++ capturedVars(sym)) + .showing(i"constr type $mt with $argTypes%, % in $cls = $result", capt) + else ownType + end instantiate + + override def recheckClosure(tree: Closure, pt: Type)(using Context): Type = + val cs = capturedVars(tree.meth.symbol) + capt.println(i"typing closure $tree with cvs $cs") + super.recheckClosure(tree, pt).capturing(cs) + .showing(i"rechecked $tree / $pt = $result", capt) + + /** Additionally to normal processing, update types of closures if the expected type + * is a function with only pure parameters. In that case, make the anonymous function + * also have the same parameters as the prototype. + * TODO: Develop a clearer rationale for this. + * TODO: Can we generalize this to arbitrary parameters? + * Currently some tests fail if we do this. (e.g. neg.../stackAlloc.scala, others) + */ + override def recheckBlock(block: Block, pt: Type)(using Context): Type = + block match + case closureDef(mdef) => + pt.dealias match + case defn.FunctionOf(ptformals, _, _, _) if ptformals.forall(_.captureSet.isAlwaysEmpty) => + // Redo setup of the anonymous function so that formal parameters don't + // get capture sets. This is important to avoid false widenings to `*` + // when taking the base type of the actual closures's dependent function + // type so that it conforms to the expected non-dependent function type. + // See withLogFile.scala for a test case. + val meth = mdef.symbol + // First, undo the previous setup which installed a completer for `meth`. + atPhase(preRecheckPhase.prev)(meth.denot.copySymDenotation()) + .installAfter(preRecheckPhase) + // Next, update all parameter symbols to match expected formals + meth.paramSymss.head.lazyZip(ptformals).foreach { (psym, pformal) => + psym.copySymDenotation(info = pformal).installAfter(preRecheckPhase) + } + // Next, update types of parameter ValDefs + mdef.paramss.head.lazyZip(ptformals).foreach { (param, pformal) => + val ValDef(_, tpt, _) = param: @unchecked + tpt.rememberTypeAlways(pformal) + } + // Next, install a new completer reflecting the new parameters for the anonymous method + val completer = new LazyType: + def complete(denot: SymDenotation)(using Context) = + denot.info = MethodType(ptformals, mdef.tpt.knownType) + .showing(i"simplify info of $meth to $result", capt) + recheckDef(mdef, meth) + meth.copySymDenotation(info = completer, initFlags = meth.flags &~ Touched) + .installAfter(preRecheckPhase) + case _ => + case _ => + super.recheckBlock(block, pt) + + override def recheckValDef(tree: ValDef, sym: Symbol)(using Context): Unit = + try + if !sym.is(Module) then // Modules are checked by checking the module class + super.recheckValDef(tree, sym) + finally + if !sym.is(Param) then + // Parameters with inferred types belong to anonymous methods. We need to wait + // for more info from the context, so we cannot interpolate. Note that we cannot + // expect to have all necessary info available at the point where the anonymous + // function is compiled since we do not propagate expected types into blocks. + interpolateVarsIn(tree.tpt) + + override def recheckDefDef(tree: DefDef, sym: Symbol)(using Context): Unit = + if !Synthetics.isExcluded(sym) then + val saved = curEnv + val localSet = capturedVars(sym) + if !localSet.isAlwaysEmpty then curEnv = Env(sym, localSet, isBoxed = false, curEnv) + try super.recheckDefDef(tree, sym) + finally + interpolateVarsIn(tree.tpt) + curEnv = saved + + /** Class-specific capture set relations: + * 1. The capture set of a class includes the capture sets of its parents. + * 2. The capture set of the self type of a class includes the capture set of the class. + * 3. The capture set of the self type of a class includes the capture set of every class parameter. + */ + override def recheckClassDef(tree: TypeDef, impl: Template, cls: ClassSymbol)(using Context): Type = + val saved = curEnv + val localSet = capturedVars(cls) + for parent <- impl.parents do // (1) + checkSubset(capturedVars(parent.tpe.classSymbol), localSet, parent.srcPos) + if !localSet.isAlwaysEmpty then curEnv = Env(cls, localSet, isBoxed = false, curEnv) + try + val thisSet = cls.classInfo.selfType.captureSet.withDescription(i"of the self type of $cls") + checkSubset(localSet, thisSet, tree.srcPos) // (2) + for param <- cls.paramGetters do + checkSubset(param.termRef.captureSet, thisSet, param.srcPos) // (3) + super.recheckClassDef(tree, impl, cls) + finally + curEnv = saved + + /** If type is of the form `T @requiresCapability(x)`, + * mark `x` as free in the current environment. This is used to require the + * correct `CanThrow` capability when encountering a `throw`. + */ + override def recheckTyped(tree: Typed)(using Context): Type = + tree.tpt.tpe match + case AnnotatedType(_, annot) if annot.symbol == defn.RequiresCapabilityAnnot => + annot.tree match + case Apply(_, cap :: Nil) => + markFree(cap.symbol, tree.srcPos) + case _ => + case _ => + super.recheckTyped(tree) + + /* Currently not needed, since capture checking takes place after ElimByName. + * Keep around in case we need to get back to it + def recheckByNameArg(tree: Tree, pt: Type)(using Context): Type = + val closureDef(mdef) = tree: @unchecked + val arg = mdef.rhs + val localSet = CaptureSet.Var() + curEnv = Env(mdef.symbol, localSet, isBoxed = false, curEnv) + val result = + try + inContext(ctx.withOwner(mdef.symbol)) { + recheckStart(arg, pt).capturing(localSet) + } + finally curEnv = curEnv.outer + recheckFinish(result, arg, pt) + */ + + /** If expected type `pt` is boxed, don't propagate free variables. + * Otherwise, if the result type is boxed, simulate an unboxing by + * adding all references in the boxed capture set to the current environment. + */ + override def recheck(tree: Tree, pt: Type = WildcardType)(using Context): Type = + if tree.isTerm && pt.isBoxedCapturing then + val saved = curEnv + curEnv = Env(curEnv.owner, CaptureSet.Var(), isBoxed = true, curEnv) + try super.recheck(tree, pt) + finally curEnv = saved + else + val res = super.recheck(tree, pt) + if tree.isTerm then markFree(res.boxedCaptureSet, tree.srcPos) + res + + /** If `tree` is a reference or an application where the result type refers + * to an enclosing class or method parameter of the reference, check that the result type + * does not capture the universal capability. This is justified since the + * result type would have to be implicitly unboxed. + * TODO: Can we find a cleaner way to achieve this? Logically, this should be part + * of simulated boxing and unboxing. + */ + override def recheckFinish(tpe: Type, tree: Tree, pt: Type)(using Context): Type = + val typeToCheck = tree match + case _: Ident | _: Select | _: Apply | _: TypeApply if tree.symbol.unboxesResult => + tpe + case _: Try => + tpe + case _: ValDef if tree.symbol.is(Mutable) => + tree.symbol.info + case _ => + NoType + def checkNotUniversal(tp: Type): Unit = tp.widenDealias match + case wtp @ CapturingType(parent, refs) => + refs.disallowRootCapability { () => + val kind = if tree.isInstanceOf[ValDef] then "mutable variable" else "expression" + report.error( + em"""The $kind's type $wtp is not allowed to capture the root capability `*`. + |This usually means that a capability persists longer than its allowed lifetime.""", + tree.srcPos) + } + checkNotUniversal(parent) + case _ => + checkNotUniversal(typeToCheck) + super.recheckFinish(tpe, tree, pt) + + /** Massage `actual` and `expected` types using the methods below before checking conformance */ + override def checkConformsExpr(actual: Type, expected: Type, tree: Tree)(using Context): Unit = + val expected1 = addOuterRefs(expected, actual) + val actual1 = adaptBoxed(actual, expected1, tree.srcPos) + //println(i"check conforms $actual1 <<< $expected1") + super.checkConformsExpr(actual1, expected1, tree) + + /** For the expected type, implement the rule outlined in #14390: + * - when checking an expression `a: Ca Ta` against an expected type `Ce Te`, + * - where the capture set `Ce` contains Cls.this, + * - and where and all method definitions enclosing `a` inside class `Cls` + * have only pure parameters, + * - add to `Ce` all references to variables or this-references in `Ca` + * that are outside `Cls`. These are all accessed through `Cls.this`, + * so we can assume they are already accounted for by `Ce` and adding + * them explicitly to `Ce` changes nothing. + */ + private def addOuterRefs(expected: Type, actual: Type)(using Context): Type = + def isPure(info: Type): Boolean = info match + case info: PolyType => isPure(info.resType) + case info: MethodType => info.paramInfos.forall(_.captureSet.isAlwaysEmpty) && isPure(info.resType) + case _ => true + def isPureContext(owner: Symbol, limit: Symbol): Boolean = + if owner == limit then true + else if !owner.exists then false + else isPure(owner.info) && isPureContext(owner.owner, limit) + def augment(erefs: CaptureSet, arefs: CaptureSet): CaptureSet = + (erefs /: erefs.elems) { (erefs, eref) => + eref match + case eref: ThisType if isPureContext(ctx.owner, eref.cls) => + erefs ++ arefs.filter { + case aref: TermRef => eref.cls.isProperlyContainedIn(aref.symbol.owner) + case aref: ThisType => eref.cls.isProperlyContainedIn(aref.cls) + case _ => false + } + case _ => + erefs + } + expected match + case CapturingType(ecore, erefs) => + val erefs1 = augment(erefs, actual.captureSet) + if erefs1 ne erefs then + capt.println(i"augmented $expected from ${actual.captureSet} --> $erefs1") + expected.derivedCapturingType(ecore, erefs1) + case _ => + expected + + /** Adapt `actual` type to `expected` type by inserting boxing and unboxing conversions */ + def adaptBoxed(actual: Type, expected: Type, pos: SrcPos)(using Context): Type = + + /** Adapt function type `actual`, which is `aargs -> ares` (possibly with dependencies) + * to `expected` type. + * @param reconstruct how to rebuild the adapted function type + */ + def adaptFun(actual: Type, aargs: List[Type], ares: Type, expected: Type, + covariant: Boolean, + reconstruct: (List[Type], Type) => Type): Type = + val (eargs, eres) = expected.dealias match + case defn.FunctionOf(eargs, eres, _, _) => (eargs, eres) + case _ => (aargs.map(_ => WildcardType), WildcardType) + val aargs1 = aargs.zipWithConserve(eargs)(adapt(_, _, !covariant)) + val ares1 = adapt(ares, eres, covariant) + if (ares1 eq ares) && (aargs1 eq aargs) then actual + else reconstruct(aargs1, ares1) + + def adapt(actual: Type, expected: Type, covariant: Boolean): Type = actual.dealias match + case actual @ CapturingType(parent, refs) => + val parent1 = adapt(parent, expected, covariant) + if actual.isBoxed != expected.isBoxedCapturing then + val criticalSet = // the set which is not allowed to have `*` + if covariant then refs // can't box with `*` + else expected.captureSet // can't unbox with `*` + if criticalSet.isUniversal then + // We can't box/unbox the universal capability. Leave `actual` as it is + // so we get an error in checkConforms. This tends to give better error + // messages than disallowing the root capability in `criticalSet`. + capt.println(i"cannot box/unbox $actual vs $expected") + actual + else + // Disallow future addition of `*` to `criticalSet`. + criticalSet.disallowRootCapability { () => + report.error( + em"""$actual cannot be box-converted to $expected + |since one of their capture sets contains the root capability `*`""", + pos) + } + if covariant == actual.isBoxed then markFree(refs, pos) + CapturingType(parent1, refs, boxed = !actual.isBoxed) + else + actual.derivedCapturingType(parent1, refs) + case actual @ AppliedType(tycon, args) if defn.isNonRefinedFunction(actual) => + adaptFun(actual, args.init, args.last, expected, covariant, + (aargs1, ares1) => actual.derivedAppliedType(tycon, aargs1 :+ ares1)) + case actual @ RefinedType(_, _, rinfo: MethodType) if defn.isFunctionType(actual) => + // TODO Find a way to combine handling of generic and dependent function types (here and elsewhere) + adaptFun(actual, rinfo.paramInfos, rinfo.resType, expected, covariant, + (aargs1, ares1) => + rinfo.derivedLambdaType(paramInfos = aargs1, resType = ares1) + .toFunctionType(isJava = false, alwaysDependent = true)) + case _ => actual + + var actualw = actual.widenDealias + actual match + case ref: CaptureRef if ref.isTracked => + actualw match + case CapturingType(p, refs) => + actualw = actualw.derivedCapturingType(p, ref.singletonCaptureSet) + // given `a: C T`, improve `C T` to `{a} T` + case _ => + case _ => + val adapted = adapt(actualw, expected, covariant = true) + if adapted ne actualw then + capt.println(i"adapt boxed $actual vs $expected ===> $adapted") + adapted + else actual + end adaptBoxed + + override def checkUnit(unit: CompilationUnit)(using Context): Unit = + Setup(preRecheckPhase, thisPhase, recheckDef) + .traverse(ctx.compilationUnit.tpdTree) + withCaptureSetsExplained { + super.checkUnit(unit) + checkSelfTypes(unit.tpdTree) + postCheck(unit.tpdTree) + if ctx.settings.YccDebug.value then + show(unit.tpdTree) // this does not print tree, but makes its variables visible for dependency printing + } + + /** Check that self types of subclasses conform to self types of super classes. + * (See comment below how this is achieved). The check assumes that classes + * without an explicit self type have the universal capture set `{*}` on the + * self type. If a class without explicit self type is not `effectivelyFinal` + * it is checked that the inferred self type is universal, in order to assure + * that joint and separate compilation give the same result. + */ + def checkSelfTypes(unit: tpd.Tree)(using Context): Unit = + val parentTrees = mutable.HashMap[Symbol, List[Tree]]() + unit.foreachSubTree { + case cdef @ TypeDef(_, impl: Template) => parentTrees(cdef.symbol) = impl.parents + case _ => + } + // Perform self type checking. The problem here is that `checkParents` compares a + // self type of a subclass with the result of an asSeenFrom of the self type of the + // superclass. That's no good. We need to constrain the original superclass self type + // capture set, not the set mapped by asSeenFrom. + // + // Instead, we proceed from parent classes to child classes. For every class + // we first check its parents, and then interpolate the self type to an + // upper approximation that satisfies all constraints on its capture set. + // That means all capture sets of parent self types are constants, so mapping + // them with asSeenFrom is OK. + while parentTrees.nonEmpty do + val roots = parentTrees.keysIterator.filter { + cls => !parentTrees(cls).exists(ptree => parentTrees.contains(ptree.tpe.classSymbol)) + } + assert(roots.nonEmpty) + for root <- roots do + checkParents(root, parentTrees(root)) + val selfType = root.asClass.classInfo.selfType + interpolator(startingVariance = -1).traverse(selfType) + if !root.isEffectivelySealed then + selfType match + case CapturingType(_, refs: CaptureSet.Var) if !refs.isUniversal => + report.error( + i"""$root needs an explicitly declared self type since its + |inferred self type $selfType + |is not visible in other compilation units that define subclasses.""", + root.srcPos) + case _ => + parentTrees -= root + capt.println(i"checked $root with $selfType") + end checkSelfTypes + + /** Perform the following kinds of checks + * - Check all explicitly written capturing types for well-formedness using `checkWellFormedPost`. + * - Check that externally visible `val`s or `def`s have empty capture sets. If not, + * suggest an explicit type. This is so that separate compilation (where external + * symbols have empty capture sets) gives the same results as joint compilation. + */ + def postCheck(unit: tpd.Tree)(using Context): Unit = + unit.foreachSubTree { + case _: InferredTypeTree => + case tree: TypeTree if !tree.span.isZeroExtent => + tree.knownType.foreachPart { tp => + checkWellformedPost(tp, tree.srcPos) + tp match + case AnnotatedType(_, annot) if annot.symbol == defn.RetainsAnnot => + warnIfRedundantCaptureSet(annot.tree) + case _ => + } + case t: ValOrDefDef + if t.tpt.isInstanceOf[InferredTypeTree] && !Synthetics.isExcluded(t.symbol) => + val sym = t.symbol + val isLocal = + sym.owner.ownersIterator.exists(_.isTerm) + || sym.accessBoundary(defn.RootClass).isContainedIn(sym.topLevelClass) + + // The following classes of definitions need explicit capture types ... + if !isLocal // ... since external capture types are not inferred + || sym.owner.is(Trait) // ... since we do OverridingPairs checking before capture inference + || sym.allOverriddenSymbols.nonEmpty // ... since we do override checking before capture inference + then + val inferred = t.tpt.knownType + def checkPure(tp: Type) = tp match + case CapturingType(_, refs) if !refs.elems.isEmpty => + val resultStr = if t.isInstanceOf[DefDef] then " result" else "" + report.error( + em"""Non-local $sym cannot have an inferred$resultStr type + |$inferred + |with non-empty capture set $refs. + |The type needs to be declared explicitly.""", t.srcPos) + case _ => + inferred.foreachPart(checkPure, StopAt.Static) + case _ => + } + + end CaptureChecker +end CheckCaptures diff --git a/compiler/src/dotty/tools/dotc/cc/Setup.scala b/compiler/src/dotty/tools/dotc/cc/Setup.scala new file mode 100644 index 000000000000..a3e88699e424 --- /dev/null +++ b/compiler/src/dotty/tools/dotc/cc/Setup.scala @@ -0,0 +1,463 @@ +package dotty.tools +package dotc +package cc + +import core._ +import Phases.*, DenotTransformers.*, SymDenotations.* +import Contexts.*, Names.*, Flags.*, Symbols.*, Decorators.* +import Types.*, StdNames.* +import config.Printers.capt +import ast.tpd +import transform.Recheck.* +import CaptureSet.IdentityCaptRefMap +import Synthetics.isExcluded + +/** A tree traverser that prepares a compilation unit to be capture checked. + * It does the following: + * - For every inferred type, drop any retains annotations, + * add capture sets to all its parts, add refinements to class types and function types. + * (c.f. mapInferred) + * - For explicit capturing types, expand throws aliases to the underlying (pure) function, + * and add some implied capture sets to curried functions (c.f. expandThrowsAlias, expandAbbreviations). + * - Add capture sets to self types of classes and objects, unless the self type was written explicitly. + * - Box the types of mutable variables and type arguments to methods (type arguments of types + * are boxed on access). + * - Link the external types of val and def symbols with the inferred types based on their parameter symbols. + */ +class Setup( + preRecheckPhase: DenotTransformer, + thisPhase: DenotTransformer, + recheckDef: (tpd.ValOrDefDef, Symbol) => Context ?=> Unit) +extends tpd.TreeTraverser: + import tpd.* + + /** Create dependent function with underlying function class `tycon` and given + * arguments `argTypes` and result `resType`. + */ + private def depFun(tycon: Type, argTypes: List[Type], resType: Type)(using Context): Type = + MethodType.companion( + isContextual = defn.isContextFunctionClass(tycon.classSymbol), + isErased = defn.isErasedFunctionClass(tycon.classSymbol) + )(argTypes, resType) + .toFunctionType(isJava = false, alwaysDependent = true) + + /** If `tp` is an unboxed capturing type or a function returning an unboxed capturing type, + * convert it to be boxed. + */ + private def box(tp: Type)(using Context): Type = + def recur(tp: Type): Type = tp.dealias match + case tp @ CapturingType(parent, refs) if !tp.isBoxed => + tp.boxed + case tp1 @ AppliedType(tycon, args) if defn.isNonRefinedFunction(tp1) => + val res = args.last + val boxedRes = recur(res) + if boxedRes eq res then tp + else tp1.derivedAppliedType(tycon, args.init :+ boxedRes) + case tp1 @ RefinedType(_, _, rinfo) if defn.isFunctionType(tp1) => + val boxedRinfo = recur(rinfo) + if boxedRinfo eq rinfo then tp + else boxedRinfo.toFunctionType(isJava = false, alwaysDependent = true) + case tp1: MethodOrPoly => + val res = tp1.resType + val boxedRes = recur(res) + if boxedRes eq res then tp + else tp1.derivedLambdaType(resType = boxedRes) + case _ => tp + tp match + case tp: MethodOrPoly => tp // don't box results of methods outside refinements + case _ => recur(tp) + + /** Perform the following transformation steps everywhere in a type: + * 1. Drop retains annotations + * 2. Turn plain function types into dependent function types, so that + * we can refer to their parameters in capture sets. Currently this is + * only done at the toplevel, i.e. for function types that are not + * themselves argument types of other function types. Without this restriction + * pos.../lists.scala and pos/...curried-shorthands.scala fail. + * Need to figure out why. + * 3. Refine other class types C by adding capture set variables to their parameter getters + * (see addCaptureRefinements) + * 4. Add capture set variables to all types that can be tracked + * + * Polytype bounds are only cleaned using step 1, but not otherwise transformed. + */ + private def mapInferred(using Context) = new TypeMap: + + /** Drop @retains annotations everywhere */ + object cleanup extends TypeMap: + def apply(t: Type) = t match + case AnnotatedType(parent, annot) if annot.symbol == defn.RetainsAnnot => + apply(parent) + case _ => + mapOver(t) + + /** Refine a possibly applied class type C where the class has tracked parameters + * x_1: T_1, ..., x_n: T_n to C { val x_1: CV_1 T_1, ..., val x_n: CV_n T_n } + * where CV_1, ..., CV_n are fresh capture sets. + */ + def addCaptureRefinements(tp: Type): Type = tp match + case _: TypeRef | _: AppliedType if tp.typeParams.isEmpty => + tp.typeSymbol match + case cls: ClassSymbol if !defn.isFunctionClass(cls) => + cls.paramGetters.foldLeft(tp) { (core, getter) => + if getter.termRef.isTracked then + val getterType = tp.memberInfo(getter).strippedDealias + RefinedType(core, getter.name, CapturingType(getterType, CaptureSet.Var())) + .showing(i"add capture refinement $tp --> $result", capt) + else + core + } + case _ => tp + case _ => tp + + private def superTypeIsImpure(tp: Type): Boolean = { + tp.dealias match + case CapturingType(_, refs) => + !refs.isAlwaysEmpty + case tp: (TypeRef | AppliedType) => + val sym = tp.typeSymbol + if sym.isClass then + tp.typeSymbol == defn.AnyClass + // we assume Any is a shorthand of {*} Any, so if Any is an upper + // bound, the type is taken to be impure. + else superTypeIsImpure(tp.superType) + case tp: (RefinedOrRecType | MatchType) => + superTypeIsImpure(tp.underlying) + case tp: AndType => + superTypeIsImpure(tp.tp1) || canHaveInferredCapture(tp.tp2) + case tp: OrType => + superTypeIsImpure(tp.tp1) && superTypeIsImpure(tp.tp2) + case _ => + false + }.showing(i"super type is impure $tp = $result", capt) + + /** Should a capture set variable be added on type `tp`? */ + def canHaveInferredCapture(tp: Type): Boolean = { + tp.typeParams.isEmpty && tp.match + case tp: (TypeRef | AppliedType) => + val tp1 = tp.dealias + if tp1 ne tp then canHaveInferredCapture(tp1) + else + val sym = tp1.typeSymbol + if sym.isClass then !sym.isValueClass && sym != defn.AnyClass + else superTypeIsImpure(tp1) + case tp: (RefinedOrRecType | MatchType) => + canHaveInferredCapture(tp.underlying) + case tp: AndType => + canHaveInferredCapture(tp.tp1) && canHaveInferredCapture(tp.tp2) + case tp: OrType => + canHaveInferredCapture(tp.tp1) || canHaveInferredCapture(tp.tp2) + case CapturingType(_, refs) => + refs.isConst && !refs.isUniversal + case _ => + false + }.showing(i"can have inferred capture $tp = $result", capt) + + /** Add a capture set variable to `tp` if necessary, or maybe pull out + * an embedded capture set variable from a part of `tp`. + */ + def addVar(tp: Type) = tp match + case tp @ RefinedType(parent @ CapturingType(parent1, refs), rname, rinfo) => + CapturingType(tp.derivedRefinedType(parent1, rname, rinfo), refs, parent.isBoxed) + case tp: RecType => + tp.parent match + case parent @ CapturingType(parent1, refs) => + CapturingType(tp.derivedRecType(parent1), refs, parent.isBoxed) + case _ => + tp // can return `tp` here since unlike RefinedTypes, RecTypes are never created + // by `mapInferred`. Hence if the underlying type admits capture variables + // a variable was already added, and the first case above would apply. + case AndType(tp1 @ CapturingType(parent1, refs1), tp2 @ CapturingType(parent2, refs2)) => + assert(refs1.asVar.elems.isEmpty) + assert(refs2.asVar.elems.isEmpty) + assert(tp1.isBoxed == tp2.isBoxed) + CapturingType(AndType(parent1, parent2), refs1 ** refs2, tp1.isBoxed) + case tp @ OrType(tp1 @ CapturingType(parent1, refs1), tp2 @ CapturingType(parent2, refs2)) => + assert(refs1.asVar.elems.isEmpty) + assert(refs2.asVar.elems.isEmpty) + assert(tp1.isBoxed == tp2.isBoxed) + CapturingType(OrType(parent1, parent2, tp.isSoft), refs1 ++ refs2, tp1.isBoxed) + case tp @ OrType(tp1 @ CapturingType(parent1, refs1), tp2) => + CapturingType(OrType(parent1, tp2, tp.isSoft), refs1, tp1.isBoxed) + case tp @ OrType(tp1, tp2 @ CapturingType(parent2, refs2)) => + CapturingType(OrType(tp1, parent2, tp.isSoft), refs2, tp2.isBoxed) + case _ if canHaveInferredCapture(tp) => + val cs = tp.dealias match + case CapturingType(_, refs) => CaptureSet.Var(refs.elems) + case _ => CaptureSet.Var() + CapturingType(tp, cs) + case _ => + tp + + private var isTopLevel = true + + private def mapNested(ts: List[Type]): List[Type] = + val saved = isTopLevel + isTopLevel = false + try ts.mapConserve(this) finally isTopLevel = saved + + def apply(t: Type) = + val tp = expandThrowsAlias(t) + val tp1 = tp match + case AnnotatedType(parent, annot) if annot.symbol == defn.RetainsAnnot => + // Drop explicit retains annotations + apply(parent) + case tp @ AppliedType(tycon, args) => + val tycon1 = this(tycon) + if defn.isNonRefinedFunction(tp) then + // Convert toplevel generic function types to dependent functions + val args0 = args.init + var res0 = args.last + val args1 = mapNested(args0) + val res1 = this(res0) + if isTopLevel then + depFun(tycon1, args1, res1) + .showing(i"add function refinement $tp --> $result", capt) + else if (tycon1 eq tycon) && (args1 eq args0) && (res1 eq res0) then + tp + else + tp.derivedAppliedType(tycon1, args1 :+ res1) + else + tp.derivedAppliedType(tycon1, args.mapConserve(arg => this(arg))) + case tp @ RefinedType(core, rname, rinfo) if defn.isFunctionType(tp) => + val rinfo1 = apply(rinfo) + if rinfo1 ne rinfo then rinfo1.toFunctionType(isJava = false, alwaysDependent = true) + else tp + case tp: MethodType => + tp.derivedLambdaType( + paramInfos = mapNested(tp.paramInfos), + resType = this(tp.resType)) + case tp: TypeLambda => + // Don't recurse into parameter bounds, just cleanup any stray retains annotations + tp.derivedLambdaType( + paramInfos = tp.paramInfos.mapConserve(cleanup(_).bounds), + resType = this(tp.resType)) + case _ => + mapOver(tp) + addVar(addCaptureRefinements(tp1)) + end apply + end mapInferred + + private def transformInferredType(tp: Type, boxed: Boolean)(using Context): Type = + val tp1 = mapInferred(tp) + if boxed then box(tp1) else tp1 + + /** Expand some aliases of function types to the underlying functions. + * Right now, these are only $throws aliases, but this could be generalized. + */ + private def expandThrowsAlias(tp: Type)(using Context) = tp match + case AppliedType(tycon, res :: exc :: Nil) if tycon.typeSymbol == defn.throwsAlias => + // hard-coded expansion since $throws aliases in stdlib are defined with `?=>` rather than `?->` + defn.FunctionOf(defn.CanThrowClass.typeRef.appliedTo(exc) :: Nil, res, isContextual = true, isErased = true) + case _ => tp + + private def expandThrowsAliases(using Context) = new TypeMap: + def apply(t: Type) = t match + case _: AppliedType => + val t1 = expandThrowsAlias(t) + if t1 ne t then apply(t1) else mapOver(t) + case _: LazyRef => + t + case t @ AnnotatedType(t1, ann) => + // Don't map capture sets, since that would implicitly normalize sets that + // are not well-formed. + t.derivedAnnotatedType(apply(t1), ann) + case _ => + mapOver(t) + + /** Fill in capture sets of curried function types from left to right, using + * a combination of the following two rules: + * + * 1. Expand `{c} (x: A) -> (y: B) -> C` + * to `{c} (x: A) -> {c} (y: B) -> C` + * 2. Expand `(x: A) -> (y: B) -> C` where `x` is tracked + * to `(x: A) -> {x} (y: B) -> C` + * + * TODO: Should we also propagate capture sets to the left? + */ + private def expandAbbreviations(using Context) = new TypeMap: + + /** Propagate `outerCs` as well as all tracked parameters as capture set to the result type + * of the dependent function type `tp`. + */ + def propagateDepFunctionResult(tp: Type, outerCs: CaptureSet): Type = tp match + case RefinedType(parent, nme.apply, rinfo: MethodType) => + val localCs = CaptureSet(rinfo.paramRefs.filter(_.isTracked)*) + val rinfo1 = rinfo.derivedLambdaType( + resType = propagateEnclosing(rinfo.resType, CaptureSet.empty, outerCs ++ localCs)) + if rinfo1 ne rinfo then rinfo1.toFunctionType(isJava = false, alwaysDependent = true) + else tp + + /** If `tp` is a function type: + * - add `outerCs` as its capture set, + * - propagate `currentCs`, `outerCs`, and all tracked parameters of `tp` to the right. + */ + def propagateEnclosing(tp: Type, currentCs: CaptureSet, outerCs: CaptureSet): Type = tp match + case tp @ AppliedType(tycon, args) if defn.isFunctionClass(tycon.typeSymbol) => + val tycon1 = this(tycon) + val args1 = args.init.mapConserve(this) + val tp1 = + if args1.exists(!_.captureSet.isAlwaysEmpty) then + val propagated = propagateDepFunctionResult( + depFun(tycon, args1, args.last), currentCs ++ outerCs) + propagated match + case RefinedType(_, _, mt: MethodType) => + if mt.isCaptureDependent then propagated + else + // No need to introduce dependent type, switch back to generic function type + tp.derivedAppliedType(tycon1, args1 :+ mt.resType) + else + val resType1 = propagateEnclosing( + args.last, CaptureSet.empty, currentCs ++ outerCs) + tp.derivedAppliedType(tycon1, args1 :+ resType1) + tp1.capturing(outerCs) + case tp @ RefinedType(parent, nme.apply, rinfo: MethodType) if defn.isFunctionType(tp) => + propagateDepFunctionResult(mapOver(tp), currentCs ++ outerCs) + .capturing(outerCs) + case _ => + mapOver(tp) + + def apply(tp: Type): Type = tp match + case CapturingType(parent, cs) => + tp.derivedCapturingType(propagateEnclosing(parent, cs, CaptureSet.empty), cs) + case _ => + propagateEnclosing(tp, CaptureSet.empty, CaptureSet.empty) + end expandAbbreviations + + private def transformExplicitType(tp: Type, boxed: Boolean)(using Context): Type = + val tp1 = expandThrowsAliases(if boxed then box(tp) else tp) + if tp1 ne tp then capt.println(i"expanded: $tp --> $tp1") + if ctx.settings.YccNoAbbrev.value then tp1 + else expandAbbreviations(tp1) + + /** Transform type of type tree, and remember the transformed type as the type the tree */ + private def transformTT(tree: TypeTree, boxed: Boolean)(using Context): Unit = + tree.rememberType( + if tree.isInstanceOf[InferredTypeTree] + then transformInferredType(tree.tpe, boxed) + else transformExplicitType(tree.tpe, boxed)) + + /** Substitute parameter symbols in `from` to paramRefs in corresponding + * method or poly types `to`. We use a single BiTypeMap to do everything. + * @param from a list of lists of type or term parameter symbols of a curried method + * @param to a list of method or poly types corresponding one-to-one to the parameter lists + */ + private class SubstParams(from: List[List[Symbol]], to: List[LambdaType])(using Context) + extends DeepTypeMap, BiTypeMap: + + def apply(t: Type): Type = t match + case t: NamedType => + val sym = t.symbol + def outer(froms: List[List[Symbol]], tos: List[LambdaType]): Type = + def inner(from: List[Symbol], to: List[ParamRef]): Type = + if from.isEmpty then outer(froms.tail, tos.tail) + else if sym eq from.head then to.head + else inner(from.tail, to.tail) + if tos.isEmpty then t + else inner(froms.head, tos.head.paramRefs) + outer(from, to) + case _ => + mapOver(t) + + def inverse(t: Type): Type = t match + case t: ParamRef => + def recur(from: List[LambdaType], to: List[List[Symbol]]): Type = + if from.isEmpty then t + else if t.binder eq from.head then to.head(t.paramNum).namedType + else recur(from.tail, to.tail) + recur(to, from) + case _ => + mapOver(t) + end SubstParams + + /** Update info of `sym` for CheckCaptures phase only */ + private def updateInfo(sym: Symbol, info: Type)(using Context) = + sym.updateInfoBetween(preRecheckPhase, thisPhase, info) + + def traverse(tree: Tree)(using Context): Unit = + tree match + case tree: DefDef if isExcluded(tree.symbol) => + return + case tree @ ValDef(_, tpt: TypeTree, _) if tree.symbol.is(Mutable) => + transformTT(tpt, boxed = true) // types of mutable variables are boxed + traverse(tree.rhs) + case tree @ TypeApply(fn, args) => + traverse(fn) + for case arg: TypeTree <- args do + transformTT(arg, boxed = true) // type arguments in type applications are boxed + case _ => + traverseChildren(tree) + tree match + case tree: TypeTree => + transformTT(tree, boxed = false) // other types are not boxed + case tree: ValOrDefDef => + val sym = tree.symbol + + // replace an existing symbol info with inferred types where capture sets of + // TypeParamRefs and TermParamRefs put in correspondence by BiTypeMaps with the + // capture sets of the types of the method's parameter symbols and result type. + def integrateRT( + info: Type, // symbol info to replace + psymss: List[List[Symbol]], // the local (type and term) parameter symbols corresponding to `info` + prevPsymss: List[List[Symbol]], // the local parameter symbols seen previously in reverse order + prevLambdas: List[LambdaType] // the outer method and polytypes generated previously in reverse order + ): Type = + info match + case mt: MethodOrPoly => + val psyms = psymss.head + mt.companion(mt.paramNames)( + mt1 => + if !psyms.exists(_.isUpdatedAfter(preRecheckPhase)) && !mt.isParamDependent && prevLambdas.isEmpty then + mt.paramInfos + else + val subst = SubstParams(psyms :: prevPsymss, mt1 :: prevLambdas) + psyms.map(psym => subst(psym.info).asInstanceOf[mt.PInfo]), + mt1 => + integrateRT(mt.resType, psymss.tail, psyms :: prevPsymss, mt1 :: prevLambdas) + ) + case info: ExprType => + info.derivedExprType(resType = + integrateRT(info.resType, psymss, prevPsymss, prevLambdas)) + case _ => + val restp = tree.tpt.knownType + if prevLambdas.isEmpty then restp + else SubstParams(prevPsymss, prevLambdas)(restp) + + if tree.tpt.hasRememberedType && !sym.isConstructor then + val newInfo = integrateRT(sym.info, sym.paramSymss, Nil, Nil) + .showing(i"update info $sym: ${sym.info} --> $result", capt) + if newInfo ne sym.info then + val completer = new LazyType: + def complete(denot: SymDenotation)(using Context) = + denot.info = newInfo + recheckDef(tree, sym) + updateInfo(sym, completer) + case tree: Bind => + val sym = tree.symbol + updateInfo(sym, transformInferredType(sym.info, boxed = false)) + case tree: TypeDef => + tree.symbol match + case cls: ClassSymbol => + val cinfo @ ClassInfo(prefix, _, ps, decls, selfInfo) = cls.classInfo + if (selfInfo eq NoType) || cls.is(ModuleClass) && !cls.isStatic then + // add capture set to self type of nested classes if no self type is given explicitly + val localRefs = CaptureSet.Var() + val newInfo = ClassInfo(prefix, cls, ps, decls, + CapturingType(cinfo.selfType, localRefs) + .showing(i"inferred self type for $cls: $result", capt)) + updateInfo(cls, newInfo) + cls.thisType.asInstanceOf[ThisType].invalidateCaches() + if cls.is(ModuleClass) then + // if it's a module, the capture set of the module reference is the capture set of the self type + val modul = cls.sourceModule + updateInfo(modul, CapturingType(modul.info, localRefs)) + modul.termRef.invalidateCaches() + case _ => + val info = atPhase(preRecheckPhase)(tree.symbol.info) + val newInfo = transformExplicitType(info, boxed = false) + if newInfo ne info then + updateInfo(tree.symbol, newInfo) + capt.println(i"update info of ${tree.symbol} from $info to $newInfo") + case _ => + end traverse +end Setup diff --git a/compiler/src/dotty/tools/dotc/cc/Synthetics.scala b/compiler/src/dotty/tools/dotc/cc/Synthetics.scala new file mode 100644 index 000000000000..e8f7fd502baa --- /dev/null +++ b/compiler/src/dotty/tools/dotc/cc/Synthetics.scala @@ -0,0 +1,180 @@ +package dotty.tools +package dotc +package cc + +import core.* +import Symbols.*, SymDenotations.*, Contexts.*, Flags.*, Types.*, Decorators.* +import StdNames.nme +import Names.Name +import NameKinds.DefaultGetterName +import Phases.checkCapturesPhase +import config.Printers.capt + +/** Classification and transformation methods for synthetic + * case class methods that need to be treated specially. + * In particular, compute capturing types for some of these methods which + * have inferred (result-)types that need to be established under separate + * compilation. + */ +object Synthetics: + private def isSyntheticCopyMethod(sym: SymDenotation)(using Context) = + sym.name == nme.copy && sym.is(Synthetic) && sym.owner.isClass && sym.owner.is(Case) + + private def isSyntheticCompanionMethod(sym: SymDenotation, names: Name*)(using Context): Boolean = + names.contains(sym.name) && sym.is(Synthetic) && sym.owner.is(Module) && sym.owner.companionClass.is(Case) + + private def isSyntheticCopyDefaultGetterMethod(sym: SymDenotation)(using Context) = sym.name match + case DefaultGetterName(nme.copy, _) => sym.is(Synthetic) && sym.owner.isClass && sym.owner.is(Case) + case _ => false + + /** Is `sym` a synthetic apply, copy, or copy default getter method? + * The types of these symbols are transformed in a special way without + * looking at the definitions's RHS + */ + def needsTransform(sym: SymDenotation)(using Context): Boolean = + isSyntheticCopyMethod(sym) + || isSyntheticCompanionMethod(sym, nme.apply, nme.unapply) + || isSyntheticCopyDefaultGetterMethod(sym) + + /** Method is excluded from regular capture checking. + * Excluded are synthetic class members + * - that override a synthesized case class symbol, or + * - the fromProduct method, or + * - members transformed specially as indicated by `needsTransform`. + */ + def isExcluded(sym: Symbol)(using Context): Boolean = + sym.is(Synthetic) + && sym.owner.isClass + && ( defn.caseClassSynthesized.exists( + ccsym => sym.overriddenSymbol(ccsym.owner.asClass) == ccsym) + || isSyntheticCompanionMethod(sym, nme.fromProduct) + || needsTransform(sym)) + + /** Add capture dependencies to the type of the `apply` or `copy` method of a case class. + * An apply method in a case class like this: + * case class CC(a: {d} A, b: B, {*} c: C) + * would get type + * def apply(a': {d} A, b: B, {*} c': C): {a', c'} CC { val a = {a'} A, val c = {c'} C } + * where `'` is used to indicate the difference between parameter symbol and refinement name. + * Analogous for the copy method. + */ + private def addCaptureDeps(info: Type)(using Context): Type = info match + case info: MethodType => + val trackedParams = info.paramRefs.filter(atPhase(checkCapturesPhase)(_.isTracked)) + def augmentResult(tp: Type): Type = tp match + case tp: MethodOrPoly => + tp.derivedLambdaType(resType = augmentResult(tp.resType)) + case _ => + val refined = trackedParams.foldLeft(tp) { (parent, pref) => + RefinedType(parent, pref.paramName, + CapturingType( + atPhase(ctx.phase.next)(pref.underlying.stripCapturing), + CaptureSet(pref))) + } + CapturingType(refined, CaptureSet(trackedParams*)) + if trackedParams.isEmpty then info + else augmentResult(info).showing(i"augment apply/copy type $info to $result", capt) + case info: PolyType => + info.derivedLambdaType(resType = addCaptureDeps(info.resType)) + case _ => + info + + /** Drop capture dependencies from the type of `apply` or `copy` method of a case class */ + private def dropCaptureDeps(tp: Type)(using Context): Type = tp match + case tp: MethodOrPoly => + tp.derivedLambdaType(resType = dropCaptureDeps(tp.resType)) + case CapturingType(parent, _) => + dropCaptureDeps(parent) + case RefinedType(parent, _, _) => + dropCaptureDeps(parent) + case _ => + tp + + /** Add capture information to the type of the default getter of a case class copy method */ + private def addDefaultGetterCapture(info: Type, owner: Symbol, idx: Int)(using Context): Type = info match + case info: MethodOrPoly => + info.derivedLambdaType(resType = addDefaultGetterCapture(info.resType, owner, idx)) + case info: ExprType => + info.derivedExprType(addDefaultGetterCapture(info.resType, owner, idx)) + case EventuallyCapturingType(parent, _) => + addDefaultGetterCapture(parent, owner, idx) + case info @ AnnotatedType(parent, annot) => + info.derivedAnnotatedType(addDefaultGetterCapture(parent, owner, idx), annot) + case _ if idx < owner.asClass.paramGetters.length => + val param = owner.asClass.paramGetters(idx) + val pinfo = param.info + atPhase(ctx.phase.next) { + if pinfo.captureSet.isAlwaysEmpty then info + else CapturingType(pinfo.stripCapturing, CaptureSet(param.termRef)) + } + case _ => + info + + /** Drop capture information from the type of the default getter of a case class copy method */ + private def dropDefaultGetterCapture(info: Type)(using Context): Type = info match + case info: MethodOrPoly => + info.derivedLambdaType(resType = dropDefaultGetterCapture(info.resType)) + case CapturingType(parent, _) => + parent + case info @ AnnotatedType(parent, annot) => + info.derivedAnnotatedType(dropDefaultGetterCapture(parent), annot) + case _ => + info + + /** Augment an unapply of type `(x: C): D` to `(x: {*} C): {x} D` */ + private def addUnapplyCaptures(info: Type)(using Context): Type = info match + case info: MethodType => + val paramInfo :: Nil = info.paramInfos: @unchecked + val newParamInfo = + CapturingType(paramInfo, CaptureSet.universal) + val trackedParam = info.paramRefs.head + def newResult(tp: Type): Type = tp match + case tp: MethodOrPoly => + tp.derivedLambdaType(resType = newResult(tp.resType)) + case _ => + CapturingType(tp, CaptureSet(trackedParam)) + info.derivedLambdaType(paramInfos = newParamInfo :: Nil, resType = newResult(info.resType)) + .showing(i"augment unapply type $info to $result", capt) + case info: PolyType => + info.derivedLambdaType(resType = addUnapplyCaptures(info.resType)) + + /** Drop added capture information from the type of an `unapply` */ + private def dropUnapplyCaptures(info: Type)(using Context): Type = info match + case info: MethodType => + val CapturingType(oldParamInfo, _) :: Nil = info.paramInfos: @unchecked + def oldResult(tp: Type): Type = tp match + case tp: MethodOrPoly => + tp.derivedLambdaType(resType = oldResult(tp.resType)) + case CapturingType(tp, _) => + tp + info.derivedLambdaType(paramInfos = oldParamInfo :: Nil, resType = oldResult(info.resType)) + case info: PolyType => + info.derivedLambdaType(resType = dropUnapplyCaptures(info.resType)) + + /** If `sym` refers to a synthetic apply, unapply, copy, or copy default getter method + * of a case class, transform it to account for capture information. + * The method is run in phase CheckCaptures.Pre + * @pre needsTransform(sym) + */ + def transformToCC(sym: SymDenotation)(using Context): SymDenotation = sym.name match + case DefaultGetterName(nme.copy, n) => + sym.copySymDenotation(info = addDefaultGetterCapture(sym.info, sym.owner, n)) + case nme.unapply => + sym.copySymDenotation(info = addUnapplyCaptures(sym.info)) + case nme.apply | nme.copy => + sym.copySymDenotation(info = addCaptureDeps(sym.info)) + + + /** If `sym` refers to a synthetic apply, unapply, copy, or copy default getter method + * of a case class, transform it back to what it was before the CC phase. + * @pre needsTransform(sym) + */ + def transformFromCC(sym: SymDenotation)(using Context): SymDenotation = sym.name match + case DefaultGetterName(nme.copy, n) => + sym.copySymDenotation(info = dropDefaultGetterCapture(sym.info)) + case nme.unapply => + sym.copySymDenotation(info = dropUnapplyCaptures(sym.info)) + case nme.apply | nme.copy => + sym.copySymDenotation(info = dropCaptureDeps(sym.info)) + +end Synthetics \ No newline at end of file diff --git a/compiler/src/dotty/tools/dotc/config/Config.scala b/compiler/src/dotty/tools/dotc/config/Config.scala index 67545233136d..1b0fea9184d1 100644 --- a/compiler/src/dotty/tools/dotc/config/Config.scala +++ b/compiler/src/dotty/tools/dotc/config/Config.scala @@ -235,4 +235,14 @@ object Config { inline val checkLevelsOnConstraints = false inline val checkLevelsOnInstantiation = true + /** If true, print capturing types in the form `{c} T`. + * If false, print them in the form `T @retains(c)`. + */ + inline val printCaptureSetsAsPrefix = true + + /** If true, allow mappping capture set variables under -Ycc with maps that are neither + * bijective nor idempotent. We currently do now know how to do this correctly in all + * cases, though. + */ + inline val ccAllowUnsoundMaps = false } diff --git a/compiler/src/dotty/tools/dotc/config/Printers.scala b/compiler/src/dotty/tools/dotc/config/Printers.scala index 458c149723b9..ecb189de9bb3 100644 --- a/compiler/src/dotty/tools/dotc/config/Printers.scala +++ b/compiler/src/dotty/tools/dotc/config/Printers.scala @@ -12,6 +12,7 @@ object Printers { val default = new Printer + val capt = noPrinter val constr = noPrinter val core = noPrinter val checks = noPrinter @@ -38,6 +39,7 @@ object Printers { val pickling = noPrinter val quotePickling = noPrinter val plugins = noPrinter + val recheckr = noPrinter val refcheck = noPrinter val simplify = noPrinter val staging = noPrinter diff --git a/compiler/src/dotty/tools/dotc/config/ScalaSettings.scala b/compiler/src/dotty/tools/dotc/config/ScalaSettings.scala index 8a66b5abca8a..9e34f8d726b5 100644 --- a/compiler/src/dotty/tools/dotc/config/ScalaSettings.scala +++ b/compiler/src/dotty/tools/dotc/config/ScalaSettings.scala @@ -320,11 +320,15 @@ private sealed trait YSettings: val YprofileRunGcBetweenPhases: Setting[List[String]] = PhasesSetting("-Yprofile-run-gc", "Run a GC between phases - this allows heap size to be accurate at the expense of more time. Specify a list of phases, or *", "_") //.withPostSetHook( _ => YprofileEnabled.value = true ) - // Extremely experimental language features + // Experimental language features val YnoKindPolymorphism: Setting[Boolean] = BooleanSetting("-Yno-kind-polymorphism", "Disable kind polymorphism.") val YexplicitNulls: Setting[Boolean] = BooleanSetting("-Yexplicit-nulls", "Make reference types non-nullable. Nullable types can be expressed with unions: e.g. String|Null.") val YcheckInit: Setting[Boolean] = BooleanSetting("-Ysafe-init", "Ensure safe initialization of objects") val YrequireTargetName: Setting[Boolean] = BooleanSetting("-Yrequire-targetName", "Warn if an operator is defined without a @targetName annotation") + val YrecheckTest: Setting[Boolean] = BooleanSetting("-Yrecheck-test", "Run basic rechecking (internal test only)") + val Ycc: Setting[Boolean] = BooleanSetting("-Ycc", "Check captured references (warning: extremely experimental and unstable)") + val YccDebug: Setting[Boolean] = BooleanSetting("-Ycc-debug", "Used in conjunction with -Ycc, debug info for captured references") + val YccNoAbbrev: Setting[Boolean] = BooleanSetting("-Ycc-no-abbrev", "Used in conjunction with -Ycc, suppress type abbreviations") /** Area-specific debug output */ val YexplainLowlevel: Setting[Boolean] = BooleanSetting("-Yexplain-lowlevel", "When explaining type errors, show types at a lower level.") diff --git a/compiler/src/dotty/tools/dotc/core/Annotations.scala b/compiler/src/dotty/tools/dotc/core/Annotations.scala index 8eca364e4621..05210ec60811 100644 --- a/compiler/src/dotty/tools/dotc/core/Annotations.scala +++ b/compiler/src/dotty/tools/dotc/core/Annotations.scala @@ -45,7 +45,7 @@ object Annotations { /** The tree evaluation has finished. */ def isEvaluated: Boolean = true - /** Normally, type map over all tree nodes of this annotation, but can + /** Normally, applies a type map to all tree nodes of this annotation, but can * be overridden. Returns EmptyAnnotation if type type map produces a range * type, since ranges cannot be types of trees. */ @@ -83,6 +83,10 @@ object Annotations { def sameAnnotation(that: Annotation)(using Context): Boolean = symbol == that.symbol && tree.sameTree(that.tree) + + /** Operations for hash-consing, can be overridden */ + def hash: Int = System.identityHashCode(this) + def eql(that: Annotation) = this eq that } case class ConcreteAnnotation(t: Tree) extends Annotation: diff --git a/compiler/src/dotty/tools/dotc/core/Definitions.scala b/compiler/src/dotty/tools/dotc/core/Definitions.scala index 3e2373d3bd4b..83d945352321 100644 --- a/compiler/src/dotty/tools/dotc/core/Definitions.scala +++ b/compiler/src/dotty/tools/dotc/core/Definitions.scala @@ -8,12 +8,14 @@ import Flags._, Scopes._, Decorators._, NameOps._, Periods._, NullOpsDecorator._ import unpickleScala2.Scala2Unpickler.ensureConstructor import scala.collection.mutable import collection.mutable +import Denotations.{SingleDenotation, staticRef} import util.{SimpleIdentityMap, SourceFile, NoSource} import typer.ImportInfo.RootRef import Comments.CommentsContext import Comments.Comment import util.Spans.NoSpan import Symbols.requiredModuleRef +import cc.{CapturingType, CaptureSet, EventuallyCapturingType} import scala.annotation.tailrec @@ -88,7 +90,7 @@ class Definitions { * * FunctionN traits follow this template: * - * trait FunctionN[T0,...T{N-1}, R] extends Object { + * trait FunctionN[-T0,...-T{N-1}, +R] extends Object { * def apply($x0: T0, ..., $x{N_1}: T{N-1}): R * } * @@ -98,46 +100,65 @@ class Definitions { * * ContextFunctionN traits follow this template: * - * trait ContextFunctionN[T0,...,T{N-1}, R] extends Object { + * trait ContextFunctionN[-T0,...,-T{N-1}, +R] extends Object { * def apply(using $x0: T0, ..., $x{N_1}: T{N-1}): R * } * * ErasedFunctionN traits follow this template: * - * trait ErasedFunctionN[T0,...,T{N-1}, R] extends Object { + * trait ErasedFunctionN[-T0,...,-T{N-1}, +R] extends Object { * def apply(erased $x0: T0, ..., $x{N_1}: T{N-1}): R * } * * ErasedContextFunctionN traits follow this template: * - * trait ErasedContextFunctionN[T0,...,T{N-1}, R] extends Object { + * trait ErasedContextFunctionN[-T0,...,-T{N-1}, +R] extends Object { * def apply(using erased $x0: T0, ..., $x{N_1}: T{N-1}): R * } * * ErasedFunctionN and ErasedContextFunctionN erase to Function0. + * + * ImpureXYZFunctionN follow this template: + * + * type ImpureXYZFunctionN[-T0,...,-T{N-1}, +R] = {*} XYZFunctionN[T0,...,T{N-1}, R] */ - def newFunctionNTrait(name: TypeName): ClassSymbol = { + private def newFunctionNType(name: TypeName): Symbol = { + val impure = name.startsWith("Impure") val completer = new LazyType { def complete(denot: SymDenotation)(using Context): Unit = { - val cls = denot.asClass.classSymbol - val decls = newScope val arity = name.functionArity - val paramNamePrefix = tpnme.scala ++ str.NAME_JOIN ++ name ++ str.EXPAND_SEPARATOR - val argParamRefs = List.tabulate(arity) { i => - enterTypeParam(cls, paramNamePrefix ++ "T" ++ (i + 1).toString, Contravariant, decls).typeRef - } - val resParamRef = enterTypeParam(cls, paramNamePrefix ++ "R", Covariant, decls).typeRef - val methodType = MethodType.companion( - isContextual = name.isContextFunction, - isImplicit = false, - isErased = name.isErasedFunction) - decls.enter(newMethod(cls, nme.apply, methodType(argParamRefs, resParamRef), Deferred)) - denot.info = - ClassInfo(ScalaPackageClass.thisType, cls, ObjectType :: Nil, decls) + if impure then + val argParamNames = List.tabulate(arity)(tpnme.syntheticTypeParamName) + val argVariances = List.fill(arity)(Contravariant) + val underlyingName = name.asSimpleName.drop(6) + val underlyingClass = ScalaPackageVal.requiredClass(underlyingName) + denot.info = TypeAlias( + HKTypeLambda(argParamNames :+ "R".toTypeName, argVariances :+ Covariant)( + tl => List.fill(arity + 1)(TypeBounds.empty), + tl => CapturingType(underlyingClass.typeRef.appliedTo(tl.paramRefs), + CaptureSet.universal) + )) + else + val cls = denot.asClass.classSymbol + val decls = newScope + val paramNamePrefix = tpnme.scala ++ str.NAME_JOIN ++ name ++ str.EXPAND_SEPARATOR + val argParamRefs = List.tabulate(arity) { i => + enterTypeParam(cls, paramNamePrefix ++ "T" ++ (i + 1).toString, Contravariant, decls).typeRef + } + val resParamRef = enterTypeParam(cls, paramNamePrefix ++ "R", Covariant, decls).typeRef + val methodType = MethodType.companion( + isContextual = name.isContextFunction, + isImplicit = false, + isErased = name.isErasedFunction) + decls.enter(newMethod(cls, nme.apply, methodType(argParamRefs, resParamRef), Deferred)) + denot.info = + ClassInfo(ScalaPackageClass.thisType, cls, ObjectType :: Nil, decls) } } - val flags = Trait | NoInits - newPermanentClassSymbol(ScalaPackageClass, name, flags, completer) + if impure then + newPermanentSymbol(ScalaPackageClass, name, EmptyFlags, completer) + else + newPermanentClassSymbol(ScalaPackageClass, name, Trait | NoInits, completer) } private def newMethod(cls: ClassSymbol, name: TermName, info: Type, flags: FlagSet = EmptyFlags): TermSymbol = @@ -146,11 +167,13 @@ class Definitions { private def enterMethod(cls: ClassSymbol, name: TermName, info: Type, flags: FlagSet = EmptyFlags): TermSymbol = newMethod(cls, name, info, flags).entered - private def enterAliasType(name: TypeName, tpe: Type, flags: FlagSet = EmptyFlags): TypeSymbol = { - val sym = newPermanentSymbol(ScalaPackageClass, name, flags, TypeAlias(tpe)) + private def enterPermanentSymbol(name: Name, info: Type, flags: FlagSet = EmptyFlags): Symbol = + val sym = newPermanentSymbol(ScalaPackageClass, name, flags, info) ScalaPackageClass.currentPackageDecls.enter(sym) sym - } + + private def enterAliasType(name: TypeName, tpe: Type, flags: FlagSet = EmptyFlags): TypeSymbol = + enterPermanentSymbol(name, TypeAlias(tpe), flags).asType private def enterBinaryAlias(name: TypeName, op: (Type, Type) => Type): TypeSymbol = enterAliasType(name, @@ -209,7 +232,7 @@ class Definitions { val cls = ScalaPackageVal.moduleClass.asClass cls.info.decls.openForMutations.useSynthesizer( name => - if (name.isTypeName && name.isSyntheticFunction) newFunctionNTrait(name.asTypeName) + if (name.isTypeName && name.isSyntheticFunction) newFunctionNType(name.asTypeName) else NoSymbol) cls } @@ -446,6 +469,7 @@ class Definitions { @tu lazy val andType: TypeSymbol = enterBinaryAlias(tpnme.AND, AndType(_, _)) @tu lazy val orType: TypeSymbol = enterBinaryAlias(tpnme.OR, OrType(_, _, soft = false)) + @tu lazy val captureRoot: TermSymbol = enterPermanentSymbol(nme.CAPTURE_ROOT, AnyType).asTerm /** Method representing a throw */ @tu lazy val throwMethod: TermSymbol = enterMethod(OpsPackageClass, nme.THROWkw, @@ -947,6 +971,8 @@ class Definitions { @tu lazy val BeanPropertyAnnot: ClassSymbol = requiredClass("scala.beans.BeanProperty") @tu lazy val BooleanBeanPropertyAnnot: ClassSymbol = requiredClass("scala.beans.BooleanBeanProperty") @tu lazy val BodyAnnot: ClassSymbol = requiredClass("scala.annotation.internal.Body") + @tu lazy val CapabilityAnnot: ClassSymbol = requiredClass("scala.annotation.capability") + @tu lazy val CaptureCheckedAnnot: ClassSymbol = requiredClass("scala.annotation.internal.CaptureChecked") @tu lazy val ChildAnnot: ClassSymbol = requiredClass("scala.annotation.internal.Child") @tu lazy val ContextResultCountAnnot: ClassSymbol = requiredClass("scala.annotation.internal.ContextResultCount") @tu lazy val ProvisionalSuperClassAnnot: ClassSymbol = requiredClass("scala.annotation.internal.ProvisionalSuperClass") @@ -989,6 +1015,10 @@ class Definitions { @tu lazy val FunctionalInterfaceAnnot: ClassSymbol = requiredClass("java.lang.FunctionalInterface") @tu lazy val TargetNameAnnot: ClassSymbol = requiredClass("scala.annotation.targetName") @tu lazy val VarargsAnnot: ClassSymbol = requiredClass("scala.annotation.varargs") + @tu lazy val SinceAnnot: ClassSymbol = requiredClass("scala.annotation.since") + @tu lazy val RequiresCapabilityAnnot: ClassSymbol = requiredClass("scala.annotation.internal.requiresCapability") + @tu lazy val RetainsAnnot: ClassSymbol = requiredClass("scala.annotation.retains") + @tu lazy val RetainsByNameAnnot: ClassSymbol = requiredClass("scala.annotation.retainsByName") @tu lazy val JavaRepeatableAnnot: ClassSymbol = requiredClass("java.lang.annotation.Repeatable") @@ -1122,9 +1152,16 @@ class Definitions { } } + /** Extractor for context function types representing by-name parameters, of the form + * `() ?=> T`. + * Under -Ycc, this becomes `() ?-> T` or `{r1, ..., rN} () ?-> T`. + */ object ByNameFunction: - def apply(tp: Type)(using Context): Type = - defn.ContextFunction0.typeRef.appliedTo(tp :: Nil) + def apply(tp: Type)(using Context): Type = tp match + case tp @ EventuallyCapturingType(tp1, refs) if tp.annot.symbol == RetainsByNameAnnot => + CapturingType(apply(tp1), refs) + case _ => + defn.ContextFunction0.typeRef.appliedTo(tp :: Nil) def unapply(tp: Type)(using Context): Option[Type] = tp match case tp @ AppliedType(tycon, arg :: Nil) if defn.isByNameFunctionClass(tycon.typeSymbol) => Some(arg) @@ -1335,6 +1372,10 @@ class Definitions { def SpecializedTuple(base: Symbol, args: List[Type])(using Context): Symbol = base.owner.requiredClass(base.name.specializedName(args)) + /** Cached function types of arbitary arities. + * Function types are created on demand with newFunctionNTrait, which is + * called from a synthesizer installed in ScalaPackageClass. + */ private class FunType(prefix: String): private var classRefs: Array[TypeRef | Null] = new Array(22) def apply(n: Int): TypeRef = @@ -1342,32 +1383,44 @@ class Definitions { val classRefs1 = new Array[TypeRef | Null](classRefs.length * 2) Array.copy(classRefs, 0, classRefs1, 0, classRefs.length) classRefs = classRefs1 + val funName = s"scala.$prefix$n" if classRefs(n) == null then - classRefs(n) = requiredClassRef(prefix + n.toString) + classRefs(n) = + if prefix.startsWith("Impure") + then staticRef(funName.toTypeName).symbol.typeRef + else requiredClassRef(funName) classRefs(n).nn - - private val erasedContextFunType = FunType("scala.ErasedContextFunction") - private val contextFunType = FunType("scala.ContextFunction") - private val erasedFunType = FunType("scala.ErasedFunction") - private val funType = FunType("scala.Function") - - def FunctionClass(n: Int, isContextual: Boolean = false, isErased: Boolean = false)(using Context): Symbol = - ( if isContextual && isErased then erasedContextFunType(n) - else if isContextual then contextFunType(n) - else if isErased then erasedFunType(n) - else funType(n) - ).symbol.asClass + end FunType + + private def funTypeIdx(isContextual: Boolean, isErased: Boolean, isImpure: Boolean): Int = + (if isContextual then 1 else 0) + + (if isErased then 2 else 0) + + (if isImpure then 4 else 0) + + private val funTypeArray: IArray[FunType] = + val arr = Array.ofDim[FunType](8) + val choices = List(false, true) + for contxt <- choices; erasd <- choices; impure <- choices do + var str = "Function" + if contxt then str = "Context" + str + if erasd then str = "Erased" + str + if impure then str = "Impure" + str + arr(funTypeIdx(contxt, erasd, impure)) = FunType(str) + IArray.unsafeFromArray(arr) + + def FunctionSymbol(n: Int, isContextual: Boolean = false, isErased: Boolean = false, isImpure: Boolean = false)(using Context): Symbol = + funTypeArray(funTypeIdx(isContextual, isErased, isImpure))(n).symbol @tu lazy val Function0_apply: Symbol = Function0.requiredMethod(nme.apply) @tu lazy val ContextFunction0_apply: Symbol = ContextFunction0.requiredMethod(nme.apply) - @tu lazy val Function0: Symbol = FunctionClass(0) - @tu lazy val Function1: Symbol = FunctionClass(1) - @tu lazy val Function2: Symbol = FunctionClass(2) - @tu lazy val ContextFunction0: Symbol = FunctionClass(0, isContextual = true) + @tu lazy val Function0: Symbol = FunctionSymbol(0) + @tu lazy val Function1: Symbol = FunctionSymbol(1) + @tu lazy val Function2: Symbol = FunctionSymbol(2) + @tu lazy val ContextFunction0: Symbol = FunctionSymbol(0, isContextual = true) - def FunctionType(n: Int, isContextual: Boolean = false, isErased: Boolean = false)(using Context): TypeRef = - FunctionClass(n, isContextual && !ctx.erasedTypes, isErased).typeRef + def FunctionType(n: Int, isContextual: Boolean = false, isErased: Boolean = false, isImpure: Boolean = false)(using Context): TypeRef = + FunctionSymbol(n, isContextual && !ctx.erasedTypes, isErased, isImpure).typeRef lazy val PolyFunctionClass = requiredClass("scala.PolyFunction") def PolyFunctionType = PolyFunctionClass.typeRef @@ -1409,6 +1462,10 @@ class Definitions { */ def isFunctionClass(cls: Symbol): Boolean = scalaClassName(cls).isFunction + /** Is a function class, or an impure function type alias */ + def isFunctionSymbol(sym: Symbol): Boolean = + sym.isType && (sym.owner eq ScalaPackageClass) && sym.name.isFunction + /** Is a function class where * - FunctionN for N >= 0 and N != XXL */ @@ -1593,6 +1650,9 @@ class Definitions { def isFunctionType(tp: Type)(using Context): Boolean = isNonRefinedFunction(tp.dropDependentRefinement) + def isFunctionOrPolyType(tp: Type)(using Context): Boolean = + isFunctionType(tp) || (tp.typeSymbol eq defn.PolyFunctionClass) + private def withSpecMethods(cls: ClassSymbol, bases: List[Name], paramTypes: Set[TypeRef]) = for base <- bases; tp <- paramTypes do cls.enter(newSymbol(cls, base.specializedName(List(tp)), Method, ExprType(tp))) @@ -1639,7 +1699,7 @@ class Definitions { def isSpecializableFunction(cls: ClassSymbol, paramTypes: List[Type], retType: Type)(using Context): Boolean = paramTypes.length <= 2 - && (cls.derivesFrom(FunctionClass(paramTypes.length)) || isByNameFunctionClass(cls)) + && (cls.derivesFrom(FunctionSymbol(paramTypes.length)) || isByNameFunctionClass(cls)) && isSpecializableFunctionSAM(paramTypes, retType) /** If the Single Abstract Method of a Function class has this type, is it specializable? */ @@ -1917,7 +1977,8 @@ class Definitions { this.initCtx = ctx if (!isInitialized) { // force initialization of every symbol that is synthesized or hijacked by the compiler - val forced = syntheticCoreClasses ++ syntheticCoreMethods ++ ScalaValueClasses() :+ JavaEnumClass + val forced = syntheticCoreClasses ++ syntheticCoreMethods ++ ScalaValueClasses() + ++ (JavaEnumClass :: (if ctx.settings.Ycc.value then captureRoot :: Nil else Nil)) isInitialized = true } diff --git a/compiler/src/dotty/tools/dotc/core/Flags.scala b/compiler/src/dotty/tools/dotc/core/Flags.scala index cb590e2384a0..72428d02f5d3 100644 --- a/compiler/src/dotty/tools/dotc/core/Flags.scala +++ b/compiler/src/dotty/tools/dotc/core/Flags.scala @@ -314,8 +314,8 @@ object Flags { /** A Scala 2x super accessor / an unpickled Scala 2.x class */ val (SuperParamAliasOrScala2x @ _, SuperParamAlias @ _, Scala2x @ _) = newFlags(26, "", "") - /** A parameter with a default value */ - val (_, HasDefault @ _, _) = newFlags(27, "") + /** A parameter with a default value / an impure untpd.FunctionWithMods type */ + val (_, HasDefault @ _, Impure @ _) = newFlags(27, "", "<{*}>") /** An extension method, or a collective extension instance */ val (Extension @ _, ExtensionMethod @ _, _) = newFlags(28, "") @@ -404,7 +404,9 @@ object Flags { /** Children were queried on this class */ val (_, _, ChildrenQueried @ _) = newFlags(56, "") - /** A module variable (Scala 2.x only) */ + /** A module variable (Scala 2.x only) + * (re-used as a flag for private parameter accessors in Recheck) + */ val (_, Scala2ModuleVar @ _, _) = newFlags(57, "") /** A macro */ diff --git a/compiler/src/dotty/tools/dotc/core/NameOps.scala b/compiler/src/dotty/tools/dotc/core/NameOps.scala index 98fcf0d9e303..8ec095036b35 100644 --- a/compiler/src/dotty/tools/dotc/core/NameOps.scala +++ b/compiler/src/dotty/tools/dotc/core/NameOps.scala @@ -199,44 +199,46 @@ object NameOps { else collectDigits(acc * 10 + d, idx + 1) collectDigits(0, suffixStart + 8) - /** name[0..suffixStart) == `str` */ - private def isPreceded(str: String, suffixStart: Int) = - str.length == suffixStart && name.firstPart.startsWith(str) + private def isFunctionPrefix(suffixStart: Int, mustHave: String = "")(using Context): Boolean = + suffixStart >= 0 + && { + val first = name.firstPart + var found = mustHave.isEmpty + def skip(idx: Int, str: String) = + if first.startsWith(str, idx) then + if str == mustHave then found = true + idx + str.length + else idx + val start = if ctx.settings.Ycc.value then skip(0, "Impure") else 0 + skip(skip(start, "Erased"), "Context") == suffixStart + && found + } /** Same as `funArity`, except that it returns -1 if the prefix - * is not one of "", "Context", "Erased", "ErasedContext" + * is not one of a (possibly empty) concatenation of a subset of + * "Impure" (only under -Ycc), "Erased" and "Context" (in that order). */ - private def checkedFunArity(suffixStart: Int): Int = - if suffixStart == 0 - || isPreceded("Context", suffixStart) - || isPreceded("Erased", suffixStart) - || isPreceded("ErasedContext", suffixStart) - then funArity(suffixStart) - else -1 + private def checkedFunArity(suffixStart: Int)(using Context): Int = + if isFunctionPrefix(suffixStart) then funArity(suffixStart) else -1 /** Is a function name, i.e one of FunctionXXL, FunctionN, ContextFunctionN, ErasedFunctionN, ErasedContextFunctionN for N >= 0 */ - def isFunction: Boolean = + def isFunction(using Context): Boolean = (name eq tpnme.FunctionXXL) || checkedFunArity(functionSuffixStart) >= 0 /** Is a function name * - FunctionN for N >= 0 */ - def isPlainFunction: Boolean = functionArity >= 0 + def isPlainFunction(using Context): Boolean = functionArity >= 0 - /** Is an context function name, i.e one of ContextFunctionN or ErasedContextFunctionN for N >= 0 - */ - def isContextFunction: Boolean = + /** Is a function name that contains `mustHave` as a substring */ + private def isSpecificFunction(mustHave: String)(using Context): Boolean = val suffixStart = functionSuffixStart - (isPreceded("Context", suffixStart) || isPreceded("ErasedContext", suffixStart)) - && funArity(suffixStart) >= 0 + isFunctionPrefix(suffixStart, mustHave) && funArity(suffixStart) >= 0 - /** Is an erased function name, i.e. one of ErasedFunctionN, ErasedContextFunctionN for N >= 0 - */ - def isErasedFunction: Boolean = - val suffixStart = functionSuffixStart - (isPreceded("Erased", suffixStart) || isPreceded("ErasedContext", suffixStart)) - && funArity(suffixStart) >= 0 + def isContextFunction(using Context): Boolean = isSpecificFunction("Context") + def isErasedFunction(using Context): Boolean = isSpecificFunction("Erased") + def isImpureFunction(using Context): Boolean = isSpecificFunction("Impure") /** Is a synthetic function name, i.e. one of * - FunctionN for N > 22 @@ -244,12 +246,12 @@ object NameOps { * - ErasedFunctionN for N >= 0 * - ErasedContextFunctionN for N >= 0 */ - def isSyntheticFunction: Boolean = + def isSyntheticFunction(using Context): Boolean = val suffixStart = functionSuffixStart if suffixStart == 0 then funArity(suffixStart) > MaxImplementedFunctionArity else checkedFunArity(suffixStart) >= 0 - def functionArity: Int = + def functionArity(using Context): Int = val suffixStart = functionSuffixStart if suffixStart >= 0 then checkedFunArity(suffixStart) else -1 diff --git a/compiler/src/dotty/tools/dotc/core/NamerOps.scala b/compiler/src/dotty/tools/dotc/core/NamerOps.scala index 1a878c9547b1..fa0a89349b5e 100644 --- a/compiler/src/dotty/tools/dotc/core/NamerOps.scala +++ b/compiler/src/dotty/tools/dotc/core/NamerOps.scala @@ -191,4 +191,32 @@ object NamerOps: cls.registeredCompanion = modcls modcls.registeredCompanion = cls + /** For secondary constructors, make it known in the context that their type parameters + * are aliases of the class type parameters. + * @return if `sym` is a secondary constructor, a fresh context that + * contains GADT constraints linking the type parameters. + */ + def linkConstructorParams(sym: Symbol)(using Context): Context = + if sym.isConstructor && !sym.isPrimaryConstructor then + sym.rawParamss match + case (tparams @ (tparam :: _)) :: _ if tparam.isType => + val rhsCtx = ctx.fresh.setFreshGADTBounds + linkConstructorParams(sym, tparams, rhsCtx) + rhsCtx + case _ => + ctx + else ctx + + /** For secondary constructor `sym`, make it known in the given context `rhsCtx` + * that their type parameters are aliases of the class type parameters. This is done + * by (ab?)-using GADT constraints. See pos/i941.scala. + */ + def linkConstructorParams(sym: Symbol, tparams: List[Symbol], rhsCtx: Context)(using Context): Unit = + rhsCtx.gadt.addToConstraint(tparams) + tparams.lazyZip(sym.owner.typeParams).foreach { (psym, tparam) => + val tr = tparam.typeRef + rhsCtx.gadt.addBound(psym, tr, isUpper = false) + rhsCtx.gadt.addBound(psym, tr, isUpper = true) + } + end NamerOps diff --git a/compiler/src/dotty/tools/dotc/core/OrderingConstraint.scala b/compiler/src/dotty/tools/dotc/core/OrderingConstraint.scala index 54df9cdd9535..5f267a1c242a 100644 --- a/compiler/src/dotty/tools/dotc/core/OrderingConstraint.scala +++ b/compiler/src/dotty/tools/dotc/core/OrderingConstraint.scala @@ -12,6 +12,7 @@ import config.Printers.constr import reflect.ClassTag import annotation.tailrec import annotation.internal.sharable +import cc.{CapturingType, derivedCapturingType} object OrderingConstraint { @@ -333,6 +334,9 @@ class OrderingConstraint(private val boundsMap: ParamBounds, case tp: TypeVar => val underlying1 = recur(tp.underlying, fromBelow) if underlying1 ne tp.underlying then underlying1 else tp + case CapturingType(parent, refs) => + val parent1 = recur(parent, fromBelow) + if parent1 ne parent then tp.derivedCapturingType(parent1, refs) else tp case tp: AnnotatedType => val parent1 = recur(tp.parent, fromBelow) if parent1 ne tp.parent then tp.derivedAnnotatedType(parent1, tp.annot) else tp diff --git a/compiler/src/dotty/tools/dotc/core/Phases.scala b/compiler/src/dotty/tools/dotc/core/Phases.scala index b5ad289554c9..b4a2dcac1b85 100644 --- a/compiler/src/dotty/tools/dotc/core/Phases.scala +++ b/compiler/src/dotty/tools/dotc/core/Phases.scala @@ -14,9 +14,11 @@ import dotty.tools.dotc.transform.MegaPhase._ import dotty.tools.dotc.transform._ import Periods._ import parsing.Parser +import printing.XprintMode import typer.{TyperPhase, RefChecks} +import cc.CheckCaptures import typer.ImportInfo.withRootImports -import ast.tpd +import ast.{tpd, untpd} import scala.annotation.internal.sharable import scala.util.control.NonFatal @@ -217,6 +219,7 @@ object Phases { private var myCountOuterAccessesPhase: Phase = _ private var myFlattenPhase: Phase = _ private var myGenBCodePhase: Phase = _ + private var myCheckCapturesPhase: Phase = _ final def parserPhase: Phase = myParserPhase final def typerPhase: Phase = myTyperPhase @@ -240,6 +243,7 @@ object Phases { final def countOuterAccessesPhase = myCountOuterAccessesPhase final def flattenPhase: Phase = myFlattenPhase final def genBCodePhase: Phase = myGenBCodePhase + final def checkCapturesPhase: Phase = myCheckCapturesPhase private def setSpecificPhases() = { def phaseOfClass(pclass: Class[?]) = phases.find(pclass.isInstance).getOrElse(NoPhase) @@ -265,7 +269,8 @@ object Phases { myFlattenPhase = phaseOfClass(classOf[Flatten]) myExplicitOuterPhase = phaseOfClass(classOf[ExplicitOuter]) myGettersPhase = phaseOfClass(classOf[Getters]) - myGenBCodePhase = phaseOfClass(classOf[GenBCode]) + myGenBCodePhase = phaseOfClass(classOf[GenBCode]) + myCheckCapturesPhase = phaseOfClass(classOf[CheckCaptures]) } final def isAfterTyper(phase: Phase): Boolean = phase.id > typerPhase.id @@ -315,6 +320,10 @@ object Phases { unitCtx.compilationUnit } + /** Convert a compilation unit's tree to a string; can be overridden */ + def show(tree: untpd.Tree)(using Context): String = + tree.show(using ctx.withProperty(XprintMode, Some(()))) + def description: String = phaseName /** Output should be checkable by TreeChecker */ @@ -445,6 +454,7 @@ object Phases { def lambdaLiftPhase(using Context): Phase = ctx.base.lambdaLiftPhase def flattenPhase(using Context): Phase = ctx.base.flattenPhase def genBCodePhase(using Context): Phase = ctx.base.genBCodePhase + def checkCapturesPhase(using Context): Phase = ctx.base.checkCapturesPhase def unfusedPhases(using Context): Array[Phase] = ctx.base.phases diff --git a/compiler/src/dotty/tools/dotc/core/StdNames.scala b/compiler/src/dotty/tools/dotc/core/StdNames.scala index 9f128a71be7b..bff957721b23 100644 --- a/compiler/src/dotty/tools/dotc/core/StdNames.scala +++ b/compiler/src/dotty/tools/dotc/core/StdNames.scala @@ -282,6 +282,7 @@ object StdNames { // ----- Term names ----------------------------------------- // Compiler-internal + val CAPTURE_ROOT: N = "*" val CONSTRUCTOR: N = "" val STATIC_CONSTRUCTOR: N = "" val EVT2U: N = "evt2u$" @@ -425,7 +426,6 @@ object StdNames { val canEqual_ : N = "canEqual" val canEqualAny : N = "canEqualAny" val checkInitialized: N = "checkInitialized" - val ClassManifestFactory: N = "ClassManifestFactory" val classOf: N = "classOf" val classType: N = "classType" val clone_ : N = "clone" @@ -563,6 +563,8 @@ object StdNames { val reflectiveSelectable: N = "reflectiveSelectable" val reify : N = "reify" val releaseFence : N = "releaseFence" + val retains: N = "retains" + val retainsByName: N = "retainsByName" val rootMirror : N = "rootMirror" val run: N = "run" val runOrElse: N = "runOrElse" @@ -723,6 +725,8 @@ object StdNames { val XOR : N = "^" val ZAND : N = "&&" val ZOR : N = "||" + val PUREARROW: N = "->" + val PURECTXARROW: N = "?->" // unary operators val UNARY_PREFIX: N = "unary_" diff --git a/compiler/src/dotty/tools/dotc/core/Substituters.scala b/compiler/src/dotty/tools/dotc/core/Substituters.scala index 0db49855bfda..3e32340b21bd 100644 --- a/compiler/src/dotty/tools/dotc/core/Substituters.scala +++ b/compiler/src/dotty/tools/dotc/core/Substituters.scala @@ -1,6 +1,8 @@ -package dotty.tools.dotc.core +package dotty.tools.dotc +package core import Types._, Symbols._, Contexts._ +import cc.CaptureSet.IdempotentCaptRefMap /** Substitution operations on types. See the corresponding `subst` and * `substThis` methods on class Type for an explanation. @@ -161,8 +163,9 @@ object Substituters: .mapOver(tp) } - final class SubstBindingMap(from: BindingType, to: BindingType)(using Context) extends DeepTypeMap { + final class SubstBindingMap(from: BindingType, to: BindingType)(using Context) extends DeepTypeMap, BiTypeMap { def apply(tp: Type): Type = subst(tp, from, to, this)(using mapCtx) + def inverse(tp: Type): Type = tp.subst(to, from) } final class Subst1Map(from: Symbol, to: Type)(using Context) extends DeepTypeMap { @@ -177,8 +180,9 @@ object Substituters: def apply(tp: Type): Type = subst(tp, from, to, this)(using mapCtx) } - final class SubstSymMap(from: List[Symbol], to: List[Symbol])(using Context) extends DeepTypeMap { + final class SubstSymMap(from: List[Symbol], to: List[Symbol])(using Context) extends DeepTypeMap, BiTypeMap { def apply(tp: Type): Type = substSym(tp, from, to, this)(using mapCtx) + def inverse(tp: Type) = tp.substSym(to, from) // implicitly requires that `to` contains no duplicates. } final class SubstThisMap(from: ClassSymbol, to: Type)(using Context) extends DeepTypeMap { @@ -189,11 +193,11 @@ object Substituters: def apply(tp: Type): Type = substRecThis(tp, from, to, this)(using mapCtx) } - final class SubstParamMap(from: ParamRef, to: Type)(using Context) extends DeepTypeMap { + final class SubstParamMap(from: ParamRef, to: Type)(using Context) extends DeepTypeMap, IdempotentCaptRefMap { def apply(tp: Type): Type = substParam(tp, from, to, this)(using mapCtx) } - final class SubstParamsMap(from: BindingType, to: List[Type])(using Context) extends DeepTypeMap { + final class SubstParamsMap(from: BindingType, to: List[Type])(using Context) extends DeepTypeMap, IdempotentCaptRefMap { def apply(tp: Type): Type = substParams(tp, from, to, this)(using mapCtx) } diff --git a/compiler/src/dotty/tools/dotc/core/SymDenotations.scala b/compiler/src/dotty/tools/dotc/core/SymDenotations.scala index a1752ccc0976..66a1e44622b8 100644 --- a/compiler/src/dotty/tools/dotc/core/SymDenotations.scala +++ b/compiler/src/dotty/tools/dotc/core/SymDenotations.scala @@ -23,6 +23,8 @@ import scala.util.control.NonFatal import config.Config import reporting._ import collection.mutable +import transform.TypeUtils._ +import cc.{CapturingType, derivedCapturingType} import scala.annotation.internal.sharable @@ -228,6 +230,11 @@ object SymDenotations { ensureCompleted(); myAnnotations } + /** The annotations without ensuring that the symbol is completed. + * Used for diagnostics where we don't want to force symbols. + */ + final def annotationsUNSAFE(using Context): List[Annotation] = myAnnotations + /** Update the annotations of this denotation */ final def annotations_=(annots: List[Annotation]): Unit = myAnnotations = annots @@ -1045,6 +1052,7 @@ object SymDenotations { case tp: TermRef => tp.symbol case tp: Symbol => sourceOfSelf(tp.info) case tp: RefinedType => sourceOfSelf(tp.parent) + case tp: AnnotatedType => sourceOfSelf(tp.parent) } sourceOfSelf(selfType) case info: LazyType => @@ -1513,8 +1521,7 @@ object SymDenotations { case tp: ExprType => hasSkolems(tp.resType) case tp: AppliedType => hasSkolems(tp.tycon) || tp.args.exists(hasSkolems) case tp: LambdaType => tp.paramInfos.exists(hasSkolems) || hasSkolems(tp.resType) - case tp: AndType => hasSkolems(tp.tp1) || hasSkolems(tp.tp2) - case tp: OrType => hasSkolems(tp.tp1) || hasSkolems(tp.tp2) + case tp: AndOrType => hasSkolems(tp.tp1) || hasSkolems(tp.tp2) case tp: AnnotatedType => hasSkolems(tp.parent) case _ => false } @@ -2170,6 +2177,9 @@ object SymDenotations { case tp: TypeParamRef => // uncachable, since baseType depends on context bounds recur(TypeComparer.bounds(tp).hi) + case CapturingType(parent, refs) => + tp.derivedCapturingType(recur(parent), refs) + case tp: TypeProxy => def computeTypeProxy = { val superTp = tp.superType diff --git a/compiler/src/dotty/tools/dotc/core/TypeApplications.scala b/compiler/src/dotty/tools/dotc/core/TypeApplications.scala index c09c79a229fb..26132c7c0205 100644 --- a/compiler/src/dotty/tools/dotc/core/TypeApplications.scala +++ b/compiler/src/dotty/tools/dotc/core/TypeApplications.scala @@ -11,6 +11,7 @@ import util.Stats._ import Names._ import Flags.{Module, Provisional} import dotty.tools.dotc.config.Config +import cc.boxedUnlessFun object TypeApplications { @@ -492,10 +493,9 @@ class TypeApplications(val self: Type) extends AnyVal { * otherwise return Nil. * Existential types in arguments are returned as TypeBounds instances. */ - final def argInfos(using Context): List[Type] = self.stripped match { - case AppliedType(tycon, args) => args + final def argInfos(using Context): List[Type] = self.stripped match + case AppliedType(tycon, args) => args.boxedUnlessFun(tycon) case _ => Nil - } /** Argument types where existential types in arguments are disallowed */ def argTypes(using Context): List[Type] = argInfos mapConserve noBounds diff --git a/compiler/src/dotty/tools/dotc/core/TypeComparer.scala b/compiler/src/dotty/tools/dotc/core/TypeComparer.scala index f53426022e44..b5fe41095516 100644 --- a/compiler/src/dotty/tools/dotc/core/TypeComparer.scala +++ b/compiler/src/dotty/tools/dotc/core/TypeComparer.scala @@ -23,6 +23,7 @@ import typer.ProtoTypes.constrained import typer.Applications.productSelectorTypes import reporting.trace import annotation.constructorOnly +import cc.{CapturingType, derivedCapturingType, CaptureSet, stripCapturing, isBoxedCapturing, boxed, boxedUnlessFun, boxedIfTypeParam} /** Provides methods to compare types. */ @@ -339,6 +340,8 @@ class TypeComparer(@constructorOnly initctx: Context) extends ConstraintHandling compareWild case tp2: LazyRef => isBottom(tp1) || !tp2.evaluating && recur(tp1, tp2.ref) + case CapturingType(_, _) => + secondTry case tp2: AnnotatedType if !tp2.isRefining => recur(tp1, tp2.parent) case tp2: ThisType => @@ -423,7 +426,7 @@ class TypeComparer(@constructorOnly initctx: Context) extends ConstraintHandling case tp2: TypeParamRef => constraint.isLess(tp1, tp2) case _ => false } || - isSubTypeWhenFrozen(bounds(tp1).hi, tp2) || { + isSubTypeWhenFrozen(bounds(tp1).hi.boxed, tp2) || { if (canConstrain(tp1) && !approx.high) addConstraint(tp1, tp2, fromBelow = false) && flagNothingBound else thirdTry @@ -458,8 +461,6 @@ class TypeComparer(@constructorOnly initctx: Context) extends ConstraintHandling // See i859.scala for an example where we hit this case. tp2.isRef(AnyClass, skipRefined = false) || !tp1.evaluating && recur(tp1.ref, tp2) - case tp1: AnnotatedType if !tp1.isRefining => - recur(tp1.parent, tp2) case AndType(tp11, tp12) => if (tp11.stripTypeVar eq tp12.stripTypeVar) recur(tp11, tp2) else thirdTry @@ -512,7 +513,13 @@ class TypeComparer(@constructorOnly initctx: Context) extends ConstraintHandling // recursively, so we do it only once. See i14870.scala as a test case, which would // loop for a very long time without the recursion brake. - case tp1: MatchType => + case CapturingType(parent1, refs1) => + if subCaptures(refs1, tp2.captureSet, frozenConstraint).isOK && sameBoxed(tp1, tp2, refs1) + then recur(parent1, tp2) + else thirdTry + case tp1: AnnotatedType if !tp1.isRefining => + recur(tp1.parent, tp2) + case tp1: MatchType => val reduced = tp1.reduced if reduced.exists then recur(reduced, tp2) && recordGadtUsageIf { MatchType.thatReducesUsingGadt(tp1) } @@ -540,7 +547,7 @@ class TypeComparer(@constructorOnly initctx: Context) extends ConstraintHandling || narrowGADTBounds(tp2, tp1, approx, isUpper = false)) && (isBottom(tp1) || GADTusage(tp2.symbol)) - isSubApproxHi(tp1, info2.lo) && (trustBounds || isSubApproxHi(tp1, info2.hi)) + isSubApproxHi(tp1, info2.lo.boxedIfTypeParam(tp2.symbol)) && (trustBounds || isSubApproxHi(tp1, info2.hi)) || compareGADT || tryLiftedToThis2 || fourthTry @@ -555,8 +562,8 @@ class TypeComparer(@constructorOnly initctx: Context) extends ConstraintHandling // Note: We would like to replace this by `if (tp1.hasHigherKind)` // but right now we cannot since some parts of the standard library rely on the // idiom that e.g. `List <: Any`. We have to bootstrap without scalac first. - if (cls2 eq AnyClass) return true - if (cls2 == defn.SingletonClass && tp1.isStable) return true + if cls2 eq AnyClass then return true + if cls2 == defn.SingletonClass && tp1.isStable then return true return tryBaseType(cls2) } else if (cls2.is(JavaDefined)) { @@ -579,7 +586,7 @@ class TypeComparer(@constructorOnly initctx: Context) extends ConstraintHandling // So if the constraint is not yet frozen, we do the same comparison again // with a frozen constraint, which means that we get a chance to do the // widening in `fourthTry` before adding to the constraint. - if (frozenConstraint) recur(tp1, bounds(tp2).lo) + if (frozenConstraint) recur(tp1, bounds(tp2).lo.boxed) else isSubTypeWhenFrozen(tp1, tp2) alwaysTrue || { if (canConstrain(tp2) && !approx.low) @@ -603,6 +610,35 @@ class TypeComparer(@constructorOnly initctx: Context) extends ConstraintHandling def compareRefined: Boolean = val tp1w = tp1.widen + + if ctx.phase == Phases.checkCapturesPhase then + + // A relaxed version of subtyping for dependent functions where method types + // are treated as contravariant. + // TODO: Merge with isSubInfo in hasMatchingMember. Currently, we can't since + // the isSubinfo of hasMatchingMember has problems dealing with PolyTypes + // (---> orphan params during pickling) + def isSubInfo(info1: Type, info2: Type): Boolean = (info1, info2) match + case (info1: PolyType, info2: PolyType) => + info1.paramNames.hasSameLengthAs(info2.paramNames) + && isSubInfo(info1.resultType, info2.resultType.subst(info2, info1)) + case (info1: MethodType, info2: MethodType) => + matchingMethodParams(info1, info2, precise = false) + && isSubInfo(info1.resultType, info2.resultType.subst(info2, info1)) + case _ => + isSubType(info1, info2) + + if defn.isFunctionType(tp2) then + tp1w.widenDealias match + case tp1: RefinedType => + return isSubInfo(tp1.refinedInfo, tp2.refinedInfo) + case _ => + else if tp2.parent.typeSymbol == defn.PolyFunctionClass then + tp1.member(nme.apply).info match + case info1: PolyType => + return isSubInfo(info1, tp2.refinedInfo) + case _ => + val skipped2 = skipMatching(tp1w, tp2) if (skipped2 eq tp2) || !Config.fastPathForRefinedSubtype then if containsAnd(tp1) then @@ -624,6 +660,7 @@ class TypeComparer(@constructorOnly initctx: Context) extends ConstraintHandling else // fast path, in particular for refinements resulting from parameterization. isSubRefinements(tp1w.asInstanceOf[RefinedType], tp2, skipped2) && recur(tp1, skipped2) + end compareRefined compareRefined case tp2: RecType => @@ -758,13 +795,27 @@ class TypeComparer(@constructorOnly initctx: Context) extends ConstraintHandling def compareTypeBounds = tp1 match { case tp1 @ TypeBounds(lo1, hi1) => ((lo2 eq NothingType) || isSubType(lo2, lo1)) && - ((hi2 eq AnyType) && !hi1.isLambdaSub || (hi2 eq AnyKindType) || isSubType(hi1, hi2)) + ((hi2 eq AnyType) && !hi1.isLambdaSub + || (hi2 eq AnyKindType) + || isSubType(hi1, hi2)) case tp1: ClassInfo => tp2 contains tp1 case _ => false } compareTypeBounds + case CapturingType(parent2, refs2) => + def compareCapturing = + val refs1 = tp1.captureSet + try + if refs1.isAlwaysEmpty then recur(tp1, parent2) + else subCaptures(refs1, refs2, frozenConstraint).isOK + && sameBoxed(tp1, tp2, refs1) + && recur(tp1.widen.stripCapturing, parent2) + catch case ex: AssertionError => + println(i"assertion failed while compare captured $tp1 <:< $tp2") + throw ex + compareCapturing || fourthTry case tp2: AnnotatedType if tp2.isRefining => (tp1.derivesAnnotWith(tp2.annot.sameAnnotation) || tp1.isBottomType) && recur(tp1, tp2.parent) @@ -781,7 +832,7 @@ class TypeComparer(@constructorOnly initctx: Context) extends ConstraintHandling } def tryBaseType(cls2: Symbol) = { - val base = nonExprBaseType(tp1, cls2) + val base = nonExprBaseType(tp1, cls2).boxedIfTypeParam(tp1.typeSymbol) if base.exists && (base ne tp1) && (!caseLambda.exists || canWidenAbstract || tp1.widen.underlyingClassRef(refinementOK = true).exists) then @@ -805,7 +856,7 @@ class TypeComparer(@constructorOnly initctx: Context) extends ConstraintHandling && (tp2.isAny || GADTusage(tp1.symbol)) (!caseLambda.exists || canWidenAbstract) - && isSubType(hi1, tp2, approx.addLow) && (trustBounds || isSubType(lo1, tp2, approx.addLow)) + && isSubType(hi1.boxedIfTypeParam(tp1.symbol), tp2, approx.addLow) && (trustBounds || isSubType(lo1, tp2, approx.addLow)) || compareGADT || tryLiftedToThis1 case _ => @@ -825,6 +876,7 @@ class TypeComparer(@constructorOnly initctx: Context) extends ConstraintHandling case tp: AppliedType => isNullable(tp.tycon) case AndType(tp1, tp2) => isNullable(tp1) && isNullable(tp2) case OrType(tp1, tp2) => isNullable(tp1) || isNullable(tp2) + case AnnotatedType(tp1, _) => isNullable(tp1) case _ => false val sym1 = tp1.symbol (sym1 eq NothingClass) && tp2.isValueTypeOrLambda || @@ -842,7 +894,16 @@ class TypeComparer(@constructorOnly initctx: Context) extends ConstraintHandling case _ => false } case _ => false - comparePaths || isSubType(tp1.underlying.widenExpr, tp2, approx.addLow) + + def tp1widened = + val tp1w = tp1.underlying.widenExpr + tp1 match + case tp1: CaptureRef if tp1.isTracked => + CapturingType(tp1w.stripCapturing, tp1.singletonCaptureSet) + case _ => + tp1w + + comparePaths || isSubType(tp1widened, tp2, approx.addLow) case tp1: RefinedType => isNewSubType(tp1.parent) case tp1: RecType => @@ -1571,7 +1632,14 @@ class TypeComparer(@constructorOnly initctx: Context) extends ConstraintHandling case _ => arg1 match case arg1: TypeBounds => - compareCaptured(arg1, arg2) + CaptureSet.subCapturesRange(arg1, arg2) + // subCapturesRange is important for invariant arguments that get expanded + // to TypeBounds where each bound is obtained by adding a captureset variable + // to the argument type. If subCapturesRange returns true we know that arg1's' + // capture set can be unified with arg2's capture set, so it only remains to + // check the underlying types with `isSubArg`. + && isSubArg(arg1.hi.stripCapturing, arg2.stripCapturing) + || compareCaptured(arg1, arg2) case ExprType(arg1res) if ctx.phaseId > elimByNamePhase.id && !ctx.erasedTypes && defn.isByNameFunction(arg2.dealias) => @@ -1581,7 +1649,7 @@ class TypeComparer(@constructorOnly initctx: Context) extends ConstraintHandling else if v > 0 then isSubType(arg1, arg2) else isSameType(arg2, arg1) - isSubArg(args1.head, args2.head) + isSubArg(args1.head.boxedUnlessFun(tp1), args2.head.boxedUnlessFun(tp1)) } && recurArgs(args1.tail, args2.tail, tparams2.tail) recurArgs(args1, args2, tparams2) @@ -1814,71 +1882,70 @@ class TypeComparer(@constructorOnly initctx: Context) extends ConstraintHandling def hasMatchingMember(name: Name, tp1: Type, tp2: RefinedType): Boolean = trace(i"hasMatchingMember($tp1 . $name :? ${tp2.refinedInfo}), mbr: ${tp1.member(name).info}", subtyping) { - def qualifies(m: SingleDenotation): Boolean = - // If the member is an abstract type and the prefix is a path, compare the member itself - // instead of its bounds. This case is needed situations like: - // - // class C { type T } - // val foo: C - // foo.type <: C { type T {= , <: , >:} foo.T } - // - // or like: - // - // class C[T] - // C[?] <: C[TV] - // - // where TV is a type variable. See i2397.scala for an example of the latter. - def matchAbstractTypeMember(info1: Type): Boolean = info1 match { - case TypeBounds(lo, hi) if lo ne hi => - tp2.refinedInfo match { - case rinfo2: TypeBounds if tp1.isStable => - val ref1 = tp1.widenExpr.select(name) - isSubType(rinfo2.lo, ref1) && isSubType(ref1, rinfo2.hi) - case _ => - false - } - case _ => false - } + // If the member is an abstract type and the prefix is a path, compare the member itself + // instead of its bounds. This case is needed situations like: + // + // class C { type T } + // val foo: C + // foo.type <: C { type T {= , <: , >:} foo.T } + // + // or like: + // + // class C[T] + // C[?] <: C[TV] + // + // where TV is a type variable. See i2397.scala for an example of the latter. + def matchAbstractTypeMember(info1: Type): Boolean = info1 match { + case TypeBounds(lo, hi) if lo ne hi => + tp2.refinedInfo match { + case rinfo2: TypeBounds if tp1.isStable => + val ref1 = tp1.widenExpr.select(name) + isSubType(rinfo2.lo, ref1) && isSubType(ref1, rinfo2.hi) + case _ => + false + } + case _ => false + } - // An additional check for type member matching: If the refinement of the - // supertype `tp2` does not refer to a member symbol defined in the parent of `tp2`. - // then the symbol referred to in the subtype must have a signature that coincides - // in its parameters with the refinement's signature. The reason for the check - // is that if the refinement does not refer to a member symbol, we will have to - // resort to reflection to invoke the member. And Java reflection needs to know exact - // erased parameter types. See neg/i12211.scala. Other reflection algorithms could - // conceivably dispatch without knowning precise parameter signatures. One can signal - // this by inheriting from the `scala.reflect.SignatureCanBeImprecise` marker trait, - // in which case the signature test is elided. - def sigsOK(symInfo: Type, info2: Type) = - tp2.underlyingClassRef(refinementOK = true).member(name).exists - || tp2.derivesFrom(defn.WithoutPreciseParameterTypesClass) - || symInfo.isInstanceOf[MethodType] - && symInfo.signature.consistentParams(info2.signature) - - def tp1IsSingleton: Boolean = tp1.isInstanceOf[SingletonType] - - // A relaxed version of isSubType, which compares method types - // under the standard arrow rule which is contravarient in the parameter types, - // but under the condition that signatures might have to match (see sigsOK) - // This relaxed version is needed to correctly compare dependent function types. - // See pos/i12211.scala. - def isSubInfo(info1: Type, info2: Type, symInfo: Type): Boolean = - info2 match - case info2: MethodType => - info1 match - case info1: MethodType => - val symInfo1 = symInfo.stripPoly - matchingMethodParams(info1, info2, precise = false) - && isSubInfo(info1.resultType, info2.resultType.subst(info2, info1), symInfo1.resultType) - && sigsOK(symInfo1, info2) - case _ => inFrozenGadtIf(tp1IsSingleton) { isSubType(info1, info2) } - case _ => inFrozenGadtIf(tp1IsSingleton) { isSubType(info1, info2) } + // An additional check for type member matching: If the refinement of the + // supertype `tp2` does not refer to a member symbol defined in the parent of `tp2`. + // then the symbol referred to in the subtype must have a signature that coincides + // in its parameters with the refinement's signature. The reason for the check + // is that if the refinement does not refer to a member symbol, we will have to + // resort to reflection to invoke the member. And Java reflection needs to know exact + // erased parameter types. See neg/i12211.scala. Other reflection algorithms could + // conceivably dispatch without knowning precise parameter signatures. One can signal + // this by inheriting from the `scala.reflect.SignatureCanBeImprecise` marker trait, + // in which case the signature test is elided. + def sigsOK(symInfo: Type, info2: Type) = + tp2.underlyingClassRef(refinementOK = true).member(name).exists + || tp2.derivesFrom(defn.WithoutPreciseParameterTypesClass) + || symInfo.isInstanceOf[MethodType] + && symInfo.signature.consistentParams(info2.signature) + + def tp1IsSingleton: Boolean = tp1.isInstanceOf[SingletonType] + + // A relaxed version of isSubType, which compares method types + // under the standard arrow rule which is contravarient in the parameter types, + // but under the condition that signatures might have to match (see sigsOK) + // This relaxed version is needed to correctly compare dependent function types. + // See pos/i12211.scala. + def isSubInfo(info1: Type, info2: Type, symInfo: Type): Boolean = + info2 match + case info2: MethodType => + info1 match + case info1: MethodType => + val symInfo1 = symInfo.stripPoly + matchingMethodParams(info1, info2, precise = false) + && isSubInfo(info1.resultType, info2.resultType.subst(info2, info1), symInfo1.resultType) + && sigsOK(symInfo1, info2) + case _ => inFrozenGadtIf(tp1IsSingleton) { isSubType(info1, info2) } + case _ => inFrozenGadtIf(tp1IsSingleton) { isSubType(info1, info2) } + def qualifies(m: SingleDenotation): Boolean = val info1 = m.info.widenExpr isSubInfo(info1, tp2.refinedInfo.widenExpr, m.symbol.info.orElse(info1)) || matchAbstractTypeMember(m.info) - end qualifies tp1.member(name) match // inlined hasAltWith for performance case mbr: SingleDenotation => qualifies(mbr) @@ -2006,8 +2073,13 @@ class TypeComparer(@constructorOnly initctx: Context) extends ConstraintHandling case formal2 :: rest2 => val formal2a = if (tp2.isParamDependent) formal2.subst(tp2, tp1) else formal2 val paramsMatch = - if precise then isSameTypeWhenFrozen(formal1, formal2a) - else isSubTypeWhenFrozen(formal2a, formal1) + if precise then + isSameTypeWhenFrozen(formal1, formal2a) + else if ctx.phase == Phases.checkCapturesPhase then + // allow to constrain capture set variables + isSubType(formal2a, formal1) + else + isSubTypeWhenFrozen(formal2a, formal1) paramsMatch && loop(rest1, rest2) case nil => false @@ -2427,6 +2499,13 @@ class TypeComparer(@constructorOnly initctx: Context) extends ConstraintHandling } case tp1: TypeVar if tp1.isInstantiated => tp1.underlying & tp2 + case CapturingType(parent1, refs1) => + if subCaptures(tp2.captureSet, refs1, frozen = true).isOK + && tp1.isBoxedCapturing == tp2.isBoxedCapturing + then + parent1 & tp2 + else + tp1.derivedCapturingType(parent1 & tp2, refs1) case tp1: AnnotatedType if !tp1.isRefining => tp1.underlying & tp2 case _ => @@ -2489,6 +2568,17 @@ class TypeComparer(@constructorOnly initctx: Context) extends ConstraintHandling false } + protected def subCaptures(refs1: CaptureSet, refs2: CaptureSet, frozen: Boolean)(using Context): CaptureSet.CompareResult = + refs1.subCaptures(refs2, frozen) + + /** Is the boxing status of tp1 and tp2 the same, or alternatively, is + * the capture sets `refs1` of `tp1` a subcapture of the empty set? + * In the latter case, boxing status does not matter. + */ + protected def sameBoxed(tp1: Type, tp2: Type, refs1: CaptureSet)(using Context): Boolean = + (tp1.isBoxedCapturing == tp2.isBoxedCapturing) + || refs1.subCaptures(CaptureSet.empty, frozenConstraint).isOK + // ----------- Diagnostics -------------------------------------------------- /** A hook for showing subtype traces. Overridden in ExplainingTypeComparer */ @@ -2764,6 +2854,7 @@ object TypeComparer { else res match case ClassInfo(_, cls, _, _, _) => cls.showLocated case bounds: TypeBounds => i"type bounds [$bounds]" + case CaptureSet.CompareResult.OK => "OK" case res: printing.Showable => res.show case _ => String.valueOf(res).nn @@ -3124,5 +3215,10 @@ class ExplainingTypeComparer(initctx: Context) extends TypeComparer(initctx) { super.gadtAddBound(sym, b, isUpper) } + override def subCaptures(refs1: CaptureSet, refs2: CaptureSet, frozen: Boolean)(using Context): CaptureSet.CompareResult = + traceIndented(i"subcaptures $refs1 <:< $refs2 ${if frozen then "frozen" else ""}") { + super.subCaptures(refs1, refs2, frozen) + } + def lastTrace(header: String): String = header + { try b.toString finally b.clear() } } diff --git a/compiler/src/dotty/tools/dotc/core/TypeErrors.scala b/compiler/src/dotty/tools/dotc/core/TypeErrors.scala index 622c30a22644..5816e1254873 100644 --- a/compiler/src/dotty/tools/dotc/core/TypeErrors.scala +++ b/compiler/src/dotty/tools/dotc/core/TypeErrors.scala @@ -74,6 +74,7 @@ extends TypeError { s"""Recursion limit exceeded. |Maybe there is an illegal cyclic reference? |If that's not the case, you could also try to increase the stacksize using the -Xss JVM option. + |For the unprocessed stack trace, compile with -Yno-decode-stacktraces. |A recurring operation is (inner to outer): |${opsString(mostCommon)}""".stripMargin } diff --git a/compiler/src/dotty/tools/dotc/core/TypeOps.scala b/compiler/src/dotty/tools/dotc/core/TypeOps.scala index 2ff2570dfa02..c087aac83cb8 100644 --- a/compiler/src/dotty/tools/dotc/core/TypeOps.scala +++ b/compiler/src/dotty/tools/dotc/core/TypeOps.scala @@ -16,6 +16,10 @@ import config.Feature import typer.ProtoTypes._ import typer.ForceDegree import typer.Inferencing._ +import typer.IfBottom +import reporting.TestingReporter +import cc.{CapturingType, derivedCapturingType, CaptureSet, isBoxed, isBoxedCapturing} +import CaptureSet.{CompareResult, IdempotentCaptRefMap, IdentityCaptRefMap} import scala.annotation.internal.sharable import scala.annotation.threadUnsafe @@ -52,7 +56,7 @@ object TypeOps: } /** The TypeMap handling the asSeenFrom */ - class AsSeenFromMap(pre: Type, cls: Symbol)(using Context) extends ApproximatingTypeMap { + class AsSeenFromMap(pre: Type, cls: Symbol)(using Context) extends ApproximatingTypeMap, IdempotentCaptRefMap { /** Set to true when the result of `apply` was approximated to avoid an unstable prefix. */ var approximated: Boolean = false @@ -164,6 +168,15 @@ object TypeOps: // with Nulls (which have no base classes). Under -Yexplicit-nulls, we take // corrective steps, so no widening is wanted. simplify(l, theMap) | simplify(r, theMap) + case tp @ CapturingType(parent, refs) => + if !ctx.mode.is(Mode.Type) + && refs.subCaptures(parent.captureSet, frozen = true).isOK + && (tp.isBoxed || !parent.isBoxedCapturing) + // fuse types with same boxed status and outer boxed with any type + then + simplify(parent, theMap) + else + mapOver case tp @ AnnotatedType(parent, annot) => val parent1 = simplify(parent, theMap) if annot.symbol == defn.UncheckedVarianceAnnot @@ -191,7 +204,7 @@ object TypeOps: } } - class SimplifyMap(using Context) extends TypeMap { + class SimplifyMap(using Context) extends IdentityCaptRefMap { def apply(tp: Type): Type = simplify(tp, this) } @@ -279,15 +292,23 @@ object TypeOps: case _ => false } - // Step 1: Get RecTypes and ErrorTypes out of the way, + // Step 1: Get RecTypes and ErrorTypes and CapturingTypes out of the way, tp1 match { - case tp1: RecType => return tp1.rebind(approximateOr(tp1.parent, tp2)) - case err: ErrorType => return err + case tp1: RecType => + return tp1.rebind(approximateOr(tp1.parent, tp2)) + case CapturingType(parent1, refs1) => + return tp1.derivedCapturingType(approximateOr(parent1, tp2), refs1) + case err: ErrorType => + return err case _ => } tp2 match { - case tp2: RecType => return tp2.rebind(approximateOr(tp1, tp2.parent)) - case err: ErrorType => return err + case tp2: RecType => + return tp2.rebind(approximateOr(tp1, tp2.parent)) + case CapturingType(parent2, refs2) => + return tp2.derivedCapturingType(approximateOr(tp1, parent2), refs2) + case err: ErrorType => + return err case _ => } @@ -417,7 +438,7 @@ object TypeOps: } /** An approximating map that drops NamedTypes matching `toAvoid` and wildcard types. */ - abstract class AvoidMap(using Context) extends AvoidWildcardsMap: + abstract class AvoidMap(using Context) extends AvoidWildcardsMap, IdempotentCaptRefMap: @threadUnsafe lazy val localParamRefs = util.HashSet[Type]() def toAvoid(tp: NamedType): Boolean diff --git a/compiler/src/dotty/tools/dotc/core/Types.scala b/compiler/src/dotty/tools/dotc/core/Types.scala index 65352aaae219..935d22d14de2 100644 --- a/compiler/src/dotty/tools/dotc/core/Types.scala +++ b/compiler/src/dotty/tools/dotc/core/Types.scala @@ -36,6 +36,8 @@ import config.Printers.{core, typr, matchTypes} import reporting.{trace, Message} import java.lang.ref.WeakReference import compiletime.uninitialized +import cc.{CapturingType, CaptureSet, derivedCapturingType, isBoxedCapturing, EventuallyCapturingType, boxedUnlessFun} +import CaptureSet.{CompareResult, IdempotentCaptRefMap, IdentityCaptRefMap} import scala.annotation.internal.sharable import scala.annotation.threadUnsafe @@ -65,7 +67,7 @@ object Types { * | | +--- SkolemType * | +- TypeParamRef * | +- RefinedOrRecType -+-- RefinedType - * | | -+-- RecType + * | | +-- RecType * | +- AppliedType * | +- TypeBounds * | +- ExprType @@ -181,13 +183,13 @@ object Types { case _ => false } - /** Is this type a (possibly refined or applied or aliased) type reference + /** 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. * It makes no sense for it to be an alias type because isRef would always * return false in that case. */ - def isRef(sym: Symbol, skipRefined: Boolean = true)(using Context): Boolean = stripped match { + def isRef(sym: Symbol, skipRefined: Boolean = true)(using Context): Boolean = this match { case this1: TypeRef => this1.info match { // see comment in Namer#typeDefSig case TypeAlias(tp) => tp.isRef(sym, skipRefined) @@ -199,6 +201,10 @@ object Types { val this2 = this1.dealias if (this2 ne this1) this2.isRef(sym, skipRefined) else this1.underlying.isRef(sym, skipRefined) + case this1: TypeVar => + this1.instanceOpt.isRef(sym, skipRefined) + case this1: AnnotatedType => + this1.parent.isRef(sym, skipRefined) case _ => false } @@ -360,6 +366,7 @@ object Types { case tp: AndOrType => tp.tp1.unusableForInference || tp.tp2.unusableForInference case tp: LambdaType => tp.resultType.unusableForInference || tp.paramInfos.exists(_.unusableForInference) case WildcardType(optBounds) => optBounds.unusableForInference + case CapturingType(parent, refs) => parent.unusableForInference || refs.elems.exists(_.unusableForInference) case _: ErrorType => true case _ => false @@ -1179,9 +1186,13 @@ object Types { */ def stripAnnots(using Context): Type = this - /** Strip TypeVars and Annotation wrappers */ + /** Strip TypeVars and Annotation and CapturingType wrappers */ def stripped(using Context): Type = this + def strippedDealias(using Context): Type = + val tp1 = stripped.dealias + if tp1 ne this then tp1.strippedDealias else this + def rewrapAnnots(tp: Type)(using Context): Type = tp.stripTypeVar match { case AnnotatedType(tp1, annot) => AnnotatedType(rewrapAnnots(tp1), annot) case _ => this @@ -1378,8 +1389,13 @@ object Types { val tp1 = tp.instanceOpt if (tp1.exists) tp1.dealias1(keep, keepOpaques) else tp case tp: AnnotatedType => - val tp1 = tp.parent.dealias1(keep, keepOpaques) - if keep(tp) then tp.derivedAnnotatedType(tp1, tp.annot) else tp1 + val parent1 = tp.parent.dealias1(keep, keepOpaques) + tp match + case tp @ CapturingType(parent, refs) => + tp.derivedCapturingType(parent1, refs) + case _ => + if keep(tp) then tp.derivedAnnotatedType(parent1, tp.annot) + else parent1 case tp: LazyRef => tp.ref.dealias1(keep, keepOpaques) case _ => this @@ -1478,7 +1494,7 @@ object Types { if (tp.tycon.isLambdaSub) NoType else tp.superType.underlyingClassRef(refinementOK) case tp: AnnotatedType => - tp.underlying.underlyingClassRef(refinementOK) + tp.parent.underlyingClassRef(refinementOK) case tp: RefinedType => if (refinementOK) tp.underlying.underlyingClassRef(refinementOK) else NoType case tp: RecType => @@ -1521,6 +1537,9 @@ object Types { case _ => if (isRepeatedParam) this.argTypesHi.head else this } + /** The capture set of this type. Overridden and cached in CaptureRef */ + def captureSet(using Context): CaptureSet = CaptureSet.ofType(this) + // ----- Normalizing typerefs over refined types ---------------------------- /** If this normalizes* to a refinement type that has a refinement for `name` (which might be followed @@ -1813,10 +1832,11 @@ object Types { /** Turn type into a function type. * @pre this is a method type without parameter dependencies. - * @param dropLast The number of trailing parameters that should be dropped - * when forming the function type. + * @param dropLast the number of trailing parameters that should be dropped + * when forming the function type. + * @param alwaysDependent if true, always create a dependent function type. */ - def toFunctionType(isJava: Boolean, dropLast: Int = 0)(using Context): Type = this match { + def toFunctionType(isJava: Boolean, dropLast: Int = 0, alwaysDependent: Boolean = false)(using Context): Type = this match { case mt: MethodType if !mt.isParamDependent => val formals1 = if (dropLast == 0) mt.paramInfos else mt.paramInfos dropRight dropLast val isContextual = mt.isContextualMethod && !ctx.erasedTypes @@ -1828,7 +1848,7 @@ object Types { val funType = defn.FunctionOf( formals1 mapConserve (_.translateFromRepeated(toArray = isJava)), result1, isContextual, isErased) - if (mt.isResultDependent) RefinedType(funType, nme.apply, mt) + if alwaysDependent || mt.isResultDependent then RefinedType(funType, nme.apply, mt) else funType } @@ -1860,6 +1880,20 @@ object Types { case _ => this } + /** A type capturing `ref` */ + def capturing(ref: CaptureRef)(using Context): Type = + if captureSet.accountsFor(ref) then this + else CapturingType(this, ref.singletonCaptureSet) + + /** A type capturing the capture set `cs`. If this type is already a capturing type + * the two capture sets are combined. + */ + def capturing(cs: CaptureSet)(using Context): Type = + if cs.isConst && cs.subCaptures(captureSet, frozen = true).isOK then this + else this match + case CapturingType(parent, cs1) => parent.capturing(cs1 ++ cs) + case _ => CapturingType(this, cs) + /** The set of distinct symbols referred to by this type, after all aliases are expanded */ def coveringSet(using Context): Set[Symbol] = (new CoveringSetAccumulator).apply(Set.empty[Symbol], this) @@ -2043,6 +2077,56 @@ object Types { def isOverloaded(using Context): Boolean = false } + /** A trait for references in CaptureSets. These can be NamedTypes, ThisTypes or ParamRefs */ + trait CaptureRef extends SingletonType: + private var myCaptureSet: CaptureSet | Null = _ + private var myCaptureSetRunId: Int = NoRunId + private var mySingletonCaptureSet: CaptureSet.Const | Null = null + + /** Can the reference be tracked? This is true for all ThisTypes or ParamRefs + * but only for some NamedTypes. + */ + def canBeTracked(using Context): Boolean + + /** Is the reference tracked? This is true if it can be tracked and the capture + * set of the underlying type is not always empty. + */ + final def isTracked(using Context): Boolean = canBeTracked && !captureSetOfInfo.isAlwaysEmpty + + /** Is this reference the root capability `*` ? */ + def isRootCapability(using Context): Boolean = false + + /** Normalize reference so that it can be compared with `eq` for equality */ + def normalizedRef(using Context): CaptureRef = this + + /** The capture set consisting of exactly this reference */ + def singletonCaptureSet(using Context): CaptureSet.Const = + if mySingletonCaptureSet == null then + mySingletonCaptureSet = CaptureSet(this.normalizedRef) + mySingletonCaptureSet.uncheckedNN + + /** The capture set of the type underlying this reference */ + def captureSetOfInfo(using Context): CaptureSet = + if ctx.runId == myCaptureSetRunId then myCaptureSet.nn + else if myCaptureSet.asInstanceOf[AnyRef] eq CaptureSet.Pending then CaptureSet.empty + else + myCaptureSet = CaptureSet.Pending + val computed = CaptureSet.ofInfo(this) + if ctx.phase != Phases.checkCapturesPhase || underlying.isProvisional then + myCaptureSet = null + else + myCaptureSet = computed + myCaptureSetRunId = ctx.runId + computed + + def invalidateCaches() = + myCaptureSetRunId = NoRunId + + override def captureSet(using Context): CaptureSet = + val cs = captureSetOfInfo + if canBeTracked && !cs.isAlwaysEmpty then singletonCaptureSet else cs + end CaptureRef + /** A trait for types that bind other types that refer to them. * Instances are: LambdaType, RecType. */ @@ -2090,7 +2174,7 @@ object Types { // --- NamedTypes ------------------------------------------------------------------ - abstract class NamedType extends CachedProxyType with ValueType { self => + abstract class NamedType extends CachedProxyType, ValueType { self => type ThisType >: this.type <: NamedType type ThisName <: Name @@ -2469,8 +2553,8 @@ object Types { val tparam = symbol val cls = tparam.owner val base = pre.baseType(cls) - base match { - case AppliedType(_, allArgs) => + base.stripped match { + case AppliedType(tycon, allArgs) => var tparams = cls.typeParams var args = allArgs var idx = 0 @@ -2478,7 +2562,7 @@ object Types { if (tparams.head.eq(tparam)) return args.head match { case _: TypeBounds if !widenAbstract => TypeRef(pre, tparam) - case arg => arg + case arg => arg.boxedUnlessFun(tycon) } tparams = tparams.tail args = args.tail @@ -2666,7 +2750,7 @@ object Types { */ abstract case class TermRef(override val prefix: Type, private var myDesignator: Designator) - extends NamedType with SingletonType with ImplicitRef { + extends NamedType, ImplicitRef, CaptureRef { type ThisType = TermRef type ThisName = TermName @@ -2690,6 +2774,24 @@ object Types { def implicitName(using Context): TermName = name def underlyingRef: TermRef = this + + /** A term reference can be tracked if it is a local term ref to a value + * or a method term parameter. References to term parameters of classes + * cannot be tracked individually. + * They are subsumed in the capture sets of the enclosing class. + * TODO: ^^^ What about call-by-name? + */ + def canBeTracked(using Context) = + ((prefix eq NoPrefix) + || symbol.is(ParamAccessor) && (prefix eq symbol.owner.thisType) + || isRootCapability + ) && !symbol.is(Method) + + override def isRootCapability(using Context): Boolean = + name == nme.CAPTURE_ROOT && symbol == defn.captureRoot + + override def normalizedRef(using Context): CaptureRef = + if canBeTracked then symbol.termRef else this } abstract case class TypeRef(override val prefix: Type, @@ -2825,7 +2927,7 @@ object Types { * Note: we do not pass a class symbol directly, because symbols * do not survive runs whereas typerefs do. */ - abstract case class ThisType(tref: TypeRef) extends CachedProxyType with SingletonType { + abstract case class ThisType(tref: TypeRef) extends CachedProxyType, CaptureRef { def cls(using Context): ClassSymbol = tref.stableInRunSymbol match { case cls: ClassSymbol => cls case _ if ctx.mode.is(Mode.Interactive) => defn.AnyClass // was observed to happen in IDE mode @@ -2839,6 +2941,8 @@ object Types { // can happen in IDE if `cls` is stale } + def canBeTracked(using Context) = true + override def computeHash(bs: Binders): Int = doHash(bs, tref) override def eql(that: Type): Boolean = that match { @@ -3623,7 +3727,7 @@ object Types { override def resultType(using Context): Type = if (dependencyStatus == FalseDeps) { // dealias all false dependencies - val dealiasMap = new TypeMap { + val dealiasMap = new TypeMap with IdentityCaptRefMap { def apply(tp: Type) = tp match { case tp @ TypeRef(pre, _) => tp.info match { @@ -3667,9 +3771,17 @@ object Types { case tp: AppliedType => tp.fold(status, compute(_, _, theAcc)) case tp: TypeVar if !tp.isInstantiated => combine(status, Provisional) case tp: TermParamRef if tp.binder eq thisLambdaType => TrueDeps - case AnnotatedType(parent, ann) => - if ann.refersToParamOf(thisLambdaType) then TrueDeps - else compute(status, parent, theAcc) + case tp: AnnotatedType => + tp match + case CapturingType(parent, refs) => + (compute(status, parent, theAcc) /: refs.elems) { + (s, ref) => ref match + case tp: TermParamRef if tp.binder eq thisLambdaType => combine(s, CaptureDeps) + case _ => s + } + case _ => + if tp.annot.refersToParamOf(thisLambdaType) then TrueDeps + else compute(status, tp.parent, theAcc) case _: ThisType | _: BoundType | NoPrefix => status case t: LazyRef => if t.completed then compute(status, t.ref, theAcc) @@ -3711,29 +3823,42 @@ object Types { /** Does result type contain references to parameters of this method type, * which cannot be eliminated by de-aliasing? */ - def isResultDependent(using Context): Boolean = dependencyStatus == TrueDeps + def isResultDependent(using Context): Boolean = + dependencyStatus == TrueDeps || dependencyStatus == CaptureDeps /** Does one of the parameter types contain references to earlier parameters * of this method type which cannot be eliminated by de-aliasing? */ def isParamDependent(using Context): Boolean = paramDependencyStatus == TrueDeps + /** Is there a dependency involving a reference in a capture set, but + * otherwise no true result dependency? + */ + def isCaptureDependent(using Context) = dependencyStatus == CaptureDeps + def newParamRef(n: Int): TermParamRef = new TermParamRefImpl(this, n) /** The least supertype of `resultType` that does not contain parameter dependencies */ def nonDependentResultApprox(using Context): Type = - if (isResultDependent) { - val dropDependencies = new ApproximatingTypeMap { + if isResultDependent then + val dropDependencies = new ApproximatingTypeMap with IdempotentCaptRefMap { def apply(tp: Type) = tp match { case tp @ TermParamRef(`thisLambdaType`, _) => range(defn.NothingType, atVariance(1)(apply(tp.underlying))) + case CapturingType(_, _) => + mapOver(tp) case AnnotatedType(parent, ann) if ann.refersToParamOf(thisLambdaType) => - mapOver(parent) + val parent1 = mapOver(parent) + if ann.symbol == defn.RetainsAnnot || ann.symbol == defn.RetainsByNameAnnot then + range( + AnnotatedType(parent1, CaptureSet.empty.toRegularAnnotation(ann.symbol)), + AnnotatedType(parent1, CaptureSet.universal.toRegularAnnotation(ann.symbol))) + else + parent1 case _ => mapOver(tp) } } dropDependencies(resultType) - } else resultType } @@ -4101,12 +4226,13 @@ object Types { private object DepStatus { type DependencyStatus = Byte - final val Unknown: DependencyStatus = 0 // not yet computed - final val NoDeps: DependencyStatus = 1 // no dependent parameters found - final val FalseDeps: DependencyStatus = 2 // all dependent parameters are prefixes of non-depended alias types - final val TrueDeps: DependencyStatus = 3 // some truly dependent parameters exist - final val StatusMask: DependencyStatus = 3 // the bits indicating actual dependency status - final val Provisional: DependencyStatus = 4 // set if dependency status can still change due to type variable instantiations + final val Unknown: DependencyStatus = 0 // not yet computed + final val NoDeps: DependencyStatus = 1 // no dependent parameters found + final val FalseDeps: DependencyStatus = 2 // all dependent parameters are prefixes of non-depended alias types + final val CaptureDeps: DependencyStatus = 3 // dependencies in capture sets under -Ycc, otherwise only false dependencoes + final val TrueDeps: DependencyStatus = 4 // some truly dependent parameters exist + final val StatusMask: DependencyStatus = 7 // the bits indicating actual dependency status + final val Provisional: DependencyStatus = 8 // set if dependency status can still change due to type variable instantiations } // ----- Type application: LambdaParam, AppliedType --------------------- @@ -4373,8 +4499,9 @@ object Types { /** Only created in `binder.paramRefs`. Use `binder.paramRefs(paramNum)` to * refer to `TermParamRef(binder, paramNum)`. */ - abstract case class TermParamRef(binder: TermLambda, paramNum: Int) extends ParamRef with SingletonType { + abstract case class TermParamRef(binder: TermLambda, paramNum: Int) extends ParamRef, CaptureRef { type BT = TermLambda + def canBeTracked(using Context) = true def kindString: String = "Term" def copyBoundType(bt: BT): Type = bt.paramRefs(paramNum) } @@ -4794,7 +4921,11 @@ object Types { if (!givenSelf.isValueType) appliedRef else if (clsd.is(Module)) givenSelf else if (ctx.erasedTypes) appliedRef - else AndType(givenSelf, appliedRef) + else givenSelf match + case givenSelf @ EventuallyCapturingType(tp, _) => + givenSelf.derivedAnnotatedType(tp & appliedRef, givenSelf.annot) + case _ => + AndType(givenSelf, appliedRef) } selfTypeCache.nn } @@ -5058,7 +5189,7 @@ object Types { // ----- Annotated and Import types ----------------------------------------------- /** An annotated type tpe @ annot */ - abstract case class AnnotatedType(parent: Type, annot: Annotation) extends CachedProxyType with ValueType { + abstract case class AnnotatedType(parent: Type, annot: Annotation) extends CachedProxyType, ValueType { override def underlying(using Context): Type = parent @@ -5087,16 +5218,16 @@ object Types { // equals comes from case class; no matching override is needed override def computeHash(bs: Binders): Int = - doHash(bs, System.identityHashCode(annot), parent) + doHash(bs, annot.hash, parent) override def hashIsStable: Boolean = parent.hashIsStable override def eql(that: Type): Boolean = that match - case that: AnnotatedType => (parent eq that.parent) && (annot eq that.annot) + case that: AnnotatedType => (parent eq that.parent) && (annot eql that.annot) case _ => false override def iso(that: Any, bs: BinderPairs): Boolean = that match - case that: AnnotatedType => parent.equals(that.parent, bs) && (annot eq that.annot) + case that: AnnotatedType => parent.equals(that.parent, bs) && (annot eql that.annot) case _ => false } @@ -5107,6 +5238,7 @@ object Types { annots.foldLeft(underlying)(apply(_, _)) def apply(parent: Type, annot: Annotation)(using Context): AnnotatedType = unique(CachedAnnotatedType(parent, annot)) + end AnnotatedType // Special type objects and classes ----------------------------------------------------- @@ -5327,7 +5459,7 @@ object Types { /** Common base class of TypeMap and TypeAccumulator */ abstract class VariantTraversal: - protected[core] var variance: Int = 1 + protected[dotc] var variance: Int = 1 inline protected def atVariance[T](v: Int)(op: => T): T = { val saved = variance @@ -5353,6 +5485,31 @@ object Types { } end VariantTraversal + /** A supertrait for some typemaps that are bijections. Used for capture checking. + * BiTypeMaps should map capture references to capture references. + */ + trait BiTypeMap extends TypeMap: + thisMap => + + /** The inverse of the type map as a function */ + def inverse(tp: Type): Type + + /** The inverse of the type map as a BiTypeMap map, which + * has the original type map as its own inverse. + */ + def inverseTypeMap(using Context) = new BiTypeMap: + def apply(tp: Type) = thisMap.inverse(tp) + def inverse(tp: Type) = thisMap.apply(tp) + + /** A restriction of this map to a function on tracked CaptureRefs */ + def forward(ref: CaptureRef): CaptureRef = this(ref) match + case result: CaptureRef if result.canBeTracked => result + + /** A restriction of the inverse to a function on tracked CaptureRefs */ + def backward(ref: CaptureRef): CaptureRef = inverse(ref) match + case result: CaptureRef if result.canBeTracked => result + end BiTypeMap + abstract class TypeMap(implicit protected var mapCtx: Context) extends VariantTraversal with (Type => Type) { thisMap => @@ -5380,6 +5537,8 @@ object Types { tp.derivedMatchType(bound, scrutinee, cases) protected def derivedAnnotatedType(tp: AnnotatedType, underlying: Type, annot: Annotation): Type = tp.derivedAnnotatedType(underlying, annot) + protected def derivedCapturingType(tp: Type, parent: Type, refs: CaptureSet): Type = + tp.derivedCapturingType(parent, refs) protected def derivedWildcardType(tp: WildcardType, bounds: Type): Type = tp.derivedWildcardType(bounds) protected def derivedSkolemType(tp: SkolemType, info: Type): Type = @@ -5415,6 +5574,12 @@ object Types { def isRange(tp: Type): Boolean = tp.isInstanceOf[Range] + protected def mapCapturingType(tp: Type, parent: Type, refs: CaptureSet, v: Int): Type = + val saved = variance + variance = v + try derivedCapturingType(tp, this(parent), refs.map(this)) + finally variance = saved + /** Map this function over given type */ def mapOver(tp: Type): Type = { record(s"TypeMap mapOver ${getClass}") @@ -5456,6 +5621,9 @@ object Types { case tp: ExprType => derivedExprType(tp, this(tp.resultType)) + case CapturingType(parent, refs) => + mapCapturingType(tp, parent, refs, variance) + case tp @ AnnotatedType(underlying, annot) => val underlying1 = this(underlying) val annot1 = annot.mapWith(this) @@ -5723,7 +5891,7 @@ object Types { if args.exists(isRange) then if variance > 0 then tp.derivedAppliedType(tycon, args.map(rangeToBounds)) match - case tp1: AppliedType if tp1.isUnreducibleWild => + case tp1: AppliedType if tp1.isUnreducibleWild && ctx.phase != checkCapturesPhase => // don't infer a type that would trigger an error later in // Checking.checkAppliedType; fall through to default handling instead case tp1 => @@ -5783,6 +5951,13 @@ object Types { if (underlying.isExactlyNothing) underlying else tp.derivedAnnotatedType(underlying, annot) } + override protected def derivedCapturingType(tp: Type, parent: Type, refs: CaptureSet): Type = + parent match // TODO ^^^ handle ranges in capture sets as well + case Range(lo, hi) => + range(derivedCapturingType(tp, lo, refs), derivedCapturingType(tp, hi, refs)) + case _ => + tp.derivedCapturingType(parent, refs) + override protected def derivedWildcardType(tp: WildcardType, bounds: Type): WildcardType = tp.derivedWildcardType(rangeToBounds(bounds)) @@ -5829,6 +6004,12 @@ object Types { tp.derivedLambdaType(tp.paramNames, formals, restpe) } + override def mapCapturingType(tp: Type, parent: Type, refs: CaptureSet, v: Int): Type = + if v == 0 then + range(mapCapturingType(tp, parent, refs, -1), mapCapturingType(tp, parent, refs, 1)) + else + super.mapCapturingType(tp, parent, refs, v) + protected def reapply(tp: Type): Type = apply(tp) } @@ -5926,6 +6107,9 @@ object Types { val x2 = atVariance(0)(this(x1, tp.scrutinee)) foldOver(x2, tp.cases) + case CapturingType(parent, refs) => + (this(x, parent) /: refs.elems)(this) + case AnnotatedType(underlying, annot) => this(applyToAnnot(x, annot), underlying) diff --git a/compiler/src/dotty/tools/dotc/core/tasty/TreeUnpickler.scala b/compiler/src/dotty/tools/dotc/core/tasty/TreeUnpickler.scala index 440871481114..6887937ed6fe 100644 --- a/compiler/src/dotty/tools/dotc/core/tasty/TreeUnpickler.scala +++ b/compiler/src/dotty/tools/dotc/core/tasty/TreeUnpickler.scala @@ -31,6 +31,8 @@ import util.{SourceFile, Property} import ast.{Trees, tpd, untpd} import Trees._ import Decorators._ +import transform.SymUtils._ +import cc.adaptFunctionTypeUnderCC import dotty.tools.tasty.{TastyBuffer, TastyReader} import TastyBuffer._ @@ -85,6 +87,9 @@ class TreeUnpickler(reader: TastyReader, /** The root owner tree. See `OwnerTree` class definition. Set by `enterTopLevel`. */ private var ownerTree: OwnerTree = _ + /** Was unpickled class compiled with -Ycc? */ + private var wasCaptureChecked: Boolean = false + private def registerSym(addr: Addr, sym: Symbol) = symAtAddr(addr) = sym @@ -371,7 +376,7 @@ class TreeUnpickler(reader: TastyReader, // Note that the lambda "rt => ..." is not equivalent to a wildcard closure! // Eta expansion of the latter puts readType() out of the expression. case APPLIEDtype => - readType().appliedTo(until(end)(readType())) + postProcessFunction(readType().appliedTo(until(end)(readType()))) case TYPEBOUNDS => val lo = readType() if nothingButMods(end) then @@ -484,6 +489,12 @@ class TreeUnpickler(reader: TastyReader, def readTermRef()(using Context): TermRef = readType().asInstanceOf[TermRef] + /** Under -Ycc, map all function types to impure function types, + * unless the unpickled class was also compiled with -Ycc. + */ + private def postProcessFunction(tp: Type)(using Context): Type = + if wasCaptureChecked then tp else tp.adaptFunctionTypeUnderCC + // ------ Reading definitions ----------------------------------------------------- private def nothingButMods(end: Addr): Boolean = @@ -631,6 +642,8 @@ class TreeUnpickler(reader: TastyReader, } registerSym(start, sym) if (isClass) { + if sym.owner.is(Package) && annots.exists(_.symbol == defn.CaptureCheckedAnnot) then + wasCaptureChecked = true sym.completer.withDecls(newScope) forkAt(templateStart).indexTemplateParams()(using localContext(sym)) } @@ -847,7 +860,7 @@ class TreeUnpickler(reader: TastyReader, def TypeDef(rhs: Tree) = ta.assignType(untpd.TypeDef(sym.name.asTypeName, rhs), sym) - def ta = ctx.typeAssigner + def ta = ctx.typeAssigner val name = readName() pickling.println(s"reading def of $name at $start") @@ -1337,11 +1350,9 @@ class TreeUnpickler(reader: TastyReader, // types. This came up in #137 of collection strawman. val tycon = readTpt() val args = until(end)(readTpt()) - val ownType = - if (tycon.symbol == defn.andType) AndType(args(0).tpe, args(1).tpe) - else if (tycon.symbol == defn.orType) OrType(args(0).tpe, args(1).tpe, soft = false) - else tycon.tpe.safeAppliedTo(args.tpes) - untpd.AppliedTypeTree(tycon, args).withType(ownType) + val tree = untpd.AppliedTypeTree(tycon, args) + val ownType = ctx.typeAssigner.processAppliedType(tree, tycon.tpe.safeAppliedTo(args.tpes)) + tree.withType(postProcessFunction(ownType)) case ANNOTATEDtpt => Annotated(readTpt(), readTerm()) case LAMBDAtpt => diff --git a/compiler/src/dotty/tools/dotc/core/unpickleScala2/Scala2Unpickler.scala b/compiler/src/dotty/tools/dotc/core/unpickleScala2/Scala2Unpickler.scala index 535159409c69..333cd9fa9ec3 100644 --- a/compiler/src/dotty/tools/dotc/core/unpickleScala2/Scala2Unpickler.scala +++ b/compiler/src/dotty/tools/dotc/core/unpickleScala2/Scala2Unpickler.scala @@ -32,6 +32,7 @@ import scala.collection.mutable import scala.collection.mutable.ListBuffer import scala.annotation.switch import reporting._ +import cc.adaptFunctionTypeUnderCC object Scala2Unpickler { @@ -822,7 +823,9 @@ class Scala2Unpickler(bytes: Array[Byte], classRoot: ClassDenotation, moduleClas // special-case in erasure, see TypeErasure#eraseInfo. OrType(args(0), args(1), soft = false) } - else if (args.nonEmpty) tycon.safeAppliedTo(EtaExpandIfHK(sym.typeParams, args.map(translateTempPoly))) + else if args.nonEmpty then + tycon.safeAppliedTo(EtaExpandIfHK(sym.typeParams, args.map(translateTempPoly))) + .adaptFunctionTypeUnderCC else if (sym.typeParams.nonEmpty) tycon.EtaExpand(sym.typeParams) else tycon case TYPEBOUNDStpe => diff --git a/compiler/src/dotty/tools/dotc/parsing/Parsers.scala b/compiler/src/dotty/tools/dotc/parsing/Parsers.scala index 28f02b7db2a0..309dd8a20aba 100644 --- a/compiler/src/dotty/tools/dotc/parsing/Parsers.scala +++ b/compiler/src/dotty/tools/dotc/parsing/Parsers.scala @@ -188,6 +188,8 @@ object Parsers { def isIdent = in.isIdent def isIdent(name: Name) = in.isIdent(name) + def isPureArrow(name: Name): Boolean = ctx.settings.Ycc.value && isIdent(name) + def isPureArrow: Boolean = isPureArrow(nme.PUREARROW) || isPureArrow(nme.PURECTXARROW) def isErased = isIdent(nme.erased) && in.erasedEnabled def isSimpleLiteral = simpleLiteralTokens.contains(in.token) @@ -426,7 +428,10 @@ object Parsers { /** Convert tree to formal parameter list */ def convertToParams(tree: Tree): List[ValDef] = - val mods = if in.token == CTXARROW then Modifiers(Given) else EmptyModifiers + val mods = + if in.token == CTXARROW || isPureArrow(nme.PURECTXARROW) + then Modifiers(Given) + else EmptyModifiers tree match case Parens(t) => convertToParam(t, mods) :: Nil @@ -901,7 +906,7 @@ object Parsers { && { lookahead.observeColonEOL(inTemplate = false) lookahead.nextToken() - canStartTypeTokens.contains(lookahead.token) + canStartInfixTypeTokens.contains(lookahead.token) } /** Is current ident a `*`, and is it followed by a `)`, `, )`, `,EOF`? The latter two are not @@ -955,6 +960,29 @@ object Parsers { isArrowIndent() else false + /** Under -Ycc: is the following token sequuence a capture set `{ref1, ..., refN}` + * followed by a token that can start a type? + */ + def followingIsCaptureSet(): Boolean = + ctx.settings.Ycc.value && { + val lookahead = in.LookaheadScanner() + def followingIsTypeStart() = + lookahead.nextToken() + canStartInfixTypeTokens.contains(lookahead.token) + || lookahead.token == LBRACKET + def recur(): Boolean = + (lookahead.isIdent || lookahead.token == THIS) && { + lookahead.nextToken() + if lookahead.token == COMMA then + lookahead.nextToken() + recur() + else + lookahead.token == RBRACE && followingIsTypeStart() + } + lookahead.nextToken() + if lookahead.token == RBRACE then followingIsTypeStart() else recur() + } + /* --------- OPERAND/OPERATOR STACK --------------------------------------- */ var opStack: List[OpInfo] = Nil @@ -1405,51 +1433,74 @@ object Parsers { case _ => false } + /** CaptureRef ::= ident | `this` + */ + def captureRef(): Tree = + if in.token == THIS then simpleRef() else termIdent() + + /** CaptureSet ::= `{` CaptureRef {`,` CaptureRef} `}` -- under -Ycc + */ + def captureSet(): List[Tree] = inBraces { + if in.token == RBRACE then Nil else commaSeparated(captureRef) + } + /** Type ::= FunType * | HkTypeParamClause ‘=>>’ Type * | FunParamClause ‘=>>’ Type * | MatchType * | InfixType + * | CaptureSet Type -- under -Ycc * FunType ::= (MonoFunType | PolyFunType) * MonoFunType ::= FunTypeArgs (‘=>’ | ‘?=>’) Type + * | (‘->’ | ‘?->’ ) Type -- under -Ycc * PolyFunType ::= HKTypeParamClause '=>' Type + * | HKTypeParamClause ‘->’ Type -- under -Ycc * FunTypeArgs ::= InfixType * | `(' [ [ ‘[using]’ ‘['erased'] FunArgType {`,' FunArgType } ] `)' * | '(' [ ‘[using]’ ‘['erased'] TypedFunParam {',' TypedFunParam } ')' */ - def typ(): Tree = { + def typ(): Tree = val start = in.offset var imods = Modifiers() def functionRest(params: List[Tree]): Tree = val paramSpan = Span(start, in.lastOffset) atSpan(start, in.offset) { - if in.token == TLARROW then + var token = in.token + if isPureArrow(nme.PUREARROW) then + token = ARROW + else if isPureArrow(nme.PURECTXARROW) then + token = CTXARROW + else if token == TLARROW then if !imods.flags.isEmpty || params.isEmpty then syntaxError(em"illegal parameter list for type lambda", start) - in.token = ARROW - else - for case ValDef(_, tpt: ByNameTypeTree, _) <- params do - syntaxError(em"parameter of type lambda may not be call-by-name", tpt.span) - in.nextToken() - return TermLambdaTypeTree(params.asInstanceOf[List[ValDef]], typ()) + token = ARROW + else if ctx.settings.Ycc.value then + // `=>` means impure function under -Ycc whereas `->` is a regular function. + imods |= Impure - if in.token == CTXARROW then + if token == CTXARROW then in.nextToken() imods |= Given + else if token == ARROW || token == TLARROW then + in.nextToken() else accept(ARROW) - val t = typ() - if imods.isOneOf(Given | Erased) then + val resultType = typ() + if token == TLARROW then + for case ValDef(_, tpt, _) <- params do + if isByNameType(tpt) then + syntaxError(em"parameter of type lambda may not be call-by-name", tpt.span) + TermLambdaTypeTree(params.asInstanceOf[List[ValDef]], resultType) + else if imods.isOneOf(Given | Erased | Impure) then if imods.is(Given) && params.isEmpty then syntaxError("context function types require at least one parameter", paramSpan) - new FunctionWithMods(params, t, imods) + FunctionWithMods(params, resultType, imods) else if !ctx.settings.YkindProjector.isDefault then - val (newParams :+ newT, tparams) = replaceKindProjectorPlaceholders(params :+ t): @unchecked - - lambdaAbstract(tparams, Function(newParams, newT)) + val (newParams :+ newResultType, tparams) = replaceKindProjectorPlaceholders(params :+ resultType): @unchecked + lambdaAbstract(tparams, Function(newParams, newResultType)) else - Function(params, t) + Function(params, resultType) } var isValParamList = false @@ -1475,18 +1526,16 @@ object Parsers { commaSeparatedRest(t, funArgType) } accept(RPAREN) - if isValParamList || in.isArrow then + if isValParamList || in.isArrow || isPureArrow then functionRest(ts) else { - val ts1 = - for (t <- ts) yield - t match { - case t@ByNameTypeTree(t1) => - syntaxError(ByNameParameterNotSupported(t), t.span) - t1 - case _ => - t - } + val ts1 = ts.mapConserve { t => + if isByNameType(t) then + syntaxError(ByNameParameterNotSupported(t), t.span) + stripByNameType(t) + else + t + } val tuple = atSpan(start) { makeTupleOrParens(ts1) } infixTypeRest( refinedTypeRest( @@ -1501,7 +1550,7 @@ object Parsers { val tparams = typeParamClause(ParamOwner.TypeParam) if (in.token == TLARROW) atSpan(start, in.skipToken())(LambdaTypeTree(tparams, toplevelTyp())) - else if (in.token == ARROW) { + else if (in.token == ARROW || isPureArrow(nme.PUREARROW)) { val arrowOffset = in.skipToken() val body = toplevelTyp() atSpan(start, arrowOffset) { @@ -1515,19 +1564,23 @@ object Parsers { } else { accept(TLARROW); typ() } } + else if in.token == LBRACE && followingIsCaptureSet() then + CapturingTypeTree(captureSet(), typ()) else if (in.token == INDENT) enclosed(INDENT, typ()) else infixType() - in.token match { + in.token match case ARROW | CTXARROW => functionRest(t :: Nil) case MATCH => matchType(t) case FORSOME => syntaxError(ExistentialTypesNoLongerSupported()); t case _ => - if (imods.is(Erased) && !t.isInstanceOf[FunctionWithMods]) - syntaxError(ErasedTypesCanOnlyBeFunctionTypes(), implicitKwPos(start)) - t - } - } + if isPureArrow then + functionRest(t :: Nil) + else + if (imods.is(Erased) && !t.isInstanceOf[FunctionWithMods]) + syntaxError(ErasedTypesCanOnlyBeFunctionTypes(), implicitKwPos(start)) + t + end typ private def makeKindProjectorTypeDef(name: TypeName): TypeDef = { val isVarianceAnnotated = name.startsWith("+") || name.startsWith("-") @@ -1583,8 +1636,8 @@ object Parsers { def infixType(): Tree = infixTypeRest(refinedType()) def infixTypeRest(t: Tree): Tree = - infixOps(t, canStartTypeTokens, refinedTypeFn, Location.ElseWhere, ParseKind.Type, - isOperator = !followingIsVararg()) + infixOps(t, canStartInfixTypeTokens, refinedTypeFn, Location.ElseWhere, ParseKind.Type, + isOperator = !followingIsVararg() && !isPureArrow) /** RefinedType ::= WithType {[nl] Refinement} */ @@ -1822,17 +1875,48 @@ object Parsers { else commaSeparated(() => argType()) } - /** FunArgType ::= Type | `=>' Type + def paramTypeOf(core: () => Tree): Tree = + if in.token == ARROW || isPureArrow(nme.PUREARROW) then + val isImpure = in.token == ARROW + val tp = atSpan(in.skipToken()) { ByNameTypeTree(core()) } + if isImpure && ctx.settings.Ycc.value then ImpureByNameTypeTree(tp) else tp + else if in.token == LBRACE && followingIsCaptureSet() then + val start = in.offset + val cs = captureSet() + val endCsOffset = in.lastOffset + val startTpOffset = in.offset + val tp = paramTypeOf(core) + val tp1 = tp match + case ImpureByNameTypeTree(tp1) => + syntaxError("explicit captureSet is superfluous for impure call-by-name type", start) + tp1 + case CapturingTypeTree(_, tp1: ByNameTypeTree) => + syntaxError("only one captureSet is allowed here", start) + tp1 + case _: ByNameTypeTree if startTpOffset > endCsOffset => + report.warning( + i"""Style: by-name `->` should immediately follow closing `}` of capture set + |to avoid confusion with function type. + |That is, `{c}-> T` instead of `{c} -> T`.""", + source.atSpan(Span(startTpOffset, startTpOffset))) + tp + case _ => + tp + CapturingTypeTree(cs, tp1) + else + core() + + /** FunArgType ::= Type + * | `=>' Type + * | [CaptureSet] `->' Type */ - val funArgType: () => Tree = () => - if (in.token == ARROW) atSpan(in.skipToken()) { ByNameTypeTree(typ()) } - else typ() + val funArgType: () => Tree = () => paramTypeOf(typ) - /** ParamType ::= [`=>'] ParamValueType + /** ParamType ::= ParamValueType + * | `=>' ParamValueType + * | [CaptureSet] `->' ParamValueType */ - def paramType(): Tree = - if (in.token == ARROW) atSpan(in.skipToken()) { ByNameTypeTree(paramValueType()) } - else paramValueType() + def paramType(): Tree = paramTypeOf(paramValueType) /** ParamValueType ::= Type [`*'] */ @@ -1898,6 +1982,8 @@ object Parsers { def typeDependingOn(location: Location): Tree = if location.inParens then typ() else if location.inPattern then rejectWildcardType(refinedType()) + else if in.token == LBRACE && followingIsCaptureSet() then + CapturingTypeTree(captureSet(), infixType()) else infixType() /* ----------- EXPRESSIONS ------------------------------------------------ */ @@ -1969,7 +2055,7 @@ object Parsers { * | ‘inline’ InfixExpr MatchClause * Bindings ::= `(' [Binding {`,' Binding}] `)' * Binding ::= (id | `_') [`:' Type] - * Ascription ::= `:' InfixType + * Ascription ::= `:' [CaptureSet] InfixType * | `:' Annotation {Annotation} * | `:' `_' `*' * Catches ::= ‘catch’ (Expr | ExprCaseClause) @@ -3099,6 +3185,7 @@ object Parsers { acceptColon() if (in.token == ARROW && ofClass && !mods.is(Local)) syntaxError(VarValParametersMayNotBeCallByName(name, mods.is(Mutable))) + // needed?, it's checked later anyway val tpt = paramType() val default = if (in.token == EQUALS) { in.nextToken(); subExpr() } @@ -3256,7 +3343,7 @@ object Parsers { ImportSelector( atSpan(in.skipToken()) { Ident(nme.EMPTY) }, bound = - if canStartTypeTokens.contains(in.token) then rejectWildcardType(infixType()) + if canStartInfixTypeTokens.contains(in.token) then rejectWildcardType(infixType()) else EmptyTree) /** id [‘as’ (id | ‘_’) */ @@ -3952,8 +4039,8 @@ object Parsers { stats.toList } - /** SelfType ::= id [‘:’ InfixType] ‘=>’ - * | ‘this’ ‘:’ InfixType ‘=>’ + /** SelfType ::= id [‘:’ [CaptureSet] InfixType] ‘=>’ + * | ‘this’ ‘:’ [CaptureSet] InfixType ‘=>’ */ def selfType(): ValDef = if (in.isIdent || in.token == THIS) @@ -3969,7 +4056,10 @@ object Parsers { val selfTpt = if in.isColon then in.nextToken() - infixType() + if in.token == LBRACE && followingIsCaptureSet() then + CapturingTypeTree(captureSet(), infixType()) + else + infixType() else if selfName == nme.WILDCARD then accept(COLONfollow) TypeTree() diff --git a/compiler/src/dotty/tools/dotc/parsing/Tokens.scala b/compiler/src/dotty/tools/dotc/parsing/Tokens.scala index 82a5297faf97..13bcfcb511df 100644 --- a/compiler/src/dotty/tools/dotc/parsing/Tokens.scala +++ b/compiler/src/dotty/tools/dotc/parsing/Tokens.scala @@ -228,7 +228,7 @@ object Tokens extends TokensCommon { final val canStartExprTokens2: TokenSet = canStartExprTokens3 | BitSet(DO) - final val canStartTypeTokens: TokenSet = literalTokens | identifierTokens | BitSet( + final val canStartInfixTypeTokens: TokenSet = literalTokens | identifierTokens | BitSet( THIS, SUPER, USCORE, LPAREN, LBRACE, AT) final val templateIntroTokens: TokenSet = BitSet(CLASS, TRAIT, OBJECT, ENUM, CASECLASS, CASEOBJECT) diff --git a/compiler/src/dotty/tools/dotc/printing/PlainPrinter.scala b/compiler/src/dotty/tools/dotc/printing/PlainPrinter.scala index d3545f09b0e7..11001fc4d115 100644 --- a/compiler/src/dotty/tools/dotc/printing/PlainPrinter.scala +++ b/compiler/src/dotty/tools/dotc/printing/PlainPrinter.scala @@ -14,13 +14,16 @@ import Variances.varianceSign import util.SourcePosition import scala.util.control.NonFatal import scala.annotation.switch +import config.Config +import cc.{CapturingType, EventuallyCapturingType, CaptureSet, isBoxed} class PlainPrinter(_ctx: Context) extends Printer { + /** The context of all public methods in Printer and subclasses. * Overridden in RefinedPrinter. */ - protected def curCtx: Context = _ctx.addMode(Mode.Printing) - protected given [DummyToEnforceDef]: Context = curCtx + def printerContext: Context = _ctx.addMode(Mode.Printing) + protected given [DummyToEnforceDef]: Context = printerContext protected def printDebug = ctx.settings.YprintDebug.value @@ -140,6 +143,9 @@ class PlainPrinter(_ctx: Context) extends Printer { + defn.ObjectClass + defn.FromJavaObjectSymbol + def toText(cs: CaptureSet): Text = + "{" ~ Text(cs.elems.toList.map(toTextCaptureRef), ", ") ~ "}" + def toText(tp: Type): Text = controlled { homogenize(tp) match { case tp: TypeType => @@ -194,6 +200,21 @@ class PlainPrinter(_ctx: Context) extends Printer { keywordStr(" match ") ~ "{" ~ casesText ~ "}" ~ (" <: " ~ toText(bound) provided !bound.isAny) }.close + case tp @ EventuallyCapturingType(parent, refs) => + def box = + Str("box ") provided tp.isBoxed //&& ctx.settings.YccDebug.value + def printRegular(refsText: Text) = + changePrec(GlobalPrec)(box ~ refsText ~ " " ~ toText(parent)) + if printDebug && !refs.isConst then + printRegular(refs.toString) + else if ctx.settings.YccDebug.value then + printRegular(refs.show) + else if !refs.isConst && refs.elems.isEmpty then + printRegular("?") + else if Config.printCaptureSetsAsPrefix then + printRegular(toText(refs)) + else + changePrec(InfixPrec)(box ~ toText(parent) ~ " @retains(" ~ toText(refs.elems.toList, ",") ~ ")") case tp: PreviousErrorType if ctx.settings.XprintTypes.value => "" // do not print previously reported error message because they may try to print this error type again recuresevely case tp: ErrorType => @@ -214,8 +235,14 @@ class PlainPrinter(_ctx: Context) extends Printer { ~ (if tp.resultType.isInstanceOf[MethodType] then ")" else "): ") ~ toText(tp.resultType) } - case tp: ExprType => - changePrec(GlobalPrec) { "=> " ~ toText(tp.resultType) } + case ExprType(ct @ EventuallyCapturingType(parent, refs)) + if ct.annot.symbol == defn.RetainsByNameAnnot => + if refs.isUniversal then changePrec(GlobalPrec) { "=> " ~ toText(parent) } + else toText(CapturingType(ExprType(parent), refs)) + case ExprType(restp) => + changePrec(GlobalPrec) { + (if ctx.settings.Ycc.value then "-> " else "=> ") ~ toText(restp) + } case tp: HKTypeLambda => changePrec(GlobalPrec) { "[" ~ paramsText(tp) ~ "]" ~ lambdaHash(tp) ~ Str(" =>> ") ~ toTextGlobal(tp.resultType) @@ -346,6 +373,11 @@ class PlainPrinter(_ctx: Context) extends Printer { } } + def toTextCaptureRef(tp: Type): Text = + homogenize(tp) match + case tp: SingletonType => toTextRef(tp) + case _ => toText(tp) + protected def isOmittablePrefix(sym: Symbol): Boolean = defn.unqualifiedOwnerTypes.exists(_.symbol == sym) || isEmptyPrefix(sym) @@ -392,8 +424,8 @@ class PlainPrinter(_ctx: Context) extends Printer { case tp: AliasingBounds => " = " ~ toText(tp.alias) case TypeBounds(lo, hi) => - (if (lo isRef defn.NothingClass) Text() else " >: " ~ toText(lo)) - ~ (if hi.isAny || (!printDebug && hi.isFromJavaObject) then Text() else " <: " ~ toText(hi)) + (if lo.isExactlyNothing then Text() else " >: " ~ toText(lo)) + ~ (if hi.isExactlyAny || (!printDebug && hi.isFromJavaObject) then Text() else " <: " ~ toText(hi)) tparamStr ~ binder case tp @ ClassInfo(pre, cls, cparents, decls, selfInfo) => val preText = toTextLocal(pre) diff --git a/compiler/src/dotty/tools/dotc/printing/Printer.scala b/compiler/src/dotty/tools/dotc/printing/Printer.scala index 550bdb94af4f..b883b6be805b 100644 --- a/compiler/src/dotty/tools/dotc/printing/Printer.scala +++ b/compiler/src/dotty/tools/dotc/printing/Printer.scala @@ -6,7 +6,7 @@ import core._ import Texts._, ast.Trees._ import Types.{Type, SingletonType, LambdaParam}, Symbols.Symbol, Scopes.Scope, Constants.Constant, - Names.Name, Denotations._, Annotations.Annotation + Names.Name, Denotations._, Annotations.Annotation, Contexts.Context import typer.Implicits.SearchResult import util.SourcePosition import typer.ImportInfo @@ -104,6 +104,9 @@ abstract class Printer { /** Textual representation of a prefix of some reference, ending in `.` or `#` */ def toTextPrefix(tp: Type): Text + /** Textual representation of a reference in a capture set */ + def toTextCaptureRef(tp: Type): Text + /** Textual representation of symbol's declaration */ def dclText(sym: Symbol): Text @@ -182,6 +185,9 @@ abstract class Printer { /** A plain printer without any embellishments */ def plain: Printer + + /** The context in which this printer operates */ + def printerContext: Context } object Printer { diff --git a/compiler/src/dotty/tools/dotc/printing/RefinedPrinter.scala b/compiler/src/dotty/tools/dotc/printing/RefinedPrinter.scala index b74e78ba591b..619bfafeb775 100644 --- a/compiler/src/dotty/tools/dotc/printing/RefinedPrinter.scala +++ b/compiler/src/dotty/tools/dotc/printing/RefinedPrinter.scala @@ -15,8 +15,8 @@ import Annotations.Annotation import Denotations._ import SymDenotations._ import StdNames.{nme, tpnme} -import ast.{Trees, untpd} -import typer.{Implicits, Namer} +import ast.{Trees, tpd, untpd} +import typer.{Implicits, Namer, Applications} import typer.ProtoTypes._ import Trees._ import TypeApplications._ @@ -24,19 +24,21 @@ import NameKinds.{WildcardParamName, DefaultGetterName} import util.Chars.isOperatorPart import transform.TypeUtils._ import transform.SymUtils._ +import config.Config import dotty.tools.dotc.util.SourcePosition import dotty.tools.dotc.ast.untpd.{MemberDef, Modifiers, PackageDef, RefTree, Template, TypeDef, ValOrDefDef} +import cc.{CaptureSet, toCaptureSet, IllegalCaptureRef} class RefinedPrinter(_ctx: Context) extends PlainPrinter(_ctx) { /** A stack of enclosing DefDef, TypeDef, or ClassDef, or ModuleDefs nodes */ private var enclosingDef: untpd.Tree = untpd.EmptyTree - private var myCtx: Context = super.curCtx + private var myCtx: Context = super.printerContext private var printPos = ctx.settings.YprintPos.value private val printLines = ctx.settings.printLines.value - override protected def curCtx: Context = myCtx + override def printerContext: Context = myCtx def withEnclosingDef(enclDef: Tree[? >: Untyped])(op: => Text): Text = { val savedCtx = myCtx @@ -134,14 +136,14 @@ class RefinedPrinter(_ctx: Context) extends PlainPrinter(_ctx) { else simpleNameString(tsym) } - private def arrow(isGiven: Boolean): String = - if isGiven then "?=>" else "=>" + private def arrow(isGiven: Boolean, isPure: Boolean): String = + (if isGiven then "?" else "") + (if isPure then "->" else "=>") override def toText(tp: Type): Text = controlled { def toTextTuple(args: List[Type]): Text = "(" ~ argsText(args) ~ ")" - def toTextFunction(args: List[Type], isGiven: Boolean, isErased: Boolean): Text = + def toTextFunction(args: List[Type], isGiven: Boolean, isErased: Boolean, isPure: Boolean): Text = changePrec(GlobalPrec) { val argStr: Text = if args.length == 2 @@ -154,29 +156,26 @@ class RefinedPrinter(_ctx: Context) extends PlainPrinter(_ctx) { ~ keywordText("erased ").provided(isErased) ~ argsText(args.init) ~ ")" - argStr ~ " " ~ arrow(isGiven) ~ " " ~ argText(args.last) + argStr ~ " " ~ arrow(isGiven, isPure) ~ " " ~ argText(args.last) } - def toTextMethodAsFunction(info: Type): Text = info match + def toTextMethodAsFunction(info: Type, isPure: Boolean): Text = info match case info: MethodType => changePrec(GlobalPrec) { "(" ~ keywordText("erased ").provided(info.isErasedMethod) - ~ ( if info.isParamDependent || info.isResultDependent - then paramsText(info) - else argsText(info.paramInfos) - ) + ~ paramsText(info) ~ ") " - ~ arrow(info.isImplicitMethod) + ~ arrow(info.isImplicitMethod, isPure) ~ " " - ~ toTextMethodAsFunction(info.resultType) + ~ toTextMethodAsFunction(info.resultType, isPure) } case info: PolyType => changePrec(GlobalPrec) { "[" ~ paramsText(info) ~ "] => " - ~ toTextMethodAsFunction(info.resultType) + ~ toTextMethodAsFunction(info.resultType, isPure) } case _ => toText(info) @@ -218,9 +217,11 @@ class RefinedPrinter(_ctx: Context) extends PlainPrinter(_ctx) { tp.tupleElementTypes match case Some(types) if types.size >= 2 && !printDebug => toTextTuple(types) case _ => - val cls = tycon.typeSymbol + val tsym = tycon.typeSymbol if tycon.isRepeatedParam then toTextLocal(args.head) ~ "*" - else if defn.isFunctionClass(cls) then toTextFunction(args, cls.name.isContextFunction, cls.name.isErasedFunction) + else if defn.isFunctionSymbol(tsym) then + toTextFunction(args, tsym.name.isContextFunction, tsym.name.isErasedFunction, + isPure = ctx.settings.Ycc.value && !tsym.name.isImpureFunction) else if isInfixType(tp) then val l :: r :: Nil = args: @unchecked val opName = tyconName(tycon) @@ -245,10 +246,9 @@ class RefinedPrinter(_ctx: Context) extends PlainPrinter(_ctx) { if !printDebug && appliedText(tp.asInstanceOf[HKLambda].resType).isEmpty => // don't eta contract if the application would be printed specially toText(tycon) - case tp: RefinedType - if (defn.isFunctionType(tp) || (tp.parent.typeSymbol eq defn.PolyFunctionClass)) - && !printDebug => - toTextMethodAsFunction(tp.refinedInfo) + case tp: RefinedType if defn.isFunctionOrPolyType(tp) && !printDebug => + toTextMethodAsFunction(tp.refinedInfo, + isPure = ctx.settings.Ycc.value && !tp.typeSymbol.name.isImpureFunction) case tp: TypeRef => if (tp.symbol.isAnonymousClass && !showUniqueIds) toText(tp.info) @@ -264,7 +264,7 @@ class RefinedPrinter(_ctx: Context) extends PlainPrinter(_ctx) { case tp: ClassInfo => if tp.cls.derivesFrom(defn.PolyFunctionClass) then tp.member(nme.apply).info match - case info: PolyType => toTextMethodAsFunction(info) + case info: PolyType => toTextMethodAsFunction(info, isPure = false) case _ => toTextParents(tp.parents) ~~ "{...}" else toTextParents(tp.parents) ~~ "{...}" case JavaArrayType(elemtp) => @@ -532,7 +532,7 @@ class RefinedPrinter(_ctx: Context) extends PlainPrinter(_ctx) { changePrec(OrTypePrec) { toText(args(0)) ~ " | " ~ atPrec(OrTypePrec + 1) { toText(args(1)) } } else if (tpt.symbol == defn.andType && args.length == 2) changePrec(AndTypePrec) { toText(args(0)) ~ " & " ~ atPrec(AndTypePrec + 1) { toText(args(1)) } } - else if defn.isFunctionClass(tpt.symbol) + else if defn.isFunctionSymbol(tpt.symbol) && tpt.isInstanceOf[TypeTree] && tree.hasType && !printDebug then changePrec(GlobalPrec) { toText(tree.typeOpt) } else args match @@ -554,7 +554,8 @@ class RefinedPrinter(_ctx: Context) extends PlainPrinter(_ctx) { (" <: " ~ toText(bound) provided !bound.isEmpty) } case ByNameTypeTree(tpt) => - "=> " ~ toTextLocal(tpt) + (if ctx.settings.Ycc.value then "-> " else "=> ") + ~ toTextLocal(tpt) case TypeBoundsTree(lo, hi, alias) => if (lo eq hi) && alias.isEmpty then optText(lo)(" = " ~ _) else optText(lo)(" >: " ~ _) ~ optText(hi)(" <: " ~ _) ~ optText(alias)(" = " ~ _) @@ -607,7 +608,17 @@ class RefinedPrinter(_ctx: Context) extends PlainPrinter(_ctx) { case tree: Template => toTextTemplate(tree) case Annotated(arg, annot) => - toTextLocal(arg) ~~ annotText(annot.symbol.enclosingClass, annot) + def captureSet = + annot.asInstanceOf[tpd.Tree].toCaptureSet + def toTextAnnot = + toTextLocal(arg) ~~ annotText(annot.symbol.enclosingClass, annot) + def toTextRetainsAnnot = + try changePrec(GlobalPrec)(toText(captureSet) ~ " " ~ toText(arg)) + catch case ex: IllegalCaptureRef => toTextAnnot + if annot.symbol.maybeOwner == defn.RetainsAnnot + && ctx.settings.Ycc.value && Config.printCaptureSetsAsPrefix && !printDebug + then toTextRetainsAnnot + else toTextAnnot case EmptyTree => "" case TypedSplice(t) => @@ -650,7 +661,12 @@ class RefinedPrinter(_ctx: Context) extends PlainPrinter(_ctx) { ~ Text(args.map(argToText), ", ") ~ ")" } - argsText ~ " " ~ arrow(isGiven) ~ " " ~ toText(body) + val isPure = + ctx.settings.Ycc.value + && tree.match + case tree: FunctionWithMods => !tree.mods.is(Impure) + case _ => true + argsText ~ " " ~ arrow(isGiven, isPure) ~ " " ~ toText(body) case PolyFunction(targs, body) => val targsText = "[" ~ Text(targs.map((arg: Tree) => toText(arg)), ", ") ~ "]" changePrec(GlobalPrec) { @@ -703,6 +719,12 @@ class RefinedPrinter(_ctx: Context) extends PlainPrinter(_ctx) { val contentText = toTextGlobal(content) val tptText = toTextGlobal(tpt) prefix ~~ idx.toString ~~ "|" ~~ tptText ~~ "|" ~~ argsText ~~ "|" ~~ contentText ~~ postfix + case CapturingTypeTree(refs, parent) => + parent match + case ImpureByNameTypeTree(bntpt) => + "=> " ~ toTextLocal(bntpt) + case _ => + changePrec(GlobalPrec)("{" ~ Text(refs.map(toText), ", ") ~ "} " ~ toText(parent)) case _ => tree.fallbackToText(this) } @@ -789,9 +811,9 @@ class RefinedPrinter(_ctx: Context) extends PlainPrinter(_ctx) { if mdef.hasType then Modifiers(mdef.symbol) else mdef.rawMods private def Modifiers(sym: Symbol): Modifiers = untpd.Modifiers( - sym.flags & (if (sym.isType) ModifierFlags | VarianceFlags else ModifierFlags), + sym.flagsUNSAFE & (if (sym.isType) ModifierFlags | VarianceFlags else ModifierFlags), if (sym.privateWithin.exists) sym.privateWithin.asType.name else tpnme.EMPTY, - sym.annotations.filterNot(ann => dropAnnotForModText(ann.symbol)).map(_.tree)) + sym.annotationsUNSAFE.filterNot(ann => dropAnnotForModText(ann.symbol)).map(_.tree)) protected def dropAnnotForModText(sym: Symbol): Boolean = sym == defn.BodyAnnot @@ -995,13 +1017,13 @@ class RefinedPrinter(_ctx: Context) extends PlainPrinter(_ctx) { else if (suppressKw) PrintableFlags(isType) &~ Private else PrintableFlags(isType) if (homogenizedView && mods.flags.isTypeFlags) flagMask &~= GivenOrImplicit // drop implicit/given from classes - val rawFlags = if (sym.exists) sym.flags else mods.flags + val rawFlags = if (sym.exists) sym.flagsUNSAFE else mods.flags if (rawFlags.is(Param)) flagMask = flagMask &~ Given &~ Erased val flags = rawFlags & flagMask var flagsText = toTextFlags(sym, flags) val annotTexts = if sym.exists then - sym.annotations.filterNot(ann => dropAnnotForModText(ann.symbol)).map(toText) + sym.annotationsUNSAFE.filterNot(ann => dropAnnotForModText(ann.symbol)).map(toText) else mods.annotations.filterNot(tree => dropAnnotForModText(tree.symbol)).map(annotText(NoSymbol, _)) Text(annotTexts, " ") ~~ flagsText ~~ (Str(kw) provided !suppressKw) diff --git a/compiler/src/dotty/tools/dotc/reporting/messages.scala b/compiler/src/dotty/tools/dotc/reporting/messages.scala index 0a01e4ef4911..a3af4c1b2582 100644 --- a/compiler/src/dotty/tools/dotc/reporting/messages.scala +++ b/compiler/src/dotty/tools/dotc/reporting/messages.scala @@ -25,6 +25,7 @@ import ast.Trees._ import ast.untpd import ast.tpd import transform.SymUtils._ +import cc.CaptureSet.IdentityCaptRefMap /** Messages * ======== @@ -250,7 +251,7 @@ import transform.SymUtils._ // the type mismatch on the bounds instead of the original TypeParamRefs, since // these are usually easier to analyze. We exclude F-bounds since these would // lead to a recursive infinite expansion. - object reported extends TypeMap: + object reported extends TypeMap, IdentityCaptRefMap: def setVariance(v: Int) = variance = v val constraint = mapCtx.typerState.constraint var fbounded = false @@ -295,7 +296,6 @@ import transform.SymUtils._ val treeStr = inTree.map(x => s"\nTree: ${x.show}").getOrElse("") treeStr + "\n" + super.explain - end TypeMismatch class NotAMember(site: Type, val name: Name, selected: String, addendum: => String = "")(using Context) @@ -674,7 +674,7 @@ import transform.SymUtils._ } } - class ByNameParameterNotSupported(tpe: untpd.TypTree)(using Context) + class ByNameParameterNotSupported(tpe: untpd.Tree)(using Context) extends SyntaxMsg(ByNameParameterNotSupportedID) { def msg = em"By-name parameter type ${tpe} not allowed here." diff --git a/compiler/src/dotty/tools/dotc/transform/ElimByName.scala b/compiler/src/dotty/tools/dotc/transform/ElimByName.scala index efdd18291821..479a455b4aea 100644 --- a/compiler/src/dotty/tools/dotc/transform/ElimByName.scala +++ b/compiler/src/dotty/tools/dotc/transform/ElimByName.scala @@ -94,6 +94,7 @@ class ElimByName extends MiniPhase, InfoTransformer: sym.is(Method) || exprBecomesFunction(sym) def byNameClosure(arg: Tree, argType: Type)(using Context): Tree = + report.log(i"creating by name closure for $argType") val meth = newAnonFun(ctx.owner, MethodType(Nil, argType), coord = arg.span) Closure(meth, _ => arg.changeOwnerAfter(ctx.owner, meth, thisPhase), @@ -137,12 +138,8 @@ class ElimByName extends MiniPhase, InfoTransformer: if isByNameRef(qual) && (isPureExpr(qual) || qual.symbol.isAllOf(InlineParam)) => qual case _ => - if isByNameRef(arg) || arg.symbol.name.is(SuperArgName) - then arg - else - var argType = arg.tpe.widenIfUnstable - if argType.isBottomType then argType = formalResult - byNameClosure(arg, argType) + if isByNameRef(arg) || arg.symbol.name.is(SuperArgName) then arg + else byNameClosure(arg, formalResult) case _ => arg diff --git a/compiler/src/dotty/tools/dotc/transform/EmptyPhase.scala b/compiler/src/dotty/tools/dotc/transform/EmptyPhase.scala new file mode 100644 index 000000000000..9a287b2dd1d9 --- /dev/null +++ b/compiler/src/dotty/tools/dotc/transform/EmptyPhase.scala @@ -0,0 +1,19 @@ +package dotty.tools.dotc +package transform + +import core.* +import Contexts.Context +import Phases.Phase + +/** A phase that can be inserted directly after a phase that cannot + * be checked, to enable a -Ycheck as soon as possible afterwards + */ +class EmptyPhase extends Phase: + + def phaseName: String = "dummy" + + override def isEnabled(using Context) = prev.isEnabled + + override def run(using Context) = () + +end EmptyPhase \ No newline at end of file diff --git a/compiler/src/dotty/tools/dotc/transform/FirstTransform.scala b/compiler/src/dotty/tools/dotc/transform/FirstTransform.scala index 54483f74163a..6968eb271961 100644 --- a/compiler/src/dotty/tools/dotc/transform/FirstTransform.scala +++ b/compiler/src/dotty/tools/dotc/transform/FirstTransform.scala @@ -101,9 +101,21 @@ class FirstTransform extends MiniPhase with InfoTransformer { thisPhase => reorder(stats, Nil) } - /** eliminate self in Template */ + /** Eliminate self in Template + * Under -Ycc, we keep the self type `S` around in a type definition + * + * private[this] type $this = S + * + * This is so that the type can be checked for well-formedness in the CaptureCheck phase. + */ override def transformTemplate(impl: Template)(using Context): Tree = - cpy.Template(impl)(self = EmptyValDef) + impl.self match + case self: ValDef if !self.tpt.isEmpty && ctx.settings.Ycc.value => + val tsym = newSymbol(ctx.owner, tpnme.SELF, PrivateLocal, TypeAlias(self.tpt.tpe)) + val tdef = untpd.cpy.TypeDef(self)(tpnme.SELF, self.tpt).withType(tsym.typeRef) + cpy.Template(impl)(self = EmptyValDef, body = tdef :: impl.body) + case _ => + cpy.Template(impl)(self = EmptyValDef) override def transformDefDef(ddef: DefDef)(using Context): Tree = val meth = ddef.symbol.asTerm diff --git a/compiler/src/dotty/tools/dotc/transform/OverridingPairs.scala b/compiler/src/dotty/tools/dotc/transform/OverridingPairs.scala index b820ae94be4d..b27a75436d86 100644 --- a/compiler/src/dotty/tools/dotc/transform/OverridingPairs.scala +++ b/compiler/src/dotty/tools/dotc/transform/OverridingPairs.scala @@ -29,7 +29,9 @@ object OverridingPairs: /** Symbols to exclude: Here these are constructors and private locals. * But it may be refined in subclasses. */ - protected def exclude(sym: Symbol): Boolean = !sym.memberCanMatchInheritedSymbols + protected def exclude(sym: Symbol): Boolean = + !sym.memberCanMatchInheritedSymbols + || ctx.phase == Phases.checkCapturesPhase && sym.is(Recheck.ResetPrivate) /** The parents of base that are checked when deciding whether an overriding * pair has already been treated in a parent class. diff --git a/compiler/src/dotty/tools/dotc/transform/PatternMatcher.scala b/compiler/src/dotty/tools/dotc/transform/PatternMatcher.scala index d74045cd2a21..70fa0e5cc513 100644 --- a/compiler/src/dotty/tools/dotc/transform/PatternMatcher.scala +++ b/compiler/src/dotty/tools/dotc/transform/PatternMatcher.scala @@ -932,7 +932,7 @@ object PatternMatcher { } emitWithMashedConditions(plan :: Nil) case LetPlan(sym, body) => - val valDef = ValDef(sym, initializer(sym).ensureConforms(sym.info)).withSpan(sym.span) + val valDef = ValDef(sym, initializer(sym).ensureConforms(sym.info), inferred = true).withSpan(sym.span) seq(valDef :: Nil, emit(body)) case LabeledPlan(label, expr) => Labeled(label, emit(expr)) diff --git a/compiler/src/dotty/tools/dotc/transform/PostTyper.scala b/compiler/src/dotty/tools/dotc/transform/PostTyper.scala index 8b7ac94675a1..3db751df4145 100644 --- a/compiler/src/dotty/tools/dotc/transform/PostTyper.scala +++ b/compiler/src/dotty/tools/dotc/transform/PostTyper.scala @@ -391,13 +391,13 @@ class PostTyper extends MacroTransform with IdentityDenotTransformer { thisPhase for parent <- impl.parents do Checking.checkTraitInheritance(parent.tpe.classSymbol, sym.asClass, parent.srcPos) // Add SourceFile annotation to top-level classes - if sym.owner.is(Package) - && ctx.compilationUnit.source.exists - && sym != defn.SourceFileAnnot - then - val reference = ctx.settings.sourceroot.value - val relativePath = util.SourceFile.relativePath(ctx.compilationUnit.source, reference) - sym.addAnnotation(Annotation.makeSourceFile(relativePath)) + if sym.owner.is(Package) then + if ctx.compilationUnit.source.exists && sym != defn.SourceFileAnnot then + val reference = ctx.settings.sourceroot.value + val relativePath = util.SourceFile.relativePath(ctx.compilationUnit.source, reference) + sym.addAnnotation(Annotation.makeSourceFile(relativePath)) + if ctx.settings.Ycc.value && sym != defn.CaptureCheckedAnnot then + sym.addAnnotation(Annotation(defn.CaptureCheckedAnnot)) else if !sym.is(Param) && !sym.owner.isOneOf(AbstractOrTrait) then Checking.checkGoodBounds(tree.symbol) diff --git a/compiler/src/dotty/tools/dotc/transform/PreRecheck.scala b/compiler/src/dotty/tools/dotc/transform/PreRecheck.scala new file mode 100644 index 000000000000..db9e28d7aad7 --- /dev/null +++ b/compiler/src/dotty/tools/dotc/transform/PreRecheck.scala @@ -0,0 +1,19 @@ +package dotty.tools.dotc +package transform + +import core.Phases.Phase +import core.DenotTransformers.DenotTransformer +import core.Contexts.{Context, ctx} + +/** A base class for a phase that precedes a rechecker and that allows installing + * new types for local symbols. + */ +abstract class PreRecheck extends Phase, DenotTransformer: + + def phaseName: String = "preRecheck" + + override def changesBaseTypes: Boolean = true + + def run(using Context): Unit = () + + override def isCheckable = false diff --git a/compiler/src/dotty/tools/dotc/transform/Recheck.scala b/compiler/src/dotty/tools/dotc/transform/Recheck.scala new file mode 100644 index 000000000000..36044e6bcb91 --- /dev/null +++ b/compiler/src/dotty/tools/dotc/transform/Recheck.scala @@ -0,0 +1,468 @@ +package dotty.tools +package dotc +package transform + +import core.* +import Symbols.*, Contexts.*, Types.*, ContextOps.*, Decorators.*, SymDenotations.* +import Flags.*, SymUtils.*, NameKinds.* +import ast.* +import Names.Name +import Phases.Phase +import DenotTransformers.{DenotTransformer, IdentityDenotTransformer, SymTransformer} +import NamerOps.{methodType, linkConstructorParams} +import NullOpsDecorator.stripNull +import typer.ErrorReporting.err +import typer.ProtoTypes.* +import typer.TypeAssigner.seqLitType +import typer.ConstFold +import NamerOps.methodType +import config.Printers.recheckr +import util.Property +import StdNames.nme +import reporting.trace +import annotation.constructorOnly + +object Recheck: + import tpd.Tree + + /** A flag used to indicate that a ParamAccessor has been temporarily made not-private + * Only used at the start of the Recheck phase, reset at its end. + * The flag repurposes the Scala2ModuleVar flag. No confusion is possible since + * Scala2ModuleVar cannot be also ParamAccessors. + */ + val ResetPrivate = Scala2ModuleVar + val ResetPrivateParamAccessor = ResetPrivate | ParamAccessor + + /** Attachment key for rechecked types of TypeTrees */ + val RecheckedType = Property.Key[Type] + + extension (sym: Symbol) + + /** Update symbol's info to newInfo from prevPhase.next to lastPhase. + * Reset to previous info for phases after lastPhase. + */ + def updateInfoBetween(prevPhase: DenotTransformer, lastPhase: DenotTransformer, newInfo: Type)(using Context): Unit = + if sym.info ne newInfo then + sym.copySymDenotation( + initFlags = + if sym.flags.isAllOf(ResetPrivateParamAccessor) + then sym.flags &~ ResetPrivate | Private + else sym.flags + ).installAfter(lastPhase) // reset + sym.copySymDenotation( + info = newInfo, + initFlags = + if newInfo.isInstanceOf[LazyType] then sym.flags &~ Touched + else sym.flags + ).installAfter(prevPhase) + + /** Does symbol have a new denotation valid from phase.next that is different + * from the denotation it had before? + */ + def isUpdatedAfter(phase: Phase)(using Context) = + val symd = sym.denot + symd.validFor.firstPhaseId == phase.id + 1 && (sym.originDenotation ne symd) + + extension (tree: Tree) + + /** Remember `tpe` as the type of `tree`, which might be different from the + * type stored in the tree itself, unless a type was already remembered for `tree`. + */ + def rememberType(tpe: Type)(using Context): Unit = + if !tree.hasAttachment(RecheckedType) then rememberTypeAlways(tpe) + + /** Remember `tpe` as the type of `tree`, which might be different from the + * type stored in the tree itself + */ + def rememberTypeAlways(tpe: Type)(using Context): Unit = + if tpe ne tree.tpe then tree.putAttachment(RecheckedType, tpe) + + /** The remembered type of the tree, or if none was installed, the original type */ + def knownType = + tree.attachmentOrElse(RecheckedType, tree.tpe) + + def hasRememberedType: Boolean = tree.hasAttachment(RecheckedType) + +/** A base class that runs a simplified typer pass over an already re-typed program. The pass + * does not transform trees but returns instead the re-typed type of each tree as it is + * traversed. The Recheck phase must be directly preceded by a phase of type PreRecheck. + */ +abstract class Recheck extends Phase, SymTransformer: + thisPhase => + + import ast.tpd.* + import Recheck.* + + def preRecheckPhase = this.prev.asInstanceOf[PreRecheck] + + override def changesBaseTypes: Boolean = true + + override def isCheckable = false + // TODO: investigate what goes wrong we Ycheck directly after rechecking. + // One failing test is pos/i583a.scala + + /** Change any `ResetPrivate` flags back to `Private` */ + def transformSym(sym: SymDenotation)(using Context): SymDenotation = + if sym.isAllOf(Recheck.ResetPrivateParamAccessor) then + sym.copySymDenotation(initFlags = sym.flags &~ Recheck.ResetPrivate | Private) + else sym + + def run(using Context): Unit = + newRechecker().checkUnit(ctx.compilationUnit) + + def newRechecker()(using Context): Rechecker + + /** The typechecker pass */ + class Rechecker(@constructorOnly ictx: Context): + private val ta = ictx.typeAssigner + + /** If true, remember types of all tree nodes in attachments so that they + * can be retrieved with `knownType` + */ + private val keepAllTypes = inContext(ictx) { + ictx.settings.Xprint.value.containsPhase(thisPhase) + } + + /** Should type of `tree` be kept in an attachment so that it can be retrieved with + * `knownType`? By default true only is `keepAllTypes` hold, but can be overridden. + */ + def keepType(tree: Tree): Boolean = keepAllTypes + + /** Constant-folded rechecked type `tp` of tree `tree` */ + private def constFold(tree: Tree, tp: Type)(using Context): Type = + val tree1 = tree.withType(tp) + val tree2 = ConstFold(tree1) + if tree2 ne tree1 then tree2.tpe else tp + + def recheckIdent(tree: Ident)(using Context): Type = + tree.tpe + + def recheckSelect(tree: Select)(using Context): Type = + val Select(qual, name) = tree + recheckSelection(tree, recheck(qual).widenIfUnstable, name) + + /** Keep the symbol of the `select` but re-infer its type */ + def recheckSelection(tree: Select, qualType: Type, name: Name)(using Context) = + if name.is(OuterSelectName) then tree.tpe + else + //val pre = ta.maybeSkolemizePrefix(qualType, name) + val mbr = qualType.findMember(name, qualType, + excluded = if tree.symbol.is(Private) then EmptyFlags else Private + ).suchThat(tree.symbol == _) + constFold(tree, qualType.select(name, mbr)) + //.showing(i"recheck select $qualType . $name : ${mbr.info} = $result") + + def recheckBind(tree: Bind, pt: Type)(using Context): Type = tree match + case Bind(name, body) => + recheck(body, pt) + val sym = tree.symbol + if sym.isType then sym.typeRef else sym.info + + def recheckLabeled(tree: Labeled, pt: Type)(using Context): Type = tree match + case Labeled(bind, expr) => + val bindType = recheck(bind, pt) + val exprType = recheck(expr, defn.UnitType) + bindType + + def recheckValDef(tree: ValDef, sym: Symbol)(using Context): Unit = + if !tree.rhs.isEmpty then recheck(tree.rhs, sym.info) + + def recheckDefDef(tree: DefDef, sym: Symbol)(using Context): Unit = + val rhsCtx = linkConstructorParams(sym).withOwner(sym) + if !tree.rhs.isEmpty && !sym.isInlineMethod && !sym.isEffectivelyErased then + inContext(rhsCtx) { recheck(tree.rhs, recheck(tree.tpt)) } + + def recheckTypeDef(tree: TypeDef, sym: Symbol)(using Context): Type = + recheck(tree.rhs) + sym.typeRef + + def recheckClassDef(tree: TypeDef, impl: Template, sym: ClassSymbol)(using Context): Type = + recheck(impl.constr) + impl.parentsOrDerived.foreach(recheck(_)) + recheck(impl.self) + recheckStats(impl.body) + sym.typeRef + + /** Assuming `formals` are parameters of a Java-defined method, remap Object + * to FromJavaObject since it got lost in ElimRepeated + */ + private def mapJavaArgs(formals: List[Type])(using Context): List[Type] = + val tm = new TypeMap: + def apply(t: Type) = t match + case t: TypeRef if t.symbol == defn.ObjectClass => defn.FromJavaObjectType + case _ => mapOver(t) + formals.mapConserve(tm) + + /** Hook for method type instantiation */ + protected def instantiate(mt: MethodType, argTypes: List[Type], sym: Symbol)(using Context): Type = + mt.instantiate(argTypes) + + def recheckApply(tree: Apply, pt: Type)(using Context): Type = + recheck(tree.fun).widen match + case fntpe: MethodType => + assert(fntpe.paramInfos.hasSameLengthAs(tree.args)) + val formals = + if tree.symbol.is(JavaDefined) then mapJavaArgs(fntpe.paramInfos) + else fntpe.paramInfos + def recheckArgs(args: List[Tree], formals: List[Type], prefs: List[ParamRef]): List[Type] = args match + case arg :: args1 => + val argType = recheck(arg, formals.head) + val formals1 = + if fntpe.isParamDependent + then formals.tail.map(_.substParam(prefs.head, argType)) + else formals.tail + argType :: recheckArgs(args1, formals1, prefs.tail) + case Nil => + assert(formals.isEmpty) + Nil + val argTypes = recheckArgs(tree.args, formals, fntpe.paramRefs) + constFold(tree, instantiate(fntpe, argTypes, tree.fun.symbol)) + //.showing(i"typed app $tree : $fntpe with ${tree.args}%, % : $argTypes%, % = $result") + + def recheckTypeApply(tree: TypeApply, pt: Type)(using Context): Type = + recheck(tree.fun).widen match + case fntpe: PolyType => + assert(fntpe.paramInfos.hasSameLengthAs(tree.args)) + val argTypes = tree.args.map(recheck(_)) + constFold(tree, fntpe.instantiate(argTypes)) + + def recheckTyped(tree: Typed)(using Context): Type = + val tptType = recheck(tree.tpt) + recheck(tree.expr, tptType) + tptType + + def recheckAssign(tree: Assign)(using Context): Type = + val lhsType = recheck(tree.lhs) + recheck(tree.rhs, lhsType.widen) + defn.UnitType + + def recheckBlock(stats: List[Tree], expr: Tree, pt: Type)(using Context): Type = + recheckStats(stats) + val exprType = recheck(expr) + // The expected type `pt` is not propagated. Doing so would allow variables in the + // expected type to contain references to local symbols of the block, so the + // local symbols could escape that way. + TypeOps.avoid(exprType, localSyms(stats).filterConserve(_.isTerm)) + + def recheckBlock(tree: Block, pt: Type)(using Context): Type = + recheckBlock(tree.stats, tree.expr, pt) + + def recheckInlined(tree: Inlined, pt: Type)(using Context): Type = + recheckBlock(tree.bindings, tree.expansion, pt) + + def recheckIf(tree: If, pt: Type)(using Context): Type = + recheck(tree.cond, defn.BooleanType) + recheck(tree.thenp, pt) | recheck(tree.elsep, pt) + + def recheckClosure(tree: Closure, pt: Type)(using Context): Type = + if tree.tpt.isEmpty then + tree.meth.tpe.widen.toFunctionType(tree.meth.symbol.is(JavaDefined)) + else + recheck(tree.tpt) + + def recheckMatch(tree: Match, pt: Type)(using Context): Type = + val selectorType = recheck(tree.selector) + val casesTypes = tree.cases.map(recheckCase(_, selectorType.widen, pt)) + TypeComparer.lub(casesTypes) + + def recheckCase(tree: CaseDef, selType: Type, pt: Type)(using Context): Type = + recheck(tree.pat, selType) + recheck(tree.guard, defn.BooleanType) + recheck(tree.body, pt) + + def recheckReturn(tree: Return)(using Context): Type = + // Avoid local pattern defined symbols in returns from matchResult blocks + // that are inserted by the PatternMatcher transform. + // For regular symbols in the case branch, we already avoid them by the rule + // for blocks since a case branch is of the form `return[MatchResultN] { ... }` + // For source-level returns from methods, there's nothing to avoid, since the + // result type of a method with a return must be given explicitly. + def avoidMap = new TypeOps.AvoidMap: + def toAvoid(tp: NamedType) = + tp.symbol.is(Case) && tp.symbol.owner.isContainedIn(ctx.owner) + + val rawType = recheck(tree.expr) + val ownType = avoidMap(rawType) + checkConforms(ownType, tree.from.symbol.returnProto, tree) + defn.NothingType + end recheckReturn + + def recheckWhileDo(tree: WhileDo)(using Context): Type = + recheck(tree.cond, defn.BooleanType) + recheck(tree.body, defn.UnitType) + defn.UnitType + + def recheckTry(tree: Try, pt: Type)(using Context): Type = + val bodyType = recheck(tree.expr, pt) + val casesTypes = tree.cases.map(recheckCase(_, defn.ThrowableType, pt)) + val finalizerType = recheck(tree.finalizer, defn.UnitType) + TypeComparer.lub(bodyType :: casesTypes) + + def recheckSeqLiteral(tree: SeqLiteral, pt: Type)(using Context): Type = + val elemProto = pt.stripNull.elemType match + case NoType => WildcardType + case bounds: TypeBounds => WildcardType(bounds) + case elemtp => elemtp + val declaredElemType = recheck(tree.elemtpt) + val elemTypes = tree.elems.map(recheck(_, elemProto)) + seqLitType(tree, TypeComparer.lub(declaredElemType :: elemTypes)) + + def recheckTypeTree(tree: TypeTree)(using Context): Type = + knownType(tree) // allows to install new types at Setup + + def recheckAnnotated(tree: Annotated)(using Context): Type = + tree.tpe match + case tp: AnnotatedType => + val argType = recheck(tree.arg) + tp.derivedAnnotatedType(argType, tp.annot) + + def recheckAlternative(tree: Alternative, pt: Type)(using Context): Type = + val altTypes = tree.trees.map(recheck(_, pt)) + TypeComparer.lub(altTypes) + + def recheckPackageDef(tree: PackageDef)(using Context): Type = + recheckStats(tree.stats) + NoType + + def recheckStats(stats: List[Tree])(using Context): Unit = + stats.foreach(recheck(_)) + + def recheckDef(tree: ValOrDefDef, sym: Symbol)(using Context): Unit = + inContext(ctx.localContext(tree, sym)) { + tree match + case tree: ValDef => recheckValDef(tree, sym) + case tree: DefDef => recheckDefDef(tree, sym) + } + + /** Recheck tree without adapting it, returning its new type. + * @param tree the original tree + * @param pt the expected result type + */ + def recheckStart(tree: Tree, pt: Type = WildcardType)(using Context): Type = + + def recheckNamed(tree: NameTree, pt: Type)(using Context): Type = + val sym = tree.symbol + tree match + case tree: Ident => recheckIdent(tree) + case tree: Select => recheckSelect(tree) + case tree: Bind => recheckBind(tree, pt) + case tree: ValOrDefDef => + if tree.isEmpty then NoType + else + if sym.isUpdatedAfter(preRecheckPhase) then + sym.ensureCompleted() // in this case the symbol's completer should recheck the right hand side + else + recheckDef(tree, sym) + sym.termRef + case tree: TypeDef => + // TODO: Should we allow for completers as for ValDefs or DefDefs? + tree.rhs match + case impl: Template => + recheckClassDef(tree, impl, sym.asClass)(using ctx.localContext(tree, sym)) + case _ => + recheckTypeDef(tree, sym)(using ctx.localContext(tree, sym)) + case tree: Labeled => recheckLabeled(tree, pt) + + def recheckUnnamed(tree: Tree, pt: Type): Type = tree match + case tree: Apply => recheckApply(tree, pt) + case tree: TypeApply => recheckTypeApply(tree, pt) + case _: New | _: This | _: Super | _: Literal => tree.tpe + case tree: Typed => recheckTyped(tree) + case tree: Assign => recheckAssign(tree) + case tree: Block => recheckBlock(tree, pt) + case tree: If => recheckIf(tree, pt) + case tree: Closure => recheckClosure(tree, pt) + case tree: Match => recheckMatch(tree, pt) + case tree: Return => recheckReturn(tree) + case tree: WhileDo => recheckWhileDo(tree) + case tree: Try => recheckTry(tree, pt) + case tree: SeqLiteral => recheckSeqLiteral(tree, pt) + case tree: Inlined => recheckInlined(tree, pt) + case tree: TypeTree => recheckTypeTree(tree) + case tree: Annotated => recheckAnnotated(tree) + case tree: Alternative => recheckAlternative(tree, pt) + case tree: PackageDef => recheckPackageDef(tree) + case tree: Thicket => defn.NothingType + case tree: Import => defn.NothingType + + tree match + case tree: NameTree => recheckNamed(tree, pt) + case tree => recheckUnnamed(tree, pt) + end recheckStart + + /** Finish rechecking a tree node: check rechecked type against expected type + * and remember rechecked type in a tree attachment if required. + * @param tpe the recheched type of `tree` + * @param tree the rechecked tree + * @param pt the expected type + */ + def recheckFinish(tpe: Type, tree: Tree, pt: Type)(using Context): Type = + checkConforms(tpe, pt, tree) + if keepType(tree) then tree.rememberType(tpe) + tpe + + def recheck(tree: Tree, pt: Type = WildcardType)(using Context): Type = + trace(i"rechecking $tree with pt = $pt", recheckr, show = true) { + try recheckFinish(recheckStart(tree, pt), tree, pt) + catch case ex: Exception => + println(i"error while rechecking $tree") + throw ex + } + + /** If true, print info for some successful checkConforms operations (failing ones give + * an error message in any case). + */ + private val debugSuccesses = false + + /** Check that widened types of `tpe` and `pt` are compatible. */ + def checkConforms(tpe: Type, pt: Type, tree: Tree)(using Context): Unit = tree match + case _: DefTree | EmptyTree | _: TypeTree | _: Closure => + // Don't report closure nodes, since their span is a point; wait instead + // for enclosing block to preduce an error + case _ => + checkConformsExpr(tpe.widenExpr, pt.widenExpr, tree) + + def checkConformsExpr(actual: Type, expected: Type, tree: Tree)(using Context): Unit = + //println(i"check conforms $actual <:< $expected") + val isCompatible = + actual <:< expected + || expected.isRepeatedParam + && actual <:< expected.translateFromRepeated(toArray = tree.tpe.isRef(defn.ArrayClass)) + if !isCompatible then + recheckr.println(i"conforms failed for ${tree}: $actual vs $expected") + err.typeMismatch(tree.withType(actual), expected) + else if debugSuccesses then + tree match + case _: Ident => + println(i"SUCCESS $tree:\n${TypeComparer.explained(_.isSubType(actual, expected))}") + case _ => + + def checkUnit(unit: CompilationUnit)(using Context): Unit = + recheck(unit.tpdTree) + + end Rechecker + + /** Show tree with rechecked types instead of the types stored in the `.tpe` field */ + override def show(tree: untpd.Tree)(using Context): String = + val addRecheckedTypes = new TreeMap: + override def transform(tree: Tree)(using Context): Tree = + val tree1 = super.transform(tree) + tree.getAttachment(RecheckedType) match + case Some(tpe) => tree1.withType(tpe) + case None => tree1 + atPhase(thisPhase) { + super.show(addRecheckedTypes.transform(tree.asInstanceOf[tpd.Tree])) + } +end Recheck + +/** A class that can be used to test basic rechecking without any customaization */ +object TestRecheck: + class Pre extends PreRecheck, IdentityDenotTransformer: + override def isEnabled(using Context) = ctx.settings.YrecheckTest.value + +class TestRecheck extends Recheck: + def phaseName: String = "recheck" + override def isEnabled(using Context) = ctx.settings.YrecheckTest.value + def newRechecker()(using Context): Rechecker = Rechecker(ctx) + + diff --git a/compiler/src/dotty/tools/dotc/transform/TreeChecker.scala b/compiler/src/dotty/tools/dotc/transform/TreeChecker.scala index aa5ccce6ad2e..5791d0c7d119 100644 --- a/compiler/src/dotty/tools/dotc/transform/TreeChecker.scala +++ b/compiler/src/dotty/tools/dotc/transform/TreeChecker.scala @@ -374,14 +374,14 @@ class TreeChecker extends Phase with SymTransformer { val tpe = tree.typeOpt // Polymorphic apply methods stay structural until Erasure - val isPolyFunctionApply = (tree.name eq nme.apply) && (tree.qualifier.typeOpt <:< defn.PolyFunctionType) + val isPolyFunctionApply = (tree.name eq nme.apply) && tree.qualifier.typeOpt.derivesFrom(defn.PolyFunctionClass) // Outer selects are pickled specially so don't require a symbol val isOuterSelect = tree.name.is(OuterSelectName) val isPrimitiveArrayOp = ctx.erasedTypes && nme.isPrimitiveName(tree.name) if !(tree.isType || isPolyFunctionApply || isOuterSelect || isPrimitiveArrayOp) then val denot = tree.denot assert(denot.exists, i"Selection $tree with type $tpe does not have a denotation") - assert(denot.symbol.exists, i"Denotation $denot of selection $tree with type $tpe does not have a symbol") + assert(denot.symbol.exists, i"Denotation $denot of selection $tree with type $tpe does not have a symbol, qualifier type = ${tree.qualifier.typeOpt}") val sym = tree.symbol val symIsFixed = tpe match { diff --git a/compiler/src/dotty/tools/dotc/transform/TryCatchPatterns.scala b/compiler/src/dotty/tools/dotc/transform/TryCatchPatterns.scala index 91f63a033c76..92d22b1cc57e 100644 --- a/compiler/src/dotty/tools/dotc/transform/TryCatchPatterns.scala +++ b/compiler/src/dotty/tools/dotc/transform/TryCatchPatterns.scala @@ -71,7 +71,7 @@ class TryCatchPatterns extends MiniPhase { case _ => isDefaultCase(cdef) } - private def isSimpleThrowable(tp: Type)(using Context): Boolean = tp.stripAnnots match { + private def isSimpleThrowable(tp: Type)(using Context): Boolean = tp.stripped match { case tp @ TypeRef(pre, _) => (pre == NoPrefix || pre.typeSymbol.isStatic) && // Does not require outer class check !tp.symbol.is(Flags.Trait) && // Traits not supported by JVM diff --git a/compiler/src/dotty/tools/dotc/transform/TypeTestsCasts.scala b/compiler/src/dotty/tools/dotc/transform/TypeTestsCasts.scala index ca977a6799f8..fc914e9b03bf 100644 --- a/compiler/src/dotty/tools/dotc/transform/TypeTestsCasts.scala +++ b/compiler/src/dotty/tools/dotc/transform/TypeTestsCasts.scala @@ -214,7 +214,7 @@ object TypeTestsCasts { * can be true in some cases. Issues a warning or an error otherwise. */ def checkSensical(foundClasses: List[Symbol])(using Context): Boolean = - def exprType = i"type ${expr.tpe.widen.stripAnnots}" + def exprType = i"type ${expr.tpe.widen.stripped}" def check(foundCls: Symbol): Boolean = if (!isCheckable(foundCls)) true else if (!foundCls.derivesFrom(testCls)) { diff --git a/compiler/src/dotty/tools/dotc/typer/Applications.scala b/compiler/src/dotty/tools/dotc/typer/Applications.scala index c662d6f00045..b7cabbebee71 100644 --- a/compiler/src/dotty/tools/dotc/typer/Applications.scala +++ b/compiler/src/dotty/tools/dotc/typer/Applications.scala @@ -2125,7 +2125,7 @@ trait Applications extends Compatibility { def isUniform[T](xs: List[T])(p: (T, T) => Boolean) = xs.forall(p(_, xs.head)) val formalsForArg: List[Type] = altFormals.map(_.head) def argTypesOfFormal(formal: Type): List[Type] = - formal match { + formal.dealias match { case defn.FunctionOf(args, result, isImplicit, isErased) => args case defn.PartialFunctionOf(arg, result) => arg :: Nil case _ => Nil diff --git a/compiler/src/dotty/tools/dotc/typer/Checking.scala b/compiler/src/dotty/tools/dotc/typer/Checking.scala index 7629776525a0..a89fa91dc6a6 100644 --- a/compiler/src/dotty/tools/dotc/typer/Checking.scala +++ b/compiler/src/dotty/tools/dotc/typer/Checking.scala @@ -74,9 +74,8 @@ object Checking { } for (arg, which, bound) <- TypeOps.boundsViolations(args, boundss, instantiate, app) do report.error( - showInferred(DoesNotConformToBound(arg.tpe, which, bound), - app, tpt), - arg.srcPos.focus) + showInferred(DoesNotConformToBound(arg.tpe, which, bound), app, tpt), + arg.srcPos.focus) /** Check that type arguments `args` conform to corresponding bounds in `tl` * Note: This does not check the bounds of AppliedTypeTrees. These @@ -147,7 +146,7 @@ object Checking { tp match case AppliedType(tycon, argTypes) => checkAppliedType( - untpd.AppliedTypeTree(TypeTree(tycon), argTypes.map(TypeTree)) + untpd.AppliedTypeTree(TypeTree(tycon), argTypes.map(TypeTree(_))) .withType(tp).withSpan(tpt.span.toSynthetic), tpt) case _ => @@ -325,6 +324,7 @@ object Checking { case AndType(tp1, tp2) => isInteresting(tp1) || isInteresting(tp2) case OrType(tp1, tp2) => isInteresting(tp1) && isInteresting(tp2) case _: RefinedOrRecType | _: AppliedType => true + case tp: AnnotatedType => isInteresting(tp.parent) case _ => false } @@ -599,6 +599,7 @@ object Checking { def checkNoPrivateLeaks(sym: Symbol)(using Context): Type = { class NotPrivate extends TypeMap { var errors: List[() => String] = Nil + private var inCaptureSet: Boolean = false def accessBoundary(sym: Symbol): Symbol = if (sym.is(Private) || !sym.owner.isClass) sym.owner @@ -612,12 +613,16 @@ object Checking { * @pre The signature of `sym` refers to `other` */ def isLeaked(other: Symbol) = - other.is(Private, butNot = TypeParam) && { + other.is(Private, butNot = TypeParam) + && { val otherBoundary = other.owner val otherLinkedBoundary = otherBoundary.linkedClass !(symBoundary.isContainedIn(otherBoundary) || otherLinkedBoundary.exists && symBoundary.isContainedIn(otherLinkedBoundary)) } + && !(inCaptureSet && other.isAllOf(LocalParamAccessor)) + // class parameters in capture sets are not treated as leaked since in + // phase CheckCaptures these are treated as normal vals. def apply(tp: Type): Type = tp match { case tp: NamedType => @@ -652,6 +657,14 @@ object Checking { declaredParents = tp.declaredParents.map(p => transformedParent(apply(p))) ) + case tp @ AnnotatedType(underlying, annot) + if annot.symbol == defn.RetainsAnnot || annot.symbol == defn.RetainsByNameAnnot => + val underlying1 = this(underlying) + val saved = inCaptureSet + inCaptureSet = true + val annot1 = annot.mapWith(this) + inCaptureSet = saved + derivedAnnotatedType(tp, underlying1, annot1) case _ => mapOver(tp) } @@ -1422,9 +1435,15 @@ trait Checking { val kind = if pattern then "pattern selector" else "value" report.warning(MatchableWarning(tp, pattern), pos) - def checkCanThrow(tp: Type, span: Span)(using Context): Unit = + /** Check that there is an implicit capability to throw a checked exception + * if the saferExceptions feature is turned on. Return that capability is it exists, + * EmptyTree otherwise. + */ + def checkCanThrow(tp: Type, span: Span)(using Context): Tree = if Feature.enabled(Feature.saferExceptions) && tp.isCheckedException then ctx.typer.implicitArgTree(defn.CanThrowClass.typeRef.appliedTo(tp), span) + else + EmptyTree /** Check that catch can generate a good CanThrow exception */ def checkCatch(pat: Tree, guard: Tree)(using Context): Unit = pat match @@ -1474,7 +1493,7 @@ trait ReChecking extends Checking { override def checkAnnotApplicable(annot: Tree, sym: Symbol)(using Context): Boolean = true override def checkMatchable(tp: Type, pos: SrcPos, pattern: Boolean)(using Context): Unit = () override def checkNoModuleClash(sym: Symbol)(using Context) = () - override def checkCanThrow(tp: Type, span: Span)(using Context): Unit = () + override def checkCanThrow(tp: Type, span: Span)(using Context): Tree = EmptyTree override def checkCatch(pat: Tree, guard: Tree)(using Context): Unit = () override def checkFeature(name: TermName, description: => String, featureUseSite: Symbol, pos: SrcPos)(using Context): Unit = () } diff --git a/compiler/src/dotty/tools/dotc/typer/ErrorReporting.scala b/compiler/src/dotty/tools/dotc/typer/ErrorReporting.scala index 215fe9bfa687..3c92a217206d 100644 --- a/compiler/src/dotty/tools/dotc/typer/ErrorReporting.scala +++ b/compiler/src/dotty/tools/dotc/typer/ErrorReporting.scala @@ -124,13 +124,27 @@ object ErrorReporting { def typeMismatch(tree: Tree, pt: Type, implicitFailure: SearchFailureType = NoMatchingImplicits): Tree = { val normTp = normalize(tree.tpe, pt) - val treeTp = if (normTp <:< pt) tree.tpe else normTp - // use normalized type if that also shows an error, original type otherwise + val normPt = normalize(pt, pt) + + def contextFunctionCount(tp: Type): Int = tp.stripped match + case defn.ContextFunctionType(_, restp, _) => 1 + contextFunctionCount(restp) + case _ => 0 + def strippedTpCount = contextFunctionCount(tree.tpe) - contextFunctionCount(normTp) + def strippedPtCount = contextFunctionCount(pt) - contextFunctionCount(normPt) + + val (treeTp, expectedTp) = + if normTp <:< normPt || strippedTpCount != strippedPtCount + then (tree.tpe, pt) + else (normTp, normPt) + // use normalized types if that also shows an error, and both sides stripped + // the same number of context functions. Use original types otherwise. + def missingElse = tree match case If(_, _, elsep @ Literal(Constant(()))) if elsep.span.isSynthetic => "\nMaybe you are missing an else part for the conditional?" case _ => "" - errorTree(tree, TypeMismatch(treeTp, pt, Some(tree), implicitFailure.whyNoConversion, missingElse)) + + errorTree(tree, TypeMismatch(treeTp, expectedTp, Some(tree), implicitFailure.whyNoConversion, missingElse)) } /** A subtype log explaining why `found` does not conform to `expected` */ diff --git a/compiler/src/dotty/tools/dotc/typer/Implicits.scala b/compiler/src/dotty/tools/dotc/typer/Implicits.scala index e8e49071bf2b..ac86702ac588 100644 --- a/compiler/src/dotty/tools/dotc/typer/Implicits.scala +++ b/compiler/src/dotty/tools/dotc/typer/Implicits.scala @@ -836,7 +836,7 @@ trait Implicits: def isOldStyleFunctionConversion(tpe: Type): Boolean = tpe match { case PolyType(_, resType) => isOldStyleFunctionConversion(resType) - case _ => tpe.derivesFrom(defn.FunctionClass(1)) && !tpe.derivesFrom(defn.ConversionClass) && !tpe.derivesFrom(defn.SubTypeClass) + case _ => tpe.derivesFrom(defn.FunctionSymbol(1)) && !tpe.derivesFrom(defn.ConversionClass) && !tpe.derivesFrom(defn.SubTypeClass) } try diff --git a/compiler/src/dotty/tools/dotc/typer/ImportSuggestions.scala b/compiler/src/dotty/tools/dotc/typer/ImportSuggestions.scala index 88e8568dfcb9..a9b53f0783bd 100644 --- a/compiler/src/dotty/tools/dotc/typer/ImportSuggestions.scala +++ b/compiler/src/dotty/tools/dotc/typer/ImportSuggestions.scala @@ -319,6 +319,8 @@ trait ImportSuggestions: * If there's nothing to suggest, an empty string is returned. */ override def importSuggestionAddendum(pt: Type)(using Context): String = + if ctx.phase == Phases.checkCapturesPhase then + return "" // it's too late then to look for implicits val (fullMatches, headMatches) = importSuggestions(pt)(using ctx.fresh.setExploreTyperState()) implicits.println(i"suggestions for $pt in ${ctx.owner} = ($fullMatches%, %, $headMatches%, %)") diff --git a/compiler/src/dotty/tools/dotc/typer/Inferencing.scala b/compiler/src/dotty/tools/dotc/typer/Inferencing.scala index 1d075902c816..27b83e025cf9 100644 --- a/compiler/src/dotty/tools/dotc/typer/Inferencing.scala +++ b/compiler/src/dotty/tools/dotc/typer/Inferencing.scala @@ -12,6 +12,7 @@ import util.{Stats, SimpleIdentityMap, SrcPos} import Decorators._ import config.Printers.{gadts, typr} import annotation.tailrec +import reporting._ import collection.mutable import scala.annotation.internal.sharable @@ -126,8 +127,8 @@ object Inferencing { couldInstantiateTypeVar(parent, applied) case tp: AndOrType => couldInstantiateTypeVar(tp.tp1, applied) || couldInstantiateTypeVar(tp.tp2, applied) - case AnnotatedType(tp, _) => - couldInstantiateTypeVar(tp, applied) + case tp: AnnotatedType => + couldInstantiateTypeVar(tp.parent, applied) case _ => false @@ -524,10 +525,10 @@ object Inferencing { } /** Replace every top-level occurrence of a wildcard type argument by - * a fresh skolem type. The skolem types are of the form $i.CAP, where - * $i is a skolem of type `scala.internal.TypeBox`, and `CAP` is its - * type member. See the documentation of `TypeBox` for a rationale why we do this. - */ + * a fresh skolem type. The skolem types are of the form $i.CAP, where + * $i is a skolem of type `scala.internal.TypeBox`, and `CAP` is its + * type member. See the documentation of `TypeBox` for a rationale why we do this. + */ def captureWildcards(tp: Type)(using Context): Type = derivedOnDealias(tp) { case tp @ AppliedType(tycon, args) if tp.hasWildcardArg => val tparams = tycon.typeParamSymbols @@ -747,4 +748,3 @@ trait Inferencing { this: Typer => enum IfBottom: case ok, fail, flip - diff --git a/compiler/src/dotty/tools/dotc/typer/RefChecks.scala b/compiler/src/dotty/tools/dotc/typer/RefChecks.scala index b416b028efe8..94eacca5c7db 100644 --- a/compiler/src/dotty/tools/dotc/typer/RefChecks.scala +++ b/compiler/src/dotty/tools/dotc/typer/RefChecks.scala @@ -95,7 +95,7 @@ object RefChecks { * and required classes. Also check that only `enum` constructs extend * `java.lang.Enum` and no user-written class extends ContextFunctionN. */ - private def checkParents(cls: Symbol, parentTrees: List[Tree])(using Context): Unit = cls.info match { + def checkParents(cls: Symbol, parentTrees: List[Tree])(using Context): Unit = cls.info match { case cinfo: ClassInfo => def checkSelfConforms(other: ClassSymbol, category: String, relation: String) = { val otherSelf = other.declaredSelfTypeAsSeenFrom(cls.thisType) @@ -106,8 +106,9 @@ object RefChecks { val psyms = cls.asClass.parentSyms for (psym <- psyms) checkSelfConforms(psym.asClass, "illegal inheritance", "parent") - for (reqd <- cinfo.cls.givenSelfType.classSymbols) - checkSelfConforms(reqd, "missing requirement", "required") + for reqd <- cinfo.cls.givenSelfType.classSymbols do + if reqd != cls then + checkSelfConforms(reqd, "missing requirement", "required") def isClassExtendingJavaEnum = !cls.isOneOf(Enum | Trait) && psyms.contains(defn.JavaEnumClass) @@ -272,7 +273,7 @@ object RefChecks { * TODO This still needs to be cleaned up; the current version is a straight port of what was there * before, but it looks too complicated and method bodies are far too large. */ - private def checkAllOverrides(clazz: ClassSymbol)(using Context): Unit = { + def checkAllOverrides(clazz: ClassSymbol)(using Context): Unit = { val self = clazz.thisType val upwardsSelf = upwardsThisType(clazz) var hasErrors = false diff --git a/compiler/src/dotty/tools/dotc/typer/TypeAssigner.scala b/compiler/src/dotty/tools/dotc/typer/TypeAssigner.scala index 24794011bf5b..c8b01b3407b7 100644 --- a/compiler/src/dotty/tools/dotc/typer/TypeAssigner.scala +++ b/compiler/src/dotty/tools/dotc/typer/TypeAssigner.scala @@ -10,10 +10,12 @@ import util.SrcPos import NameOps._ import collection.mutable import reporting._ -import Checking.checkNoPrivateLeaks +import Checking.{checkNoPrivateLeaks, checkNoWildcard} +import cc.CaptureSet trait TypeAssigner { - import tpd._ + import tpd.* + import TypeAssigner.* /** The qualifying class of a this or super with prefix `qual` (which might be empty). * @param packageOk The qualifier may refer to a package. @@ -186,6 +188,14 @@ trait TypeAssigner { if tpe.isError then tpe else errorType(ex"$whatCanNot be accessed as a member of $pre$where.$whyNot", pos) + def processAppliedType(tree: untpd.Tree, tp: Type)(using Context): Type = tp match + case AppliedType(tycon, args) => + val constr = tycon.typeSymbol + if constr == defn.andType then AndType(args(0), args(1)) + else if constr == defn.orType then OrType(args(0), args(1), soft = false) + else tp + case _ => tp + /** Type assignment method. Each method takes as parameters * - an untpd.Tree to which it assigns a type, * - typed child trees it needs to access to cpmpute that type, @@ -281,6 +291,7 @@ trait TypeAssigner { def safeSubstMethodParams(mt: MethodType, argTypes: List[Type])(using Context): Type = if mt.isResultDependent then safeSubstParams(mt.resultType, mt.paramRefs, argTypes) + else if mt.isCaptureDependent then mt.resultType.substParams(mt, argTypes) else mt.resultType def assignType(tree: untpd.Apply, fn: Tree, args: List[Tree])(using Context): Apply = { @@ -353,7 +364,7 @@ trait TypeAssigner { // reference to a polytype would have to be a fresh copy of that type, // but we want to avoid that because it would increase compilation cost. // See pos/i6682a.scala for a test case where the defensive copying matters. - val ensureFresh = new TypeMap: + val ensureFresh = new TypeMap with CaptureSet.IdempotentCaptRefMap: def apply(tp: Type) = mapOver( if tp eq pt then pt.newLikeThis(pt.paramNames, pt.paramInfos, pt.resType) else tp) @@ -444,13 +455,8 @@ trait TypeAssigner { if (cases.isEmpty) tree.withType(expr.tpe) else tree.withType(TypeComparer.lub(expr.tpe :: cases.tpes)) - def assignType(tree: untpd.SeqLiteral, elems: List[Tree], elemtpt: Tree)(using Context): SeqLiteral = { - val ownType = tree match { - case tree: untpd.JavaSeqLiteral => defn.ArrayOf(elemtpt.tpe) - case _ => if (ctx.erasedTypes) defn.SeqType else defn.SeqType.appliedTo(elemtpt.tpe) - } - tree.withType(ownType) - } + def assignType(tree: untpd.SeqLiteral, elems: List[Tree], elemtpt: Tree)(using Context): SeqLiteral = + tree.withType(seqLitType(tree, elemtpt.tpe)) def assignType(tree: untpd.SingletonTypeTree, ref: Tree)(using Context): SingletonTypeTree = tree.withType(ref.tpe) @@ -474,11 +480,10 @@ trait TypeAssigner { assert(!hasNamedArg(args) || ctx.reporter.errorsReported, tree) val tparams = tycon.tpe.typeParams val ownType = - if (tparams.hasSameLengthAs(args)) - if (tycon.symbol == defn.andType) AndType(args(0).tpe, args(1).tpe) - else if (tycon.symbol == defn.orType) OrType(args(0).tpe, args(1).tpe, soft = false) - else tycon.tpe.appliedTo(args.tpes) - else wrongNumberOfTypeArgs(tycon.tpe, tparams, args, tree.srcPos) + if tparams.hasSameLengthAs(args) then + processAppliedType(tree, tycon.tpe.appliedTo(args.tpes)) + else + wrongNumberOfTypeArgs(tycon.tpe, tparams, args, tree.srcPos) tree.withType(ownType) } @@ -545,5 +550,9 @@ trait TypeAssigner { } +object TypeAssigner extends TypeAssigner: + def seqLitType(tree: untpd.SeqLiteral, elemType: Type)(using Context) = tree match + case tree: untpd.JavaSeqLiteral => defn.ArrayOf(elemType) + case _ => if ctx.erasedTypes then defn.SeqType else defn.SeqType.appliedTo(elemType) + -object TypeAssigner extends TypeAssigner diff --git a/compiler/src/dotty/tools/dotc/typer/Typer.scala b/compiler/src/dotty/tools/dotc/typer/Typer.scala index ef0c3a49a02c..36e600faefc6 100644 --- a/compiler/src/dotty/tools/dotc/typer/Typer.scala +++ b/compiler/src/dotty/tools/dotc/typer/Typer.scala @@ -49,6 +49,8 @@ import transform.TypeUtils._ import reporting._ import Nullables._ import NullOpsDecorator._ +import cc.CheckCaptures +import config.Config import scala.annotation.constructorOnly @@ -1200,7 +1202,7 @@ class Typer(@constructorOnly nestingLevel: Int = 0) extends Namer case _ => mapOver(t) } - val pt1 = pt.stripTypeVar.dealias.normalized + val pt1 = pt.strippedDealias.normalized if (pt1 ne pt1.dropDependentRefinement) && defn.isContextFunctionType(pt1.nonPrivateMember(nme.apply).info.finalResultType) then @@ -1282,7 +1284,8 @@ class Typer(@constructorOnly nestingLevel: Int = 0) extends Namer val numArgs = args.length val isContextual = funFlags.is(Given) val isErased = funFlags.is(Erased) - val funCls = defn.FunctionClass(numArgs, isContextual, isErased) + val isImpure = funFlags.is(Impure) + val funSym = defn.FunctionSymbol(numArgs, isContextual, isErased, isImpure) /** If `app` is a function type with arguments that are all erased classes, * turn it into an erased function type. @@ -1292,7 +1295,7 @@ class Typer(@constructorOnly nestingLevel: Int = 0) extends Namer if !isErased && numArgs > 0 && args.indexWhere(!_.tpe.isErasedClass) == numArgs => - val tycon1 = TypeTree(defn.FunctionClass(numArgs, isContextual, isErased = true).typeRef) + val tycon1 = TypeTree(defn.FunctionSymbol(numArgs, isContextual, true, isImpure).typeRef) .withSpan(tycon.span) assignType(cpy.AppliedTypeTree(app)(tycon1, args), tycon1, args) case _ => @@ -1319,7 +1322,7 @@ class Typer(@constructorOnly nestingLevel: Int = 0) extends Namer report.error(i"$mt is an illegal function type because it has inter-parameter dependencies", tree.srcPos) val resTpt = TypeTree(mt.nonDependentResultApprox).withSpan(body.span) val typeArgs = appDef.termParamss.head.map(_.tpt) :+ resTpt - val tycon = TypeTree(funCls.typeRef) + val tycon = TypeTree(funSym.typeRef) val core = propagateErased(AppliedTypeTree(tycon, typeArgs)) RefinedTypeTree(core, List(appDef), ctx.owner.asClass) end typedDependent @@ -1330,7 +1333,7 @@ class Typer(@constructorOnly nestingLevel: Int = 0) extends Namer using ctx.fresh.setOwner(newRefinedClassSymbol(tree.span)).setNewScope) case _ => propagateErased( - typed(cpy.AppliedTypeTree(tree)(untpd.TypeTree(funCls.typeRef), args :+ body), pt)) + typed(cpy.AppliedTypeTree(tree)(untpd.TypeTree(funSym.typeRef), args :+ body), pt)) } } @@ -1867,11 +1870,18 @@ class Typer(@constructorOnly nestingLevel: Int = 0) extends Namer desugar.makeTryCase(handler1) :: Nil typedTry(untpd.Try(tree.expr, cases, tree.finalizer).withSpan(tree.span), pt) - def typedThrow(tree: untpd.Throw)(using Context): Tree = { + def typedThrow(tree: untpd.Throw)(using Context): Tree = val expr1 = typed(tree.expr, defn.ThrowableType) - checkCanThrow(expr1.tpe.widen, tree.span) - Throw(expr1).withSpan(tree.span) - } + val cap = checkCanThrow(expr1.tpe.widen, tree.span) + val res = Throw(expr1).withSpan(tree.span) + if ctx.settings.Ycc.value && !cap.isEmpty && !ctx.isAfterTyper then + // Record access to the CanThrow capabulity recovered in `cap` by wrapping + // the type of the `throw` (i.e. Nothing) in a `@requiresCapability` annotatoon. + Typed(res, + TypeTree( + AnnotatedType(res.tpe, + Annotation(defn.RequiresCapabilityAnnot, cap)))) + else res def typedSeqLiteral(tree: untpd.SeqLiteral, pt: Type)(using Context): SeqLiteral = { val elemProto = pt.stripNull.elemType match { @@ -2332,15 +2342,7 @@ class Typer(@constructorOnly nestingLevel: Int = 0) extends Namer // constructors are an exception as we don't allow constraining type params of classes rhsCtx.gadt.addToConstraint(tparamSyms) else if !sym.isPrimaryConstructor then - // otherwise, for secondary constructors we need a context that "knows" - // that their type parameters are aliases of the class type parameters. - // See pos/i941.scala - rhsCtx.gadt.addToConstraint(tparamSyms) - tparamSyms.lazyZip(sym.owner.typeParams).foreach { (psym, tparam) => - val tr = tparam.typeRef - rhsCtx.gadt.addBound(psym, tr, isUpper = false) - rhsCtx.gadt.addBound(psym, tr, isUpper = true) - } + linkConstructorParams(sym, tparamSyms, rhsCtx) if sym.isInlineMethod then rhsCtx.addMode(Mode.InlineableBody) if sym.is(ExtensionMethod) then rhsCtx.addMode(Mode.InExtensionMethod) @@ -2397,7 +2399,7 @@ class Typer(@constructorOnly nestingLevel: Int = 0) extends Namer //todo: make sure dependent method types do not depend on implicits or by-name params } - /** (1) Check that the signature of the class mamber does not return a repeated parameter type + /** (1) Check that the signature of the class member does not return a repeated parameter type * (2) If info is an erased class, set erased flag of member */ private def postProcessInfo(sym: Symbol)(using Context): Unit = @@ -2700,6 +2702,9 @@ class Typer(@constructorOnly nestingLevel: Int = 0) extends Namer registerNowarn(annot1, tree) val arg1 = typed(tree.arg, pt) if (ctx.mode is Mode.Type) { + val cls = annot1.symbol.maybeOwner + if cls == defn.RetainsAnnot || cls == defn.RetainsByNameAnnot then + CheckCaptures.checkWellformed(annot1) if arg1.isType then assignType(cpy.Annotated(tree)(arg1, annot1), arg1, annot1) else @@ -4057,7 +4062,7 @@ class Typer(@constructorOnly nestingLevel: Int = 0) extends Namer else if pt.isInstanceOf[PolyProto] then tree else var typeArgs = tree match - case Select(qual, nme.CONSTRUCTOR) => qual.tpe.widenDealias.argTypesLo.map(TypeTree) + case Select(qual, nme.CONSTRUCTOR) => qual.tpe.widenDealias.argTypesLo.map(TypeTree(_)) case _ => Nil if typeArgs.isEmpty then typeArgs = constrained(poly, tree)._2 convertNewGenericArray(readapt(tree.appliedToTypeTrees(typeArgs))) diff --git a/compiler/src/dotty/tools/dotc/util/SimpleIdentitySet.scala b/compiler/src/dotty/tools/dotc/util/SimpleIdentitySet.scala index ed49699b633a..dd766dc99c7e 100644 --- a/compiler/src/dotty/tools/dotc/util/SimpleIdentitySet.scala +++ b/compiler/src/dotty/tools/dotc/util/SimpleIdentitySet.scala @@ -17,6 +17,7 @@ abstract class SimpleIdentitySet[+Elem <: AnyRef] { def map[B <: AnyRef](f: Elem => B): SimpleIdentitySet[B] def /: [A, E >: Elem <: AnyRef](z: A)(f: (A, E) => A): A def toList: List[Elem] + def iterator: Iterator[Elem] final def isEmpty: Boolean = size == 0 @@ -37,6 +38,10 @@ abstract class SimpleIdentitySet[+Elem <: AnyRef] { ((SimpleIdentitySet.empty: SimpleIdentitySet[E]) /: this) { (s, x) => if (that.contains(x)) s else s + x } + + def == [E >: Elem <: AnyRef](that: SimpleIdentitySet[E]): Boolean = + this.size == that.size && forall(that.contains) + override def toString: String = toList.mkString("{", ", ", "}") } @@ -61,6 +66,7 @@ object SimpleIdentitySet { def map[B <: AnyRef](f: Nothing => B): SimpleIdentitySet[B] = empty def /: [A, E <: AnyRef](z: A)(f: (A, E) => A): A = z def toList = Nil + def iterator = Iterator.empty } private class Set1[+Elem <: AnyRef](x0: AnyRef) extends SimpleIdentitySet[Elem] { @@ -78,6 +84,7 @@ object SimpleIdentitySet { def /: [A, E >: Elem <: AnyRef](z: A)(f: (A, E) => A): A = f(z, x0.asInstanceOf[E]) def toList = x0.asInstanceOf[Elem] :: Nil + def iterator = Iterator.single(x0.asInstanceOf[Elem]) } private class Set2[+Elem <: AnyRef](x0: AnyRef, x1: AnyRef) extends SimpleIdentitySet[Elem] { @@ -97,6 +104,10 @@ object SimpleIdentitySet { def /: [A, E >: Elem <: AnyRef](z: A)(f: (A, E) => A): A = f(f(z, x0.asInstanceOf[E]), x1.asInstanceOf[E]) def toList = x0.asInstanceOf[Elem] :: x1.asInstanceOf[Elem] :: Nil + def iterator = Iterator.tabulate(2) { + case 0 => x0.asInstanceOf[Elem] + case 1 => x1.asInstanceOf[Elem] + } } private class Set3[+Elem <: AnyRef](x0: AnyRef, x1: AnyRef, x2: AnyRef) extends SimpleIdentitySet[Elem] { @@ -127,6 +138,11 @@ object SimpleIdentitySet { def /: [A, E >: Elem <: AnyRef](z: A)(f: (A, E) => A): A = f(f(f(z, x0.asInstanceOf[E]), x1.asInstanceOf[E]), x2.asInstanceOf[E]) def toList = x0.asInstanceOf[Elem] :: x1.asInstanceOf[Elem] :: x2.asInstanceOf[Elem] :: Nil + def iterator = Iterator.tabulate(3) { + case 0 => x0.asInstanceOf[Elem] + case 1 => x1.asInstanceOf[Elem] + case 2 => x2.asInstanceOf[Elem] + } } private class SetN[+Elem <: AnyRef](val xs: Array[AnyRef]) extends SimpleIdentitySet[Elem] { @@ -175,6 +191,7 @@ object SimpleIdentitySet { foreach(buf += _) buf.toList } + def iterator = xs.iterator.asInstanceOf[Iterator[Elem]] override def ++ [E >: Elem <: AnyRef](that: SimpleIdentitySet[E]): SimpleIdentitySet[E] = that match { case that: SetN[?] => diff --git a/compiler/src/scala/quoted/runtime/impl/QuotesImpl.scala b/compiler/src/scala/quoted/runtime/impl/QuotesImpl.scala index 30f4dc29aeea..f8e439baeb0e 100644 --- a/compiler/src/scala/quoted/runtime/impl/QuotesImpl.scala +++ b/compiler/src/scala/quoted/runtime/impl/QuotesImpl.scala @@ -2729,7 +2729,7 @@ class QuotesImpl private (using val ctx: Context) extends Quotes, QuoteUnpickler def SomeModule: Symbol = dotc.core.Symbols.defn.SomeClass.companionModule def ProductClass: Symbol = dotc.core.Symbols.defn.ProductClass def FunctionClass(arity: Int, isImplicit: Boolean = false, isErased: Boolean = false): Symbol = - dotc.core.Symbols.defn.FunctionClass(arity, isImplicit, isErased) + dotc.core.Symbols.defn.FunctionSymbol(arity, isImplicit, isErased) def TupleClass(arity: Int): Symbol = dotc.core.Symbols.defn.TupleType(arity).nn.classSymbol.asClass def isTupleClass(sym: Symbol): Boolean = diff --git a/compiler/test/dotc/pos-test-recheck.excludes b/compiler/test/dotc/pos-test-recheck.excludes new file mode 100644 index 000000000000..b8b75f50adc5 --- /dev/null +++ b/compiler/test/dotc/pos-test-recheck.excludes @@ -0,0 +1,20 @@ +# Cannot compensate dealiasing due to false result dependency +i6635a.scala +i6682a.scala + +# unknown reason +patmatch-in-catch.scala +t0095.scala +i8344-1.scala +i8900-promote.scala +i6199c.scala +gadt-TailCalls.scala +i13349.scala +bynamefuns.scala +i15174.scala +gadt-cast-singleton.scala +6709.scala +11463.scala +i11247.scala +i6199b.scala +t3440.scala diff --git a/compiler/test/dotc/run-test-recheck.excludes b/compiler/test/dotc/run-test-recheck.excludes new file mode 100644 index 000000000000..de2fb3f8aed8 --- /dev/null +++ b/compiler/test/dotc/run-test-recheck.excludes @@ -0,0 +1,13 @@ +# unknown reason +i10930.scala +t7584b.scala +opaque-inline +fragables-extension.scala +t7899.scala +t7181.scala +option-extract.scala +i5976.scala +tagless.scala +safeThrowsStrawman2.scala +t7584.scala +function-arity.scala diff --git a/compiler/test/dotty/tools/TestSources.scala b/compiler/test/dotty/tools/TestSources.scala index b4be973147e3..c4d36b16c90b 100644 --- a/compiler/test/dotty/tools/TestSources.scala +++ b/compiler/test/dotty/tools/TestSources.scala @@ -13,17 +13,21 @@ object TestSources { def posFromTastyBlacklistFile: String = "compiler/test/dotc/pos-from-tasty.blacklist" def posTestPicklingBlacklistFile: String = "compiler/test/dotc/pos-test-pickling.blacklist" + def posTestRecheckExcludesFile = "compiler/test/dotc/pos-test-recheck.excludes" def posFromTastyBlacklisted: List[String] = loadList(posFromTastyBlacklistFile) def posTestPicklingBlacklisted: List[String] = loadList(posTestPicklingBlacklistFile) + def posTestRecheckExcluded = loadList(posTestRecheckExcludesFile) // run tests lists def runFromTastyBlacklistFile: String = "compiler/test/dotc/run-from-tasty.blacklist" def runTestPicklingBlacklistFile: String = "compiler/test/dotc/run-test-pickling.blacklist" + def runTestRecheckExcludesFile = "compiler/test/dotc/run-test-recheck.excludes" def runFromTastyBlacklisted: List[String] = loadList(runFromTastyBlacklistFile) def runTestPicklingBlacklisted: List[String] = loadList(runTestPicklingBlacklistFile) + def runTestRecheckExcluded = loadList(runTestRecheckExcludesFile) // load lists diff --git a/compiler/test/dotty/tools/dotc/CompilationTests.scala b/compiler/test/dotty/tools/dotc/CompilationTests.scala index 1abb1ffd9ea3..8d7a16dad8a4 100644 --- a/compiler/test/dotty/tools/dotc/CompilationTests.scala +++ b/compiler/test/dotty/tools/dotc/CompilationTests.scala @@ -40,6 +40,7 @@ class CompilationTests { compileFilesInDir("tests/pos-special/isInstanceOf", allowDeepSubtypes.and("-Xfatal-warnings")), compileFilesInDir("tests/new", defaultOptions.and("-source", "3.2")), // just to see whether 3.2 works compileFilesInDir("tests/pos-scala2", scala2CompatMode), + compileFilesInDir("tests/pos-custom-args/captures", defaultOptions.and("-Ycc")), compileFilesInDir("tests/pos-custom-args/erased", defaultOptions.and("-language:experimental.erasedDefinitions")), compileFilesInDir("tests/pos", defaultOptions.and("-Ysafe-init")), compileFilesInDir("tests/pos-deep-subtype", allowDeepSubtypes), @@ -139,6 +140,7 @@ class CompilationTests { compileFilesInDir("tests/neg-custom-args/allow-double-bindings", allowDoubleBindings), compileFilesInDir("tests/neg-custom-args/allow-deep-subtypes", allowDeepSubtypes), compileFilesInDir("tests/neg-custom-args/no-experimental", defaultOptions.and("-Yno-experimental")), + compileFilesInDir("tests/neg-custom-args/captures", defaultOptions.and("-Ycc")), compileDir("tests/neg-custom-args/impl-conv", defaultOptions.and("-Xfatal-warnings", "-feature")), compileDir("tests/neg-custom-args/i13946", defaultOptions.and("-Xfatal-warnings", "-feature")), compileFile("tests/neg-custom-args/avoid-warn-deprecation.scala", defaultOptions.and("-Xfatal-warnings", "-feature")), @@ -183,6 +185,7 @@ class CompilationTests { compileFile("tests/neg-custom-args/deptypes.scala", defaultOptions.and("-language:experimental.dependent")), compileFile("tests/neg-custom-args/matchable.scala", defaultOptions.and("-Xfatal-warnings", "-source", "future")), compileFile("tests/neg-custom-args/i7314.scala", defaultOptions.and("-Xfatal-warnings", "-source", "future")), + compileFile("tests/neg-custom-args/capt-wf.scala", defaultOptions.and("-Ycc", "-Xfatal-warnings")), compileFile("tests/neg-custom-args/feature-shadowing.scala", defaultOptions.and("-Xfatal-warnings", "-feature")), compileDir("tests/neg-custom-args/hidden-type-errors", defaultOptions.and("-explain")), compileFile("tests/neg-custom-args/i13026.scala", defaultOptions.and("-print-lines")), @@ -210,6 +213,7 @@ class CompilationTests { compileFilesInDir("tests/run-custom-args/fatal-warnings", defaultOptions.and("-Xfatal-warnings")), compileDir("tests/run-custom-args/Xmacro-settings/simple", defaultOptions.and("-Xmacro-settings:one,two,three")), compileDir("tests/run-custom-args/Xmacro-settings/compileTimeEnv", defaultOptions.and("-Xmacro-settings:a,b=1,c.b.a=x.y.z=1,myLogger.level=INFO")), + compileFilesInDir("tests/run-custom-args/captures", allowDeepSubtypes.and("-Ycc")), compileFilesInDir("tests/run-deep-subtype", allowDeepSubtypes), compileFilesInDir("tests/run", defaultOptions.and("-Ysafe-init")) ).checkRuns() @@ -233,6 +237,15 @@ class CompilationTests { ).checkCompile() } + @Test def recheck: Unit = + given TestGroup = TestGroup("recheck") + aggregateTests( + compileFilesInDir("tests/new", recheckOptions), + compileFilesInDir("tests/run", recheckOptions, FileFilter.exclude(TestSources.runTestRecheckExcluded)) + //Disabled to save some time. + //compileFilesInDir("tests/pos", recheckOptions, FileFilter.exclude(TestSources.posTestRecheckExcluded)), + ).checkCompile() + // Explicit nulls tests @Test def explicitNullsNeg: Unit = { implicit val testGroup: TestGroup = TestGroup("explicitNullsNeg") diff --git a/compiler/test/dotty/tools/vulpix/TestConfiguration.scala b/compiler/test/dotty/tools/vulpix/TestConfiguration.scala index 1f53add86667..3ea364cc3a68 100644 --- a/compiler/test/dotty/tools/vulpix/TestConfiguration.scala +++ b/compiler/test/dotty/tools/vulpix/TestConfiguration.scala @@ -83,6 +83,7 @@ object TestConfiguration { ) val picklingWithCompilerOptions = picklingOptions.withClasspath(withCompilerClasspath).withRunClasspath(withCompilerClasspath) + val recheckOptions = defaultOptions.and("-Yrecheck-test") val scala2CompatMode = defaultOptions.and("-source", "3.0-migration") val explicitUTF8 = defaultOptions and ("-encoding", "UTF8") val explicitUTF16 = defaultOptions and ("-encoding", "UTF16") diff --git a/docs/_docs/reference/experimental/cc.md b/docs/_docs/reference/experimental/cc.md index 592d410a4502..878bc0a64ed6 100644 --- a/docs/_docs/reference/experimental/cc.md +++ b/docs/_docs/reference/experimental/cc.md @@ -3,9 +3,8 @@ layout: doc-page title: "Capture Checking" --- -Capture checking is a research project that modifies the Scala type system to track references to capabilities in values. It is currently -implemented in an experimental branch [cc-experiment](https://github.com/lampepfl/dotty/tree/cc-experiment) in the dotty -repo and can be enabled on this branch with a `-Ycc` compiler option. +Capture checking is a research project that modifies the Scala type system to track references to capabilities in values. It can be enabled with a `-Ycc` compiler option. +At present, capture checking is still highly experimental and unstable. To get an idea what capture checking can do, let's start with a small example: ```scala @@ -368,7 +367,7 @@ again on access, the capture information "pops out" again. For instance, even th () => p.fst : {ct} () -> {ct} Int -> String ``` In other words, references to capabilities "tunnel through" in generic instantiations from creation to access; they do not affect the capture set of the enclosing generic data constructor applications. -This principle may seem surprising at first, but it is the key to make capture checking concise and practical. +This principle plays an important part in making capture checking concise and practical. ## Escape Checking @@ -398,7 +397,7 @@ This error message was produced by the following logic: - The `f` parameter has type `{*} FileOutputStream`, which makes it a capability. - Therefore, the type of the expression `() => f.write(0)` is `{f} () -> Unit`. - - This makes the whole type of the closure passed to `usingLogFile` the dependent function type + - This makes the type of the whole closure passed to `usingLogFile` the dependent function type `(f: {*} FileOutputStream) -> {f} () -> Unit`. - The expected type of the closure is a simple, parametric, impure function type `({*} FileOutputStream) => T`, for some instantiation of the type variable `T`. diff --git a/docs/_docs/reference/experimental/overview.md b/docs/_docs/reference/experimental/overview.md index 0f2aa543ebbb..62109837290b 100644 --- a/docs/_docs/reference/experimental/overview.md +++ b/docs/_docs/reference/experimental/overview.md @@ -26,4 +26,4 @@ They can be imported at the top-level if all top-level definitions are `@experim Some experimental language features that are still in research and development can be enabled with special compiler options. These include * [`-Yexplicit-nulls`](./explicit-nulls.md). Enable support for tracking null references in the type system. - +* [`-Ycc`](./cc.md). Enable support for capture checking. diff --git a/library/src-bootstrapped/scala/annotation/retains.scala b/library/src-bootstrapped/scala/annotation/retains.scala new file mode 100644 index 000000000000..0d0099de75fb --- /dev/null +++ b/library/src-bootstrapped/scala/annotation/retains.scala @@ -0,0 +1,15 @@ +package scala.annotation + +/** An annotation that indicates capture of a set of references under -Ycc. + * + * T @retains(x, y, z) + * + * is the internal representation used for the capturing type + * + * {x, y, z} T + * + * The annotation can also be written explicitly if one wants to avoid the + * non-standard capturing type syntax. + */ +@experimental class retains(xs: Any*) extends annotation.StaticAnnotation + diff --git a/library/src-bootstrapped/scala/annotation/retainsByName.scala b/library/src-bootstrapped/scala/annotation/retainsByName.scala new file mode 100644 index 000000000000..421e0400c4e1 --- /dev/null +++ b/library/src-bootstrapped/scala/annotation/retainsByName.scala @@ -0,0 +1,6 @@ +package scala.annotation + +/** An annotation that indicates capture of an enclosing by-name type + */ +@experimental class retainsByName(xs: Any*) extends annotation.StaticAnnotation + diff --git a/library/src-bootstrapped/scala/internal/requiresCapability.scala b/library/src-bootstrapped/scala/internal/requiresCapability.scala new file mode 100644 index 000000000000..d376ba565211 --- /dev/null +++ b/library/src-bootstrapped/scala/internal/requiresCapability.scala @@ -0,0 +1,8 @@ +package scala.annotation.internal + +import annotation.{StaticAnnotation, experimental} + +/** An annotation to record a required capaility in the type of a throws + */ +@experimental class requiresCapability(capability: Any) extends StaticAnnotation + diff --git a/library/src/scala/CanThrow.scala b/library/src/scala/CanThrow.scala index fcfd11fc9197..c7f23a393715 100644 --- a/library/src/scala/CanThrow.scala +++ b/library/src/scala/CanThrow.scala @@ -1,12 +1,12 @@ package scala import language.experimental.erasedDefinitions -import annotation.{implicitNotFound, experimental} +import annotation.{implicitNotFound, experimental, capability} /** A capability class that allows to throw exception `E`. When used with the * experimental.saferExceptions feature, a `throw Ex()` expression will require * a given of class `CanThrow[Ex]` to be available. */ -@experimental +@experimental @capability @implicitNotFound("The capability to throw exception ${E} is missing.\nThe capability can be provided by one of the following:\n - Adding a using clause `(using CanThrow[${E}])` to the definition of the enclosing method\n - Adding `throws ${E}` clause after the result type of the enclosing method\n - Wrapping this piece of code with a `try` block that catches ${E}") erased class CanThrow[-E <: Exception] diff --git a/library/src/scala/annotation/capability.scala b/library/src/scala/annotation/capability.scala new file mode 100644 index 000000000000..4696ed6a015e --- /dev/null +++ b/library/src/scala/annotation/capability.scala @@ -0,0 +1,14 @@ +package scala.annotation +import annotation.experimental + +/** Marks an annotated class as a capability. + * If the annotation is present and -Ycc is set, any (possibly aliased + * or refined) instance of the class type is implicitly augmented with + * the universal capture set. Example + * + * @capability class CanThrow[T] + * + * THere, the capture set of any instance of `CanThrow` is assumed to be + * `{*}`. + */ +@experimental final class capability extends StaticAnnotation diff --git a/library/src/scala/annotation/constructorOnly.scala b/library/src/scala/annotation/constructorOnly.scala index 580354e468a3..c78c16534183 100644 --- a/library/src/scala/annotation/constructorOnly.scala +++ b/library/src/scala/annotation/constructorOnly.scala @@ -16,5 +16,8 @@ import scala.annotation.meta._ * compile errors. The annotation is particularly useful for implicit * parameters since for these a textual scan is not sufficient to know * where they are used. + * Note: the annotation is copied from constructor parameters to corresponding + * class fields. But it is checked that the field is eliminated before code + * is generated. */ -@param class constructorOnly extends scala.annotation.StaticAnnotation +@param @field class constructorOnly extends scala.annotation.StaticAnnotation diff --git a/library/src/scala/annotation/internal/CaptureChecked.scala b/library/src/scala/annotation/internal/CaptureChecked.scala new file mode 100644 index 000000000000..8392189f11f7 --- /dev/null +++ b/library/src/scala/annotation/internal/CaptureChecked.scala @@ -0,0 +1,9 @@ +package scala.annotation +package internal +import annotation.experimental + +/** A marker annotation on a toplevel class that indicates + * that the class was checked under -Ycc + */ +@experimental class CaptureChecked extends StaticAnnotation + diff --git a/tests/disabled/neg-custom-args/captures/capt-wf.scala b/tests/disabled/neg-custom-args/captures/capt-wf.scala new file mode 100644 index 000000000000..54fe545f443b --- /dev/null +++ b/tests/disabled/neg-custom-args/captures/capt-wf.scala @@ -0,0 +1,19 @@ +// No longer valid +class C +type Cap = C @retains(*) +type Top = Any @retains(*) + +type T = (x: Cap) => List[String @retains(x)] => Unit // error +val x: (x: Cap) => Array[String @retains(x)] = ??? // error +val y = x + +def test: Unit = + def f(x: Cap) = // ok + val g = (xs: List[String @retains(x)]) => () + g + def f2(x: Cap)(xs: List[String @retains(x)]) = () + val x = f // error + val x2 = f2 // error + val y = f(C()) // ok + val y2 = f2(C()) // ok + () diff --git a/tests/disabled/neg-custom-args/captures/try2.check b/tests/disabled/neg-custom-args/captures/try2.check new file mode 100644 index 000000000000..c7b20d0f7c5e --- /dev/null +++ b/tests/disabled/neg-custom-args/captures/try2.check @@ -0,0 +1,38 @@ +-- [E007] Type Mismatch Error: tests/neg-custom-args/captures/try2.scala:31:32 ----------------------------------------- +31 | (x: CanThrow[Exception]) => () => raise(new Exception)(using x) // error + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + | Found: {x} () => Nothing + | Required: () => Nothing + +longer explanation available when compiling with `-explain` +-- [E007] Type Mismatch Error: tests/neg-custom-args/captures/try2.scala:45:2 ------------------------------------------ +45 | yy // error + | ^^ + | Found: (yy : List[(xx : (() => Int) retains canThrow)]) + | Required: List[() => Int] + +longer explanation available when compiling with `-explain` +-- [E007] Type Mismatch Error: tests/neg-custom-args/captures/try2.scala:52:2 ------------------------------------------ +47 |val global = handle { +48 | (x: CanThrow[Exception]) => +49 | () => +50 | raise(new Exception)(using x) +51 | 22 +52 |} { // error + | ^ + | Found: (() => Int) retains canThrow + | Required: () => Int +53 | (ex: Exception) => () => 22 +54 |} + +longer explanation available when compiling with `-explain` +-- Error: tests/neg-custom-args/captures/try2.scala:24:28 -------------------------------------------------------------- +24 | val a = handle[Exception, CanThrow[Exception]] { // error + | ^^^^^^^^^^^^^^^^^^^ + | type argument is not allowed to capture the global capability (canThrow : *) +-- Error: tests/neg-custom-args/captures/try2.scala:36:11 -------------------------------------------------------------- +36 | val xx = handle { // error + | ^^^^^^ + |inferred type argument ((() => Int) retains canThrow) is not allowed to capture the global capability (canThrow : *) + | + |The inferred arguments are: [Exception, ((() => Int) retains canThrow)] diff --git a/tests/disabled/neg-custom-args/captures/try2.scala b/tests/disabled/neg-custom-args/captures/try2.scala new file mode 100644 index 000000000000..dd3cc890a197 --- /dev/null +++ b/tests/disabled/neg-custom-args/captures/try2.scala @@ -0,0 +1,55 @@ +// Retains syntax for classes not (yet?) supported +import language.experimental.erasedDefinitions +import annotation.ability + +@ability erased val canThrow: * = ??? + +class CanThrow[E <: Exception] extends Retains[canThrow.type] +type Top = Any @retains(*) + +infix type throws[R, E <: Exception] = (erased CanThrow[E]) ?=> R + +class Fail extends Exception + +def raise[E <: Exception](e: E): Nothing throws E = throw e + +def foo(x: Boolean): Int throws Fail = + if x then 1 else raise(Fail()) + +def handle[E <: Exception, R <: Top](op: CanThrow[E] => R)(handler: E => R): R = + val x: CanThrow[E] = ??? + try op(x) + catch case ex: E => handler(ex) + +def test: List[() => Int] = + val a = handle[Exception, CanThrow[Exception]] { // error + (x: CanThrow[Exception]) => x + }{ + (ex: Exception) => ??? + } + + val b = handle[Exception, () => Nothing] { + (x: CanThrow[Exception]) => () => raise(new Exception)(using x) // error + } { + (ex: Exception) => ??? + } + + val xx = handle { // error + (x: CanThrow[Exception]) => + () => + raise(new Exception)(using x) + 22 + } { + (ex: Exception) => () => 22 + } + val yy = xx :: Nil + yy // error + +val global = handle { + (x: CanThrow[Exception]) => + () => + raise(new Exception)(using x) + 22 +} { // error + (ex: Exception) => () => 22 +} diff --git a/tests/disabled/pos/lazylist.scala b/tests/disabled/pos/lazylist.scala new file mode 100644 index 000000000000..c24f8677b91f --- /dev/null +++ b/tests/disabled/pos/lazylist.scala @@ -0,0 +1,50 @@ +package lazylists + +abstract class LazyList[+T]: + this: {*} LazyList[T] => + + def isEmpty: Boolean + def head: T + def tail: LazyList[T] + + def map[U](f: {*} T => U): {f, this} LazyList[U] = + if isEmpty then LazyNil + else LazyCons(f(head), () => tail.map(f)) + + def concat[U >: T](that: {*} LazyList[U]): {this, that} LazyList[U] + +// def flatMap[U](f: {*} T => LazyList[U]): {f, this} LazyList[U] + +class LazyCons[+T](val x: T, val xs: {*} () => {*} LazyList[T]) extends LazyList[T]: + def isEmpty = false + def head = x + def tail: {*} LazyList[T] = xs() + def concat[U >: T](that: {*} LazyList[U]): {this, that} LazyList[U] = + LazyCons(x, () => xs().concat(that)) +// def flatMap[U](f: {*} T => LazyList[U]): {f, this} LazyList[U] = +// f(x).concat(xs().flatMap(f)) + +object LazyNil extends LazyList[Nothing]: + def isEmpty = true + def head = ??? + def tail = ??? + def concat[U](that: {*} LazyList[U]): {that} LazyList[U] = that +// def flatMap[U](f: {*} Nothing => LazyList[U]): LazyList[U] = LazyNil + +def map[A, B](xs: {*} LazyList[A], f: {*} A => B): {f, xs} LazyList[B] = + xs.map(f) + +@annotation.capability class Cap + +def test(cap1: Cap, cap2: Cap, cap3: Cap) = + def f[T](x: LazyList[T]): LazyList[T] = if cap1 == cap1 then x else LazyNil + def g(x: Int) = if cap2 == cap2 then x else 0 + def h(x: Int) = if cap3 == cap3 then x else 0 + val ref1 = LazyCons(1, () => f(LazyNil)) + val ref1c: {cap1} LazyList[Int] = ref1 + val ref2 = map(ref1, g) + val ref2c: {cap2, ref1} LazyList[Int] = ref2 + val ref3 = ref1.map(g) + val ref3c: {cap2, ref1} LazyList[Int] = ref3 + val ref4 = (if cap1 == cap2 then ref1 else ref2).map(h) + val ref4c: {cap1, cap2, cap3} LazyList[Int] = ref4 \ No newline at end of file diff --git a/tests/neg/i9325.scala b/tests/neg-custom-args/allow-deep-subtypes/i9325.scala similarity index 100% rename from tests/neg/i9325.scala rename to tests/neg-custom-args/allow-deep-subtypes/i9325.scala diff --git a/tests/neg-custom-args/capt-wf.scala b/tests/neg-custom-args/capt-wf.scala new file mode 100644 index 000000000000..3bd80e0d0f68 --- /dev/null +++ b/tests/neg-custom-args/capt-wf.scala @@ -0,0 +1,35 @@ +class C +type Cap = {*} C + +object foo + +def test(c: Cap, other: String): Unit = + val x1: {*} C = ??? // OK + val x2: {other} C = ??? // error: cs is empty + val s1 = () => "abc" + val x3: {s1} C = ??? // error: cs is empty + val x3a: () -> String = s1 + val s2 = () => if x1 == null then "" else "abc" + val x4: {s2} C = ??? // OK + val x5: {c, c} C = ??? // error: redundant + val x6: {c} {c} C = ??? // error: redundant + val x7: {c} Cap = ??? // error: redundant + val x8: {*} {c} C = ??? // OK + val x9: {c, *} C = ??? // error: redundant + val x10: {*, c} C = ??? // error: redundant + + def even(n: Int): Boolean = if n == 0 then true else odd(n - 1) + def odd(n: Int): Boolean = if n == 1 then true else even(n - 1) + val e1 = even + val o1 = odd + + val y1: {e1} String = ??? // error cs is empty + val y2: {o1} String = ??? // error cs is empty + + lazy val ev: (Int -> Boolean) = (n: Int) => + lazy val od: (Int -> Boolean) = (n: Int) => + if n == 1 then true else ev(n - 1) + if n == 0 then true else od(n - 1) + val y3: {ev} String = ??? // error cs is empty + + () \ No newline at end of file diff --git a/tests/neg-custom-args/captures/byname.check b/tests/neg-custom-args/captures/byname.check new file mode 100644 index 000000000000..486f94d599ac --- /dev/null +++ b/tests/neg-custom-args/captures/byname.check @@ -0,0 +1,20 @@ +-- Warning: tests/neg-custom-args/captures/byname.scala:17:18 ---------------------------------------------------------- +17 | def h(x: {cap1} -> I) = x // warning + | ^ + | Style: by-name `->` should immediately follow closing `}` of capture set + | to avoid confusion with function type. + | That is, `{c}-> T` instead of `{c} -> T`. +-- [E007] Type Mismatch Error: tests/neg-custom-args/captures/byname.scala:10:6 ---------------------------------------- +10 | h(f2()) // error + | ^^^^ + | Found: {cap1} (x$0: Int) -> Int + | Required: {cap2} Int -> Int + | + | longer explanation available when compiling with `-explain` +-- [E007] Type Mismatch Error: tests/neg-custom-args/captures/byname.scala:19:5 ---------------------------------------- +19 | h(g()) // error + | ^^^ + | Found: {cap2} () ?-> I + | Required: {cap1} () ?-> I + | + | longer explanation available when compiling with `-explain` diff --git a/tests/neg-custom-args/captures/byname.scala b/tests/neg-custom-args/captures/byname.scala new file mode 100644 index 000000000000..1838647f2899 --- /dev/null +++ b/tests/neg-custom-args/captures/byname.scala @@ -0,0 +1,22 @@ +@annotation.capability class Cap + +def test(cap1: Cap, cap2: Cap) = + def f() = if cap1 == cap1 then g else g + def g(x: Int) = if cap2 == cap2 then 1 else x + def g2(x: Int) = if cap1 == cap1 then 1 else x + def f2() = if cap1 == cap1 then g2 else g2 + def h(ff: => {cap2} Int -> Int) = ff + h(f()) // ok + h(f2()) // error + +class I + +def test2(cap1: Cap, cap2: Cap): {cap1} I = + def f() = if cap1 == cap1 then I() else I() + def g() = if cap2 == cap2 then I() else I() + def h(x: {cap1} -> I) = x // warning + h(f()) // OK + h(g()) // error + + + diff --git a/tests/neg-custom-args/captures/capt-box-env.scala b/tests/neg-custom-args/captures/capt-box-env.scala new file mode 100644 index 000000000000..605b446d5262 --- /dev/null +++ b/tests/neg-custom-args/captures/capt-box-env.scala @@ -0,0 +1,11 @@ +@annotation.capability class Cap + +class Pair[+A, +B](x: A, y: B): + def fst: A = x + def snd: B = y + +def test(c: Cap) = + def f(x: Cap): Unit = if c == x then () + val p = Pair(f, f) + val g = () => p.fst == p.snd + val gc: () -> Boolean = g // error diff --git a/tests/neg-custom-args/captures/capt-box.scala b/tests/neg-custom-args/captures/capt-box.scala new file mode 100644 index 000000000000..634470704fc5 --- /dev/null +++ b/tests/neg-custom-args/captures/capt-box.scala @@ -0,0 +1,11 @@ +@annotation.capability class Cap + +def test(x: Cap) = + + def foo(y: Cap) = if x == y then println() + + val x1 = foo + + val x2 = identity(x1) + + val x3: Cap -> Unit = x2 // error \ No newline at end of file diff --git a/tests/neg-custom-args/captures/capt-depfun.scala b/tests/neg-custom-args/captures/capt-depfun.scala new file mode 100644 index 000000000000..14f08f569725 --- /dev/null +++ b/tests/neg-custom-args/captures/capt-depfun.scala @@ -0,0 +1,8 @@ +import annotation.retains +class C +type Cap = C @retains(*) + +def f(y: Cap, z: Cap) = + def g(): C @retains(y, z) = ??? + val ac: ((x: Cap) => String @retains(x) => String @retains(x)) = ??? + val dc: (({y, z} String) => {y, z} String) = ac(g()) // error diff --git a/tests/neg-custom-args/captures/capt-depfun2.scala b/tests/neg-custom-args/captures/capt-depfun2.scala new file mode 100644 index 000000000000..62c2381e01ad --- /dev/null +++ b/tests/neg-custom-args/captures/capt-depfun2.scala @@ -0,0 +1,11 @@ +import annotation.retains +class C +type Cap = C @retains(*) + +def f(y: Cap, z: Cap) = + def g(): C @retains(y, z) = ??? + val ac: ((x: Cap) => Array[String @retains(x)]) = ??? + val dc = ac(g()) // error: Needs explicit type Array[? >: String <: {y, z} String] + // This is a shortcoming of rechecking since the originally inferred + // type is `Array[String]` and the actual type after rechecking + // cannot be expressed as `Array[C String]` for any capture set C \ No newline at end of file diff --git a/tests/neg-custom-args/captures/capt-env.scala b/tests/neg-custom-args/captures/capt-env.scala new file mode 100644 index 000000000000..52fa4abfdaa8 --- /dev/null +++ b/tests/neg-custom-args/captures/capt-env.scala @@ -0,0 +1,13 @@ +class C +type Cap = {*} C + +class Pair[+A, +B](x: A, y: B): + def fst: A = x + def snd: B = y + +def test(c: Cap) = + def f(x: Cap): Unit = if c == x then () + val p = Pair(f, f) + val g = () => p.fst == p.snd + val gc: () -> Boolean = g // error + diff --git a/tests/neg-custom-args/captures/capt-test.scala b/tests/neg-custom-args/captures/capt-test.scala new file mode 100644 index 000000000000..7080d6da67c6 --- /dev/null +++ b/tests/neg-custom-args/captures/capt-test.scala @@ -0,0 +1,27 @@ +import annotation.retains +import language.experimental.erasedDefinitions + +class CT[E <: Exception] +type CanThrow[E <: Exception] = CT[E] @retains(*) +type Top = Any @retains(*) + +infix type throws[R, E <: Exception] = (erased CanThrow[E]) ?=> R + +class Fail extends Exception + +def raise[E <: Exception](e: E): Nothing throws E = throw e + +def foo(x: Boolean): Int throws Fail = + if x then 1 else raise(Fail()) + +def handle[E <: Exception, R <: Top](op: (CanThrow[E]) => R)(handler: E => R): R = + val x: CanThrow[E] = ??? + try op(x) + catch case ex: E => handler(ex) + +def test: Unit = + val b = handle[Exception, () => Nothing] { + (x: CanThrow[Exception]) => () => raise(new Exception)(using x) + } { // error + (ex: Exception) => ??? + } diff --git a/tests/neg-custom-args/captures/capt-wf-typer.scala b/tests/neg-custom-args/captures/capt-wf-typer.scala new file mode 100644 index 000000000000..4fc50caed1f7 --- /dev/null +++ b/tests/neg-custom-args/captures/capt-wf-typer.scala @@ -0,0 +1,11 @@ +import annotation.retains +class C +type Cap = {*} C + +object foo + +def test(c: Cap, other: String): Unit = + val x7: {c} String = ??? // OK + val x8: String @retains(x7 + x7) = ??? // error + val x9: String @retains(foo) = ??? // error + () \ No newline at end of file diff --git a/tests/neg-custom-args/captures/capt1.check b/tests/neg-custom-args/captures/capt1.check new file mode 100644 index 000000000000..4a9e3999f731 --- /dev/null +++ b/tests/neg-custom-args/captures/capt1.check @@ -0,0 +1,53 @@ +-- [E007] Type Mismatch Error: tests/neg-custom-args/captures/capt1.scala:4:2 ------------------------------------------ +4 | () => if x == null then y else y // error + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + | Found: {x} () -> ? C + | Required: () -> C + | + | longer explanation available when compiling with `-explain` +-- [E007] Type Mismatch Error: tests/neg-custom-args/captures/capt1.scala:7:2 ------------------------------------------ +7 | () => if x == null then y else y // error + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + | Found: {x} () -> ? C + | Required: Matchable + | + | longer explanation available when compiling with `-explain` +-- [E007] Type Mismatch Error: tests/neg-custom-args/captures/capt1.scala:14:2 ----------------------------------------- +14 | def f(y: Int) = if x == null then y else y // error + | ^ + | Found: {x} Int -> Int + | Required: Matchable +15 | f + | + | longer explanation available when compiling with `-explain` +-- [E007] Type Mismatch Error: tests/neg-custom-args/captures/capt1.scala:21:2 ----------------------------------------- +21 | class F(y: Int) extends A: // error + | ^ + | Found: {x} A + | Required: A +22 | def m() = if x == null then y else y +23 | F(22) + | + | longer explanation available when compiling with `-explain` +-- [E007] Type Mismatch Error: tests/neg-custom-args/captures/capt1.scala:26:2 ----------------------------------------- +26 | new A: // error + | ^ + | Found: {x} A + | Required: A +27 | def m() = if x == null then y else y + | + | longer explanation available when compiling with `-explain` +-- [E007] Type Mismatch Error: tests/neg-custom-args/captures/capt1.scala:32:24 ---------------------------------------- +32 | val z2 = h[() -> Cap](() => x) // error + | ^^^^^^^ + | Found: {x} () -> {*} C + | Required: () -> box {*} C + | + | longer explanation available when compiling with `-explain` +-- [E007] Type Mismatch Error: tests/neg-custom-args/captures/capt1.scala:33:5 ----------------------------------------- +33 | (() => C()) // error + | ^^^^^^^^^ + | Found: ? () -> {*} C + | Required: () -> box {*} C + | + | longer explanation available when compiling with `-explain` diff --git a/tests/neg-custom-args/captures/capt1.scala b/tests/neg-custom-args/captures/capt1.scala new file mode 100644 index 000000000000..ce69e77057e0 --- /dev/null +++ b/tests/neg-custom-args/captures/capt1.scala @@ -0,0 +1,36 @@ +import annotation.retains +class C +def f(x: C @retains(*), y: C): () -> C = + () => if x == null then y else y // error + +def g(x: C @retains(*), y: C): Matchable = + () => if x == null then y else y // error + +def h1(x: C @retains(*), y: C): Any = + def f() = if x == null then y else y + () => f() // ok + +def h2(x: C @retains(*)): Matchable = + def f(y: Int) = if x == null then y else y // error + f + +class A +type Cap = C @retains(*) + +def h3(x: Cap): A = + class F(y: Int) extends A: // error + def m() = if x == null then y else y + F(22) + +def h4(x: Cap, y: Int): A = + new A: // error + def m() = if x == null then y else y + +def foo() = + val x: C @retains(*) = ??? + def h[X](a: X)(b: X) = a + val z2 = h[() -> Cap](() => x) // error + (() => C()) // error + val z3 = h[(() -> Cap) @retains(x)](() => x)(() => C()) // ok + val z4 = h[(() -> Cap) @retains(x)](() => x)(() => C()) // what was inferred for z3 + diff --git a/tests/neg-custom-args/captures/capt2.scala b/tests/neg-custom-args/captures/capt2.scala new file mode 100644 index 000000000000..8b08832dfdb9 --- /dev/null +++ b/tests/neg-custom-args/captures/capt2.scala @@ -0,0 +1,9 @@ +//import scala.retains +class C +type Cap = {*} C + +def f1(c: Cap): (() -> {c} C) = () => c // error, but would be OK under capture abbreciations for funciton types +def f2(c: Cap): ({c} () -> C) = () => c // error + +def h5(x: Cap): () -> C = + f1(x) // error diff --git a/tests/neg-custom-args/captures/capt3.scala b/tests/neg-custom-args/captures/capt3.scala new file mode 100644 index 000000000000..4ffaf4a73c06 --- /dev/null +++ b/tests/neg-custom-args/captures/capt3.scala @@ -0,0 +1,27 @@ +import annotation.retains +class C +type Cap = C @retains(*) + +def test1() = + val x: Cap = C() + val y = () => { x; () } + val z = y + z: (() -> Unit) // error + +def test2() = + val x: Cap = C() + def y = () => { x; () } + def z = y + z: (() -> Unit) // error + +def test3() = + val x: Cap = C() + def y = () => { x; () } + val z = y + z: (() -> Unit) // error + +def test4() = + val x: Cap = C() + val y = () => { x; () } + def z = y + z: (() -> Unit) // error diff --git a/tests/neg-custom-args/captures/caseclass/Ref_1.scala b/tests/neg-custom-args/captures/caseclass/Ref_1.scala new file mode 100644 index 000000000000..492457b05cd3 --- /dev/null +++ b/tests/neg-custom-args/captures/caseclass/Ref_1.scala @@ -0,0 +1 @@ +case class Ref(x: () => Unit) diff --git a/tests/neg-custom-args/captures/caseclass/Test_2.scala b/tests/neg-custom-args/captures/caseclass/Test_2.scala new file mode 100644 index 000000000000..4eac6a260292 --- /dev/null +++ b/tests/neg-custom-args/captures/caseclass/Test_2.scala @@ -0,0 +1,25 @@ +@annotation.capability class C +def test(c: C) = + val pure: () -> Unit = () => () + val impure: () => Unit = pure + val mixed: {c} () -> Unit = pure + val x = Ref(impure) + val _: Ref = x // error + val y = x.copy() + val yc: Ref = y // error + val y0 = x.copy(pure) + val yc0: Ref = y0 + + val x2 = Ref(pure) + val _: Ref = x2 + val y2 = x2.copy() + val yc2: Ref = y2 + + val x3 = Ref(mixed) + val _: {c} Ref = x3 + val y3 = x3.copy() + val yc3: {c} Ref = y3 + + val y4 = y3 match + case Ref(xx) => xx + val y4c: {x3} () -> Unit = y4 diff --git a/tests/neg-custom-args/captures/cc-this.check b/tests/neg-custom-args/captures/cc-this.check new file mode 100644 index 000000000000..c492df15078f --- /dev/null +++ b/tests/neg-custom-args/captures/cc-this.check @@ -0,0 +1,18 @@ +-- [E007] Type Mismatch Error: tests/neg-custom-args/captures/cc-this.scala:8:15 --------------------------------------- +8 | val y: C = this // error + | ^^^^ + | Found: (C.this : {C.this.x} C) + | Required: C + | + | longer explanation available when compiling with `-explain` +-- Error: tests/neg-custom-args/captures/cc-this.scala:10:15 ----------------------------------------------------------- +10 | class C2(val x: () => Int): // error + | ^ + | reference (C2.this.x : () => Int) is not included in allowed capture set {} of the self type of class C2 +-- [E058] Type Mismatch Error: tests/neg-custom-args/captures/cc-this.scala:17:8 --------------------------------------- +17 | class C4(val f: () => Int) extends C3 // error + | ^ + | illegal inheritance: self type {C4.this.f} C4 of class C4 does not conform to self type C3 + | of parent class C3 + | + | longer explanation available when compiling with `-explain` diff --git a/tests/neg-custom-args/captures/cc-this.scala b/tests/neg-custom-args/captures/cc-this.scala new file mode 100644 index 000000000000..4c05be702c51 --- /dev/null +++ b/tests/neg-custom-args/captures/cc-this.scala @@ -0,0 +1,19 @@ +@annotation.capability class Cap + +def eff(using Cap): Unit = () + +def test(using Cap) = + + class C(val x: () => Int): + val y: C = this // error + + class C2(val x: () => Int): // error + this: C2 => + + class C3: + this: C3 => + val x: Object = this + + class C4(val f: () => Int) extends C3 // error + + diff --git a/tests/neg-custom-args/captures/cc-this2.check b/tests/neg-custom-args/captures/cc-this2.check new file mode 100644 index 000000000000..d10519636ca8 --- /dev/null +++ b/tests/neg-custom-args/captures/cc-this2.check @@ -0,0 +1,8 @@ + +-- [E058] Type Mismatch Error: tests/neg-custom-args/captures/cc-this2/D_2.scala:2:6 ----------------------------------- +2 |class D extends C: // error + | ^ + | illegal inheritance: self type {*} D of class D does not conform to self type C + | of parent class C + | + | longer explanation available when compiling with `-explain` diff --git a/tests/neg-custom-args/captures/cc-this2/C_1.scala b/tests/neg-custom-args/captures/cc-this2/C_1.scala new file mode 100644 index 000000000000..22718f301313 --- /dev/null +++ b/tests/neg-custom-args/captures/cc-this2/C_1.scala @@ -0,0 +1,3 @@ +open class C: + this: C => + val x: C = this diff --git a/tests/neg-custom-args/captures/cc-this2/D_2.scala b/tests/neg-custom-args/captures/cc-this2/D_2.scala new file mode 100644 index 000000000000..793f3f6353a9 --- /dev/null +++ b/tests/neg-custom-args/captures/cc-this2/D_2.scala @@ -0,0 +1,3 @@ + +class D extends C: // error + this: {*} D => diff --git a/tests/neg-custom-args/captures/cc-this3.check b/tests/neg-custom-args/captures/cc-this3.check new file mode 100644 index 000000000000..705cdfbc00d7 --- /dev/null +++ b/tests/neg-custom-args/captures/cc-this3.check @@ -0,0 +1,14 @@ +-- [E058] Type Mismatch Error: tests/neg-custom-args/captures/cc-this3.scala:8:6 --------------------------------------- +8 |class B extends A: // error + | ^ + | illegal inheritance: self type {*} B of class B does not conform to self type {} A + | of parent class A + | + | longer explanation available when compiling with `-explain` +-- [E058] Type Mismatch Error: tests/neg-custom-args/captures/cc-this3.scala:11:6 -------------------------------------- +11 |class C(val f: () => Int) extends A // error + | ^ + | illegal inheritance: self type {C.this.f} C of class C does not conform to self type {} A + | of parent class A + | + | longer explanation available when compiling with `-explain` diff --git a/tests/neg-custom-args/captures/cc-this3.scala b/tests/neg-custom-args/captures/cc-this3.scala new file mode 100644 index 000000000000..eeb9606f0c81 --- /dev/null +++ b/tests/neg-custom-args/captures/cc-this3.scala @@ -0,0 +1,18 @@ +@annotation.capability class Cap + +def eff(using Cap): Unit = () + +class A: + val x: A = this + +class B extends A: // error + this: {*} B => + +class C(val f: () => Int) extends A // error + +class A2 + +class B2 extends A2: // ok + this: {*} B2 => + +class C2(val f: () => Int) extends A2 // ok diff --git a/tests/neg-custom-args/captures/cc-this4.check b/tests/neg-custom-args/captures/cc-this4.check new file mode 100644 index 000000000000..a54ca8d57f4e --- /dev/null +++ b/tests/neg-custom-args/captures/cc-this4.check @@ -0,0 +1,6 @@ +-- Error: tests/neg-custom-args/captures/cc-this4.scala:1:11 ----------------------------------------------------------- +1 |open class C: // error + | ^ + | class C needs an explicitly declared self type since its + | inferred self type {} C + | is not visible in other compilation units that define subclasses. diff --git a/tests/neg-custom-args/captures/cc-this4.scala b/tests/neg-custom-args/captures/cc-this4.scala new file mode 100644 index 000000000000..7580067bfa62 --- /dev/null +++ b/tests/neg-custom-args/captures/cc-this4.scala @@ -0,0 +1,7 @@ +open class C: // error + val x: C = this + +open class D: + this: D => + val x: D = this + diff --git a/tests/neg-custom-args/captures/cc-this5.check b/tests/neg-custom-args/captures/cc-this5.check new file mode 100644 index 000000000000..8cc1ac9ccc5d --- /dev/null +++ b/tests/neg-custom-args/captures/cc-this5.check @@ -0,0 +1,18 @@ +-- Error: tests/neg-custom-args/captures/cc-this5.scala:16:20 ---------------------------------------------------------- +16 | def f = println(c) // error + | ^ + | (c : Cap) cannot be referenced here; it is not included in the allowed capture set {} +-- [E007] Type Mismatch Error: tests/neg-custom-args/captures/cc-this5.scala:21:15 ------------------------------------- +21 | val x: A = this // error + | ^^^^ + | Found: (A.this : {c} A) + | Required: A + | + | longer explanation available when compiling with `-explain` +-- [E058] Type Mismatch Error: tests/neg-custom-args/captures/cc-this5.scala:7:9 --------------------------------------- +7 | object D extends C: // error + | ^ + | illegal inheritance: self type {c} D.type of object D does not conform to self type {} C + | of parent class C + | + | longer explanation available when compiling with `-explain` diff --git a/tests/neg-custom-args/captures/cc-this5.scala b/tests/neg-custom-args/captures/cc-this5.scala new file mode 100644 index 000000000000..e84c2a41f55c --- /dev/null +++ b/tests/neg-custom-args/captures/cc-this5.scala @@ -0,0 +1,21 @@ +class C: + val x: C = this + +@annotation.capability class Cap + +def foo(c: Cap) = + object D extends C: // error + def bar: Unit = println(c) + object E: + def bar: Unit = println(c) + D.bar + +def test(c: Cap) = + class A: + val x: A = this + def f = println(c) // error + +def test2(c: Cap) = + class A: + def f = println(c) + val x: A = this // error diff --git a/tests/neg-custom-args/captures/cc1.scala b/tests/neg-custom-args/captures/cc1.scala new file mode 100644 index 000000000000..7f3cd784ef84 --- /dev/null +++ b/tests/neg-custom-args/captures/cc1.scala @@ -0,0 +1,5 @@ +import annotation.retains +object Test: + + def f[A <: Matchable @retains(*)](x: A): Matchable = x // error + diff --git a/tests/neg-custom-args/captures/class-constr.scala b/tests/neg-custom-args/captures/class-constr.scala new file mode 100644 index 000000000000..eeedf1043f37 --- /dev/null +++ b/tests/neg-custom-args/captures/class-constr.scala @@ -0,0 +1,24 @@ +import annotation.{capability, constructorOnly} + +@capability class Cap + +class C(x: Cap, @constructorOnly y: Cap) + +def test(a: Cap, b: Cap) = + val f = () => C(a, b) + val f_ok: {a, b} () -> {a} C = f + val f_no1: {a, b} () -> C = f // error + val f_no2: {a} () -> {a} C = f // error + val f_no3: {b} () -> {a} C = f // error + + class D: + val xz = + println(a) + 1 + def yz = + println(b) + 2 + val d = () => new D() + val d_ok1: {a, b} () -> {a, b} D = d + val d_ok2: () -> {a, b} D = d // because of function shorthand + val d_ok3: {a, b} () -> {b} D = d // error, but should work diff --git a/tests/neg-custom-args/captures/class-contra.check b/tests/neg-custom-args/captures/class-contra.check new file mode 100644 index 000000000000..69a5f0097de8 --- /dev/null +++ b/tests/neg-custom-args/captures/class-contra.check @@ -0,0 +1,7 @@ +-- [E007] Type Mismatch Error: tests/neg-custom-args/captures/class-contra.scala:12:39 --------------------------------- +12 | def fun(x: K{val f: {a} T}) = x.setf(a) // error + | ^ + | Found: (a : {x, y} T) + | Required: T + | + | longer explanation available when compiling with `-explain` diff --git a/tests/neg-custom-args/captures/class-contra.scala b/tests/neg-custom-args/captures/class-contra.scala new file mode 100644 index 000000000000..270aaf9309a9 --- /dev/null +++ b/tests/neg-custom-args/captures/class-contra.scala @@ -0,0 +1,13 @@ + +class C +type Cap = {*} C + +class K(val f: {*} T): + def setf(x: {f} T) = ??? + +class T + +def test(x: Cap, y: Cap) = + val a: {x, y} T = ??? + def fun(x: K{val f: {a} T}) = x.setf(a) // error + () \ No newline at end of file diff --git a/tests/neg-custom-args/captures/classes.scala b/tests/neg-custom-args/captures/classes.scala new file mode 100644 index 000000000000..e4c141ea981b --- /dev/null +++ b/tests/neg-custom-args/captures/classes.scala @@ -0,0 +1,12 @@ +class B +type Cap = {*} B +class C0(n: Cap) // was error: class parameter must be a `val`, now OK + +class C(val n: Cap): + def foo(): {n} B = n + +def test(x: Cap, y: Cap) = + val c0 = C(x) + val c1: C = c0 // error + val c2 = if ??? then C(x) else identity(C(y)) + val c3: {x} C { val n: {x, y} B } = c2 // error diff --git a/tests/neg-custom-args/captures/ctest.scala b/tests/neg-custom-args/captures/ctest.scala new file mode 100644 index 000000000000..08bec16d8177 --- /dev/null +++ b/tests/neg-custom-args/captures/ctest.scala @@ -0,0 +1,6 @@ +class CC +type Cap = {*} CC + +def test(cap1: Cap, cap2: Cap) = + var b: List[String => String] = Nil // was error, now OK + val bc = b.head // error diff --git a/tests/neg-custom-args/captures/curried-simplified.check b/tests/neg-custom-args/captures/curried-simplified.check new file mode 100644 index 000000000000..b91914f72404 --- /dev/null +++ b/tests/neg-custom-args/captures/curried-simplified.check @@ -0,0 +1,42 @@ +-- [E007] Type Mismatch Error: tests/neg-custom-args/captures/curried-simplified.scala:7:28 ---------------------------- +7 | def y1: () -> () -> Int = x1 // error + | ^^ + | Found: {x} () -> {x} () -> Int + | Required: () -> () -> Int + | + | longer explanation available when compiling with `-explain` +-- [E007] Type Mismatch Error: tests/neg-custom-args/captures/curried-simplified.scala:9:28 ---------------------------- +9 | def y2: () -> () => Int = x2 // error + | ^^ + | Found: {x} () -> {*} () -> Int + | Required: () -> () => Int + | + | longer explanation available when compiling with `-explain` +-- [E007] Type Mismatch Error: tests/neg-custom-args/captures/curried-simplified.scala:11:39 --------------------------- +11 | def y3: Cap -> Protect[Int -> Int] = x3 // error + | ^^ + | Found: (x$0: Cap) -> {x$0} Int -> Int + | Required: Cap -> Protect[Int -> Int] + | + | longer explanation available when compiling with `-explain` +-- [E007] Type Mismatch Error: tests/neg-custom-args/captures/curried-simplified.scala:15:33 --------------------------- +15 | def y5: Cap -> {} Int -> Int = x5 // error + | ^^ + | Found: Cap -> {x} Int -> Int + | Required: Cap -> {} Int -> Int + | + | longer explanation available when compiling with `-explain` +-- [E007] Type Mismatch Error: tests/neg-custom-args/captures/curried-simplified.scala:17:49 --------------------------- +17 | def y6: Cap -> {} Cap -> Protect[Int -> Int] = x6 // error + | ^^ + | Found: (x$0: Cap) -> {x$0} (x$0: Cap) -> {x$0, x$0} Int -> Int + | Required: Cap -> {} Cap -> Protect[Int -> Int] + | + | longer explanation available when compiling with `-explain` +-- [E007] Type Mismatch Error: tests/neg-custom-args/captures/curried-simplified.scala:19:49 --------------------------- +19 | def y7: Cap -> Protect[Cap -> {} Int -> Int] = x7 // error + | ^^ + | Found: (x$0: Cap) -> {x$0} (x: Cap) -> {x$0, x} Int -> Int + | Required: Cap -> Protect[Cap -> {} Int -> Int] + | + | longer explanation available when compiling with `-explain` diff --git a/tests/neg-custom-args/captures/curried-simplified.scala b/tests/neg-custom-args/captures/curried-simplified.scala new file mode 100644 index 000000000000..25b23370d154 --- /dev/null +++ b/tests/neg-custom-args/captures/curried-simplified.scala @@ -0,0 +1,21 @@ +@annotation.capability class Cap + +type Protect[T] = T + +def test(x: Cap, y: Cap) = + def x1: {x} () -> () -> Int = ??? + def y1: () -> () -> Int = x1 // error + def x2: {x} () -> () => Int = ??? + def y2: () -> () => Int = x2 // error + def x3: Cap -> Int -> Int = ??? + def y3: Cap -> Protect[Int -> Int] = x3 // error + def x4: Cap -> Protect[Int -> Int] = ??? + def y4: Cap -> {} Int -> Int = x4 // ok + def x5: Cap -> {x} Int -> Int = ??? + def y5: Cap -> {} Int -> Int = x5 // error + def x6: Cap -> Cap -> Int -> Int = ??? + def y6: Cap -> {} Cap -> Protect[Int -> Int] = x6 // error + def x7: Cap -> (x: Cap) -> Int -> Int = ??? + def y7: Cap -> Protect[Cap -> {} Int -> Int] = x7 // error + + diff --git a/tests/neg-custom-args/captures/emptyref-in-self.scala b/tests/neg-custom-args/captures/emptyref-in-self.scala new file mode 100644 index 000000000000..60f782deca6b --- /dev/null +++ b/tests/neg-custom-args/captures/emptyref-in-self.scala @@ -0,0 +1,3 @@ +class Zip[A, B](underlying: String, other: {*} String) { + this: {underlying, other} Zip[A, B] => // error +} diff --git a/tests/neg-custom-args/captures/eta.check b/tests/neg-custom-args/captures/eta.check new file mode 100644 index 000000000000..da3609a86771 --- /dev/null +++ b/tests/neg-custom-args/captures/eta.check @@ -0,0 +1,14 @@ +-- [E007] Type Mismatch Error: tests/neg-custom-args/captures/eta.scala:4:9 -------------------------------------------- +4 | g // error + | ^ + | Found: (g : () -> A) + | Required: () -> {f} Proc + | + | longer explanation available when compiling with `-explain` +-- [E007] Type Mismatch Error: tests/neg-custom-args/captures/eta.scala:6:14 ------------------------------------------- +6 | bar( () => f ) // error + | ^^^^^^^ + | Found: {f} () -> box {f} () -> Unit + | Required: () -> box ? () -> Unit + | + | longer explanation available when compiling with `-explain` diff --git a/tests/neg-custom-args/captures/eta.scala b/tests/neg-custom-args/captures/eta.scala new file mode 100644 index 000000000000..3d9d759d2203 --- /dev/null +++ b/tests/neg-custom-args/captures/eta.scala @@ -0,0 +1,7 @@ + type Proc = (() -> Unit) + def foo(f: {*} Proc): {} Proc = + def bar[A <: {f} Proc](g: () -> A): () -> {f} Proc = + g // error + val stowaway: () -> {f} Proc = + bar( () => f ) // error + () => { stowaway.apply().apply() } \ No newline at end of file diff --git a/tests/neg-custom-args/captures/exceptions.scala b/tests/neg-custom-args/captures/exceptions.scala new file mode 100644 index 000000000000..bcfef0930434 --- /dev/null +++ b/tests/neg-custom-args/captures/exceptions.scala @@ -0,0 +1,16 @@ +import language.experimental.saferExceptions + +class LimitExceeded extends Exception + +val limit = 10e+10 + +def f(x: Double): Double throws LimitExceeded = + if x < limit then x * x else throw LimitExceeded() + +def escaped(xs: Double*)(using CanThrow[LimitExceeded]): () => Double = + try () => xs.map(f).sum // error + catch case ex: LimitExceeded => () => -1 + +def escaped2(xs: Double*): (() => Double) throws LimitExceeded = + try () => xs.map(f).sum // error + catch case ex: LimitExceeded => () => -1 diff --git a/tests/neg-custom-args/captures/i15049.scala b/tests/neg-custom-args/captures/i15049.scala new file mode 100644 index 000000000000..4e32172c025d --- /dev/null +++ b/tests/neg-custom-args/captures/i15049.scala @@ -0,0 +1,10 @@ +class Session: + def request = "Response" +class Foo: + private val session: {*} Session = new Session + def withSession[T](f: ({*} Session) => T): T = f(session) + +def Test = + val f = new Foo + f.withSession(s => s).request // error + f.withSession[{*} Session](t => t) // error diff --git a/tests/neg-custom-args/captures/i15116.check b/tests/neg-custom-args/captures/i15116.check new file mode 100644 index 000000000000..83c552087646 --- /dev/null +++ b/tests/neg-custom-args/captures/i15116.check @@ -0,0 +1,28 @@ +-- Error: tests/neg-custom-args/captures/i15116.scala:3:6 -------------------------------------------------------------- +3 | val x = Foo(m) // error + | ^^^^^^^^^^^^^^ + | Non-local value x cannot have an inferred type + | {Bar.this.m} Foo{m: {Bar.this.m} String} + | with non-empty capture set {Bar.this.m}. + | The type needs to be declared explicitly. +-- Error: tests/neg-custom-args/captures/i15116.scala:5:6 -------------------------------------------------------------- +5 | val x = Foo(m) // error + | ^^^^^^^^^^^^^^ + | Non-local value x cannot have an inferred type + | {Baz.this} Foo{m: {Baz.this} String} + | with non-empty capture set {Baz.this}. + | The type needs to be declared explicitly. +-- Error: tests/neg-custom-args/captures/i15116.scala:7:6 -------------------------------------------------------------- +7 | val x = Foo(m) // error + | ^^^^^^^^^^^^^^ + | Non-local value x cannot have an inferred type + | {Bar1.this.m} Foo{m: {Bar1.this.m} String} + | with non-empty capture set {Bar1.this.m}. + | The type needs to be declared explicitly. +-- Error: tests/neg-custom-args/captures/i15116.scala:9:6 -------------------------------------------------------------- +9 | val x = Foo(m) // error + | ^^^^^^^^^^^^^^ + | Non-local value x cannot have an inferred type + | {Baz2.this} Foo{m: {Baz2.this} String} + | with non-empty capture set {Baz2.this}. + | The type needs to be declared explicitly. diff --git a/tests/neg-custom-args/captures/i15116.scala b/tests/neg-custom-args/captures/i15116.scala new file mode 100644 index 000000000000..1659f251df3e --- /dev/null +++ b/tests/neg-custom-args/captures/i15116.scala @@ -0,0 +1,9 @@ +class Foo(m: {*} String) +class Bar(val m: {*} String): + val x = Foo(m) // error +trait Baz(val m: {*} String): + val x = Foo(m) // error +class Bar1(m: {*} String): + val x = Foo(m) // error +trait Baz2(m: {*} String): + val x = Foo(m) // error diff --git a/tests/neg-custom-args/captures/i15749.scala b/tests/neg-custom-args/captures/i15749.scala new file mode 100644 index 000000000000..00d1811498f7 --- /dev/null +++ b/tests/neg-custom-args/captures/i15749.scala @@ -0,0 +1,15 @@ +class Unit +object unit extends Unit + +type Top = {*} Any + +type LazyVal[T] = {*} Unit -> T + +class Foo[T](val x: T) + +// Foo[□ {*} Unit -> T] +type BoxedLazyVal[T] = Foo[LazyVal[T]] + +def force[A](v: BoxedLazyVal[A]): A = + // Γ ⊢ v.x : □ {*} Unit -> A + v.x(unit) // error: (unbox v.x)(unit), where (unbox v.x) should be untypable \ No newline at end of file diff --git a/tests/neg-custom-args/captures/i15749a.scala b/tests/neg-custom-args/captures/i15749a.scala new file mode 100644 index 000000000000..9e439e28e98c --- /dev/null +++ b/tests/neg-custom-args/captures/i15749a.scala @@ -0,0 +1,21 @@ +class Unit +object unit extends Unit + +type Top = {*} Any + +type Wrapper[T] = [X] -> (op: {*} T -> X) -> X + +def test = + + def wrapper[T](x: T): Wrapper[T] = + [X] => (op: {*} T -> X) => op(x) + + def strictMap[A <: Top, B <: Top](mx: Wrapper[A])(f: {*} A -> B): Wrapper[B] = + mx((x: A) => wrapper(f(x))) + + def force[A](thunk: {*} Unit -> A): A = thunk(unit) + + def forceWrapper[A](mx: Wrapper[{*} Unit -> A]): Wrapper[A] = + // Γ ⊢ mx: Wrapper[□ {*} Unit => A] + // `force` should be typed as ∀(□ {*} Unit -> A) A, but it can not + strictMap[{*} Unit -> A, A](mx)(t => force[A](t)) // error diff --git a/tests/neg-custom-args/captures/i15772.check b/tests/neg-custom-args/captures/i15772.check new file mode 100644 index 000000000000..0c59e81742f1 --- /dev/null +++ b/tests/neg-custom-args/captures/i15772.check @@ -0,0 +1,36 @@ +-- [E007] Type Mismatch Error: tests/neg-custom-args/captures/i15772.scala:18:2 ---------------------------------------- +18 | () => // error + | ^ + | Found: {x} () -> Int + | Required: () -> Int +19 | val c : {x} C = new C(x) +20 | val boxed1 : (({*} C) => Unit) -> Unit = box1(c) +21 | boxed1((cap: {*} C) => unsafe(c)) +22 | 0 + | + | longer explanation available when compiling with `-explain` +-- [E007] Type Mismatch Error: tests/neg-custom-args/captures/i15772.scala:25:2 ---------------------------------------- +25 | () => // error + | ^ + | Found: {x} () -> Int + | Required: () -> Int +26 | val c : {x} C = new C(x) +27 | val boxed2 : Observe[{*} C] = box2(c) +28 | boxed2((cap: {*} C) => unsafe(c)) +29 | 0 + | + | longer explanation available when compiling with `-explain` +-- [E007] Type Mismatch Error: tests/neg-custom-args/captures/i15772.scala:33:37 --------------------------------------- +33 | val boxed2 : Observe[{*} C] = box2(c) // error + | ^ + | Found: {*} C + | Required: box {*} C{arg: ? C} + | + | longer explanation available when compiling with `-explain` +-- [E007] Type Mismatch Error: tests/neg-custom-args/captures/i15772.scala:44:2 ---------------------------------------- +44 | x: (() -> Unit) // error + | ^ + | Found: (x : {sayHello, io} () -> Unit) + | Required: () -> Unit + | + | longer explanation available when compiling with `-explain` diff --git a/tests/neg-custom-args/captures/i15772.scala b/tests/neg-custom-args/captures/i15772.scala new file mode 100644 index 000000000000..29794443c297 --- /dev/null +++ b/tests/neg-custom-args/captures/i15772.scala @@ -0,0 +1,44 @@ +type Observe[T] = (T => Unit) -> Unit + +def unsafe(cap: {*} C) = cap.bad() + +def box1[T](v: T) : (T => Unit) -> Unit = { + (fn: T => Unit) => fn(v) +} + +def box2[T](v: T) : Observe[T] = { + (fn: T => Unit) => fn(v) +} + +class C(val arg: {*} C) { + def bad() = println("I've gone bad!") +} + +def main1(x: {*} C) : () -> Int = + () => // error + val c : {x} C = new C(x) + val boxed1 : (({*} C) => Unit) -> Unit = box1(c) + boxed1((cap: {*} C) => unsafe(c)) + 0 + +def main2(x: {*} C) : () -> Int = + () => // error + val c : {x} C = new C(x) + val boxed2 : Observe[{*} C] = box2(c) + boxed2((cap: {*} C) => unsafe(c)) + 0 + +def main3(x: {*} C) = + def c : {*} C = new C(x) + val boxed2 : Observe[{*} C] = box2(c) // error + boxed2((cap: {*} C) => unsafe(c)) + 0 + +trait File: + def write(s: String): Unit + +def main(io: {*} Any) = + val sayHello: (({io} File) => Unit) = (file: {io} File) => file.write("Hello World!\r\n") + val filesList : List[{io} File] = ??? + val x = () => filesList.foreach(sayHello) + x: (() -> Unit) // error \ No newline at end of file diff --git a/tests/neg-custom-args/captures/inner-classes.scala b/tests/neg-custom-args/captures/inner-classes.scala new file mode 100644 index 000000000000..cf4073b36f81 --- /dev/null +++ b/tests/neg-custom-args/captures/inner-classes.scala @@ -0,0 +1,26 @@ +object test: + + @annotation.capability class FileSystem + + def foo(fs: FileSystem) = + + trait LazyList[+A]: + this: {fs} LazyList[A] => + + def isEmpty: Boolean + def head: A + def tail: {this} LazyList[A] + + object LazyNil extends LazyList[Nothing]: + def isEmpty: Boolean = true + def head = ??? + def tail = ??? + + final class LazyCons[+T](val x: T, val xs: () => {*} LazyList[T]) extends LazyList[T]: // error + def isEmpty = false + def head = x + def tail: {this} LazyList[T] = xs() + end LazyCons + + new LazyCons(1, () => LazyNil) + diff --git a/tests/neg-custom-args/captures/io.scala b/tests/neg-custom-args/captures/io.scala new file mode 100644 index 000000000000..91af0167c9f9 --- /dev/null +++ b/tests/neg-custom-args/captures/io.scala @@ -0,0 +1,23 @@ +import annotation.retains +sealed trait IO: + def puts(msg: Any): Unit = println(msg) + +def test1 = + val IO : IO @retains(*) = new IO {} + def foo = {IO; IO.puts("hello") } + val x : () -> Unit = () => foo // error: Found: (() -> Unit) retains IO; Required: () -> Unit + +def test2 = + val IO : IO @retains(*) = new IO {} + def puts(msg: Any, io: IO @retains(*)) = println(msg) + def foo() = puts("hello", IO) + val x : () -> Unit = () => foo() // error: Found: (() -> Unit) retains IO; Required: () -> Unit + +type Capability[T] = T @retains(*) + +def test3 = + val IO : Capability[IO] = new IO {} + def puts(msg: Any, io: Capability[IO]) = println(msg) + def foo() = puts("hello", IO) + val x : () -> Unit = () => foo() // error: Found: (() -> Unit) retains IO; Required: () -> Unit + diff --git a/tests/neg-custom-args/captures/lazylist.check b/tests/neg-custom-args/captures/lazylist.check new file mode 100644 index 000000000000..e43538ad97f7 --- /dev/null +++ b/tests/neg-custom-args/captures/lazylist.check @@ -0,0 +1,42 @@ +-- [E163] Declaration Error: tests/neg-custom-args/captures/lazylist.scala:22:6 ---------------------------------------- +22 | def tail: {*} LazyList[Nothing] = ??? // error overriding + | ^ + | error overriding method tail in class LazyList of type -> lazylists.LazyList[Nothing]; + | method tail of type -> {*} lazylists.LazyList[Nothing] has incompatible type + | + | longer explanation available when compiling with `-explain` +-- [E007] Type Mismatch Error: tests/neg-custom-args/captures/lazylist.scala:35:29 ------------------------------------- +35 | val ref1c: LazyList[Int] = ref1 // error + | ^^^^ + | Found: (ref1 : {cap1} lazylists.LazyCons[Int]{xs: {cap1} () -> {*} lazylists.LazyList[Int]}) + | Required: lazylists.LazyList[Int] + | + | longer explanation available when compiling with `-explain` +-- [E007] Type Mismatch Error: tests/neg-custom-args/captures/lazylist.scala:37:36 ------------------------------------- +37 | val ref2c: {ref1} LazyList[Int] = ref2 // error + | ^^^^ + | Found: (ref2 : {cap2, ref1} lazylists.LazyList[Int]) + | Required: {ref1} lazylists.LazyList[Int] + | + | longer explanation available when compiling with `-explain` +-- [E007] Type Mismatch Error: tests/neg-custom-args/captures/lazylist.scala:39:36 ------------------------------------- +39 | val ref3c: {cap2} LazyList[Int] = ref3 // error + | ^^^^ + | Found: (ref3 : {cap2, ref1} lazylists.LazyList[Int]) + | Required: {cap2} lazylists.LazyList[Int] + | + | longer explanation available when compiling with `-explain` +-- [E007] Type Mismatch Error: tests/neg-custom-args/captures/lazylist.scala:41:48 ------------------------------------- +41 | val ref4c: {cap1, ref3, cap3} LazyList[Int] = ref4 // error + | ^^^^ + | Found: (ref4 : {cap3, cap2, ref1, cap1} lazylists.LazyList[Int]) + | Required: {cap1, ref3, cap3} lazylists.LazyList[Int] + | + | longer explanation available when compiling with `-explain` +-- Error: tests/neg-custom-args/captures/lazylist.scala:17:6 ----------------------------------------------------------- +17 | def tail = xs() // error: cannot have an inferred type + | ^^^^^^^^^^^^^^^ + | Non-local method tail cannot have an inferred result type + | {LazyCons.this.xs} lazylists.LazyList[? T] + | with non-empty capture set {LazyCons.this.xs}. + | The type needs to be declared explicitly. diff --git a/tests/neg-custom-args/captures/lazylist.scala b/tests/neg-custom-args/captures/lazylist.scala new file mode 100644 index 000000000000..56bfc3ea6da2 --- /dev/null +++ b/tests/neg-custom-args/captures/lazylist.scala @@ -0,0 +1,41 @@ +package lazylists + +abstract class LazyList[+T]: + this: ({*} LazyList[T]) => + + def isEmpty: Boolean + def head: T + def tail: LazyList[T] + + def map[U](f: T => U): {f, this} LazyList[U] = + if isEmpty then LazyNil + else LazyCons(f(head), () => tail.map(f)) + +class LazyCons[+T](val x: T, val xs: () => {*} LazyList[T]) extends LazyList[T]: + def isEmpty = false + def head = x + def tail = xs() // error: cannot have an inferred type + +object LazyNil extends LazyList[Nothing]: + def isEmpty = true + def head = ??? + def tail: {*} LazyList[Nothing] = ??? // error overriding + +def map[A, B](xs: {*} LazyList[A], f: A => B): {f, xs} LazyList[B] = + xs.map(f) + +class CC +type Cap = {*} CC + +def test(cap1: Cap, cap2: Cap, cap3: Cap) = + def f[T](x: LazyList[T]): LazyList[T] = if cap1 == cap1 then x else LazyNil + def g(x: Int) = if cap2 == cap2 then x else 0 + def h(x: Int) = if cap3 == cap3 then x else 0 + val ref1 = LazyCons(1, () => f(LazyNil)) + val ref1c: LazyList[Int] = ref1 // error + val ref2 = map(ref1, g) + val ref2c: {ref1} LazyList[Int] = ref2 // error + val ref3 = ref1.map(g) + val ref3c: {cap2} LazyList[Int] = ref3 // error + val ref4 = (if cap1 == cap2 then ref1 else ref2).map(h) + val ref4c: {cap1, ref3, cap3} LazyList[Int] = ref4 // error diff --git a/tests/neg-custom-args/captures/lazylists-exceptions.check b/tests/neg-custom-args/captures/lazylists-exceptions.check new file mode 100644 index 000000000000..bd6fad047fe9 --- /dev/null +++ b/tests/neg-custom-args/captures/lazylists-exceptions.check @@ -0,0 +1,10 @@ +-- Error: tests/neg-custom-args/captures/lazylists-exceptions.scala:36:2 ----------------------------------------------- +36 | try // error + | ^ + | The expression's type {*} LazyList[Int] is not allowed to capture the root capability `*`. + | This usually means that a capability persists longer than its allowed lifetime. +37 | tabulate(10) { i => +38 | if i > 9 then throw Ex1() +39 | i * i +40 | } +41 | catch case ex: Ex1 => LazyNil diff --git a/tests/neg-custom-args/captures/lazylists-exceptions.scala b/tests/neg-custom-args/captures/lazylists-exceptions.scala new file mode 100644 index 000000000000..6cba934d61e8 --- /dev/null +++ b/tests/neg-custom-args/captures/lazylists-exceptions.scala @@ -0,0 +1,45 @@ +import language.experimental.saferExceptions + +trait LazyList[+A]: + this: {*} LazyList[A] => + + def isEmpty: Boolean + def head: A + def tail: {this} LazyList[A] + +object LazyNil extends LazyList[Nothing]: + def isEmpty: Boolean = true + def head = ??? + def tail = ??? + +final class LazyCons[+T](val x: T, val xs: () => {*} LazyList[T]) extends LazyList[T]: + this: {*} LazyList[T] => + + def isEmpty = false + def head = x + def tail: {this} LazyList[T] = xs() +end LazyCons + +extension [A](x: A) + def #:(xs1: => {*} LazyList[A]): {xs1} LazyList[A] = + LazyCons(x, () => xs1) + +def tabulate[A](n: Int)(gen: Int => A): {gen} LazyList[A] = + def recur(i: Int): {gen} LazyList[A] = + if i == n then LazyNil + else gen(i) #: recur(i + 1) + recur(0) + +class Ex1 extends Exception + +def problem = + try // error + tabulate(10) { i => + if i > 9 then throw Ex1() + i * i + } + catch case ex: Ex1 => LazyNil + + + + diff --git a/tests/neg-custom-args/captures/lazylists1.check b/tests/neg-custom-args/captures/lazylists1.check new file mode 100644 index 000000000000..f91e2500dc15 --- /dev/null +++ b/tests/neg-custom-args/captures/lazylists1.check @@ -0,0 +1,7 @@ +-- [E007] Type Mismatch Error: tests/neg-custom-args/captures/lazylists1.scala:25:66 ----------------------------------- +25 | def concat(other: {f} LazyList[A]): {this, f} LazyList[A] = ??? : ({xs, f} LazyList[A]) // error + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^ + | Found: {xs, f} LazyList[A] + | Required: {Mapped.this, f} LazyList[A] + | + | longer explanation available when compiling with `-explain` diff --git a/tests/neg-custom-args/captures/lazylists1.scala b/tests/neg-custom-args/captures/lazylists1.scala new file mode 100644 index 000000000000..c6475223b783 --- /dev/null +++ b/tests/neg-custom-args/captures/lazylists1.scala @@ -0,0 +1,27 @@ +class CC +type Cap = {*} CC + +trait LazyList[+A]: + this: ({*} LazyList[A]) => + + def isEmpty: Boolean + def head: A + def tail: {this} LazyList[A] + +object LazyNil extends LazyList[Nothing]: + def isEmpty: Boolean = true + def head = ??? + def tail = ??? + +extension [A](xs: {*} LazyList[A]) + def map[B](f: A => B): {xs, f} LazyList[B] = + final class Mapped extends LazyList[B]: + this: ({xs, f} Mapped) => + + def isEmpty = false + def head: B = f(xs.head) + def tail: {this} LazyList[B] = xs.tail.map(f) // OK + def drop(n: Int): {this} LazyList[B] = ??? : ({xs, f} LazyList[B]) // OK + def concat(other: {f} LazyList[A]): {this, f} LazyList[A] = ??? : ({xs, f} LazyList[A]) // error + new Mapped + diff --git a/tests/neg-custom-args/captures/lazylists2.check b/tests/neg-custom-args/captures/lazylists2.check new file mode 100644 index 000000000000..41881b57da24 --- /dev/null +++ b/tests/neg-custom-args/captures/lazylists2.check @@ -0,0 +1,44 @@ +-- [E163] Declaration Error: tests/neg-custom-args/captures/lazylists2.scala:50:10 ------------------------------------- +50 | def tail: {xs, f} LazyList[B] = xs.tail.map(f) // error + | ^ + | error overriding method tail in trait LazyList of type -> {Mapped.this} LazyList[B]; + | method tail of type -> {xs, f} LazyList[B] has incompatible type + | + | longer explanation available when compiling with `-explain` +-- [E007] Type Mismatch Error: tests/neg-custom-args/captures/lazylists2.scala:18:4 ------------------------------------ +18 | final class Mapped extends LazyList[B]: // error + | ^ + | Found: {f, xs} LazyList[? B] + | Required: {f} LazyList[B] +19 | this: ({xs, f} Mapped) => +20 | def isEmpty = false +21 | def head: B = f(xs.head) +22 | def tail: {this} LazyList[B] = xs.tail.map(f) +23 | new Mapped + | + | longer explanation available when compiling with `-explain` +-- [E007] Type Mismatch Error: tests/neg-custom-args/captures/lazylists2.scala:27:4 ------------------------------------ +27 | final class Mapped extends LazyList[B]: // error + | ^ + | Found: {f, xs} LazyList[? B] + | Required: {xs} LazyList[B] +28 | this: ({xs, f} Mapped) => +29 | def isEmpty = false +30 | def head: B = f(xs.head) +31 | def tail: {this} LazyList[B] = xs.tail.map(f) +32 | new Mapped + | + | longer explanation available when compiling with `-explain` +-- Error: tests/neg-custom-args/captures/lazylists2.scala:40:20 -------------------------------------------------------- +40 | def head: B = f(xs.head) // error + | ^ + |(f : A => B) cannot be referenced here; it is not included in the allowed capture set {xs} of the self type of class Mapped +-- Error: tests/neg-custom-args/captures/lazylists2.scala:41:49 -------------------------------------------------------- +41 | def tail: {this} LazyList[B] = xs.tail.map(f) // error + | ^ + |(f : A => B) cannot be referenced here; it is not included in the allowed capture set {xs} of the self type of class Mapped +-- Error: tests/neg-custom-args/captures/lazylists2.scala:60:10 -------------------------------------------------------- +60 | class Mapped2 extends Mapped: // error + | ^ + | references {f, xs} are not all included in allowed capture set {} of the self type of class Mapped2 +61 | this: Mapped => diff --git a/tests/neg-custom-args/captures/lazylists2.scala b/tests/neg-custom-args/captures/lazylists2.scala new file mode 100644 index 000000000000..7b661e931441 --- /dev/null +++ b/tests/neg-custom-args/captures/lazylists2.scala @@ -0,0 +1,64 @@ +class CC +type Cap = {*} CC + +trait LazyList[+A]: + this: ({*} LazyList[A]) => + + def isEmpty: Boolean + def head: A + def tail: {this} LazyList[A] + +object LazyNil extends LazyList[Nothing]: + def isEmpty: Boolean = true + def head = ??? + def tail = ??? + +extension [A](xs: {*} LazyList[A]) + def map[B](f: A => B): {f} LazyList[B] = + final class Mapped extends LazyList[B]: // error + this: ({xs, f} Mapped) => + + def isEmpty = false + def head: B = f(xs.head) + def tail: {this} LazyList[B] = xs.tail.map(f) + new Mapped + + def map2[B](f: A => B): {xs} LazyList[B] = + final class Mapped extends LazyList[B]: // error + this: ({xs, f} Mapped) => + + def isEmpty = false + def head: B = f(xs.head) + def tail: {this} LazyList[B] = xs.tail.map(f) + new Mapped + + def map3[B](f: A => B): {xs} LazyList[B] = + final class Mapped extends LazyList[B]: + this: ({xs} Mapped) => + + def isEmpty = false + def head: B = f(xs.head) // error + def tail: {this} LazyList[B] = xs.tail.map(f) // error + new Mapped + + def map4[B](f: A => B): {xs} LazyList[B] = + final class Mapped extends LazyList[B]: + this: ({xs, f} Mapped) => + + def isEmpty = false + def head: B = f(xs.head) + def tail: {xs, f} LazyList[B] = xs.tail.map(f) // error + new Mapped + + def map5[B](f: A => B): LazyList[B] = + class Mapped extends LazyList[B]: + this: ({xs, f} Mapped) => + + def isEmpty = false + def head: B = f(xs.head) + def tail: {this} LazyList[B] = xs.tail.map(f) + class Mapped2 extends Mapped: // error + this: Mapped => + new Mapped2 + + diff --git a/tests/neg-custom-args/captures/lazyref.check b/tests/neg-custom-args/captures/lazyref.check new file mode 100644 index 000000000000..fcd98d0d67bd --- /dev/null +++ b/tests/neg-custom-args/captures/lazyref.check @@ -0,0 +1,28 @@ +-- [E007] Type Mismatch Error: tests/neg-custom-args/captures/lazyref.scala:19:28 -------------------------------------- +19 | val ref1c: LazyRef[Int] = ref1 // error + | ^^^^ + | Found: (ref1 : {cap1} LazyRef[Int]{elem: {cap1} () -> Int}) + | Required: LazyRef[Int] + | + | longer explanation available when compiling with `-explain` +-- [E007] Type Mismatch Error: tests/neg-custom-args/captures/lazyref.scala:21:35 -------------------------------------- +21 | val ref2c: {cap2} LazyRef[Int] = ref2 // error + | ^^^^ + | Found: (ref2 : {cap2, ref1} LazyRef[Int]{elem: {*} () -> Int}) + | Required: {cap2} LazyRef[Int] + | + | longer explanation available when compiling with `-explain` +-- [E007] Type Mismatch Error: tests/neg-custom-args/captures/lazyref.scala:23:35 -------------------------------------- +23 | val ref3c: {ref1} LazyRef[Int] = ref3 // error + | ^^^^ + | Found: (ref3 : {cap2, ref1} LazyRef[Int]{elem: {*} () -> Int}) + | Required: {ref1} LazyRef[Int] + | + | longer explanation available when compiling with `-explain` +-- [E007] Type Mismatch Error: tests/neg-custom-args/captures/lazyref.scala:25:35 -------------------------------------- +25 | val ref4c: {cap1} LazyRef[Int] = ref4 // error + | ^^^^ + | Found: (ref4 : {cap2, cap1} LazyRef[Int]{elem: {*} () -> Int}) + | Required: {cap1} LazyRef[Int] + | + | longer explanation available when compiling with `-explain` diff --git a/tests/neg-custom-args/captures/lazyref.scala b/tests/neg-custom-args/captures/lazyref.scala new file mode 100644 index 000000000000..8395e5cb42cd --- /dev/null +++ b/tests/neg-custom-args/captures/lazyref.scala @@ -0,0 +1,25 @@ +class CC +type Cap = {*} CC + +class LazyRef[T](val elem: () => T): + val get: () => T = elem + def map[U](f: T => U): {f, this} LazyRef[U] = + new LazyRef(() => f(elem())) + +def map[A, B](ref: {*} LazyRef[A], f: A => B): {f, ref} LazyRef[B] = + new LazyRef(() => f(ref.elem())) + +def mapc[A, B]: (ref: {*} LazyRef[A], f: A => B) -> {f, ref} LazyRef[B] = + (ref1, f1) => map[A, B](ref1, f1) + +def test(cap1: Cap, cap2: Cap) = + def f(x: Int) = if cap1 == cap1 then x else 0 + def g(x: Int) = if cap2 == cap2 then x else 0 + val ref1 = LazyRef(() => f(0)) + val ref1c: LazyRef[Int] = ref1 // error + val ref2 = map(ref1, g) + val ref2c: {cap2} LazyRef[Int] = ref2 // error + val ref3 = ref1.map(g) + val ref3c: {ref1} LazyRef[Int] = ref3 // error + val ref4 = (if cap1 == cap2 then ref1 else ref2).map(g) + val ref4c: {cap1} LazyRef[Int] = ref4 // error diff --git a/tests/neg-custom-args/captures/nestedclass.check b/tests/neg-custom-args/captures/nestedclass.check new file mode 100644 index 000000000000..cb4421ece0ec --- /dev/null +++ b/tests/neg-custom-args/captures/nestedclass.check @@ -0,0 +1,7 @@ +-- [E007] Type Mismatch Error: tests/neg-custom-args/captures/nestedclass.scala:15:15 ---------------------------------- +15 | val xsc: C = xs // error + | ^^ + | Found: (xs : {cap1} C) + | Required: C + | + | longer explanation available when compiling with `-explain` diff --git a/tests/neg-custom-args/captures/nestedclass.scala b/tests/neg-custom-args/captures/nestedclass.scala new file mode 100644 index 000000000000..38adf7998868 --- /dev/null +++ b/tests/neg-custom-args/captures/nestedclass.scala @@ -0,0 +1,15 @@ +class CC +type Cap = {*} CC + +abstract class C: + def head: String + +def test(cap1: Cap, cap2: Cap) = + def f(x: String): String = if cap1 == cap1 then "" else "a" + def g(x: String): String = if cap2 == cap2 then "" else "a" + + val xs = + class Cimpl extends C: + def head = f("") + new Cimpl + val xsc: C = xs // error diff --git a/tests/neg-custom-args/captures/override-boxed.scala b/tests/neg-custom-args/captures/override-boxed.scala new file mode 100644 index 000000000000..720b50732f61 --- /dev/null +++ b/tests/neg-custom-args/captures/override-boxed.scala @@ -0,0 +1,7 @@ +class A + +def test(x: {*} Any) = + abstract class Getter: + def get(): {x} A + class PolyGetter[T <: {x} A] extends Getter: + override def get(): T = ??? // error diff --git a/tests/neg-custom-args/captures/real-try.check b/tests/neg-custom-args/captures/real-try.check new file mode 100644 index 000000000000..9745470f219c --- /dev/null +++ b/tests/neg-custom-args/captures/real-try.check @@ -0,0 +1,23 @@ +-- Error: tests/neg-custom-args/captures/real-try.scala:12:2 ----------------------------------------------------------- +12 | try // error + | ^ + | The expression's type {*} () -> Unit is not allowed to capture the root capability `*`. + | This usually means that a capability persists longer than its allowed lifetime. +13 | () => foo(1) +14 | catch +15 | case _: Ex1 => ??? +16 | case _: Ex2 => ??? +-- Error: tests/neg-custom-args/captures/real-try.scala:18:2 ----------------------------------------------------------- +18 | try // error + | ^ + | The expression's type {*} () -> ? Cell[Unit] is not allowed to capture the root capability `*`. + | This usually means that a capability persists longer than its allowed lifetime. +19 | () => Cell(foo(1)) +20 | catch +21 | case _: Ex1 => ??? +22 | case _: Ex2 => ??? +-- Error: tests/neg-custom-args/captures/real-try.scala:30:4 ----------------------------------------------------------- +30 | b.x // error + | ^^^ + | The expression's type box {*} () -> Unit is not allowed to capture the root capability `*`. + | This usually means that a capability persists longer than its allowed lifetime. diff --git a/tests/neg-custom-args/captures/real-try.scala b/tests/neg-custom-args/captures/real-try.scala new file mode 100644 index 000000000000..94e1eafd9af2 --- /dev/null +++ b/tests/neg-custom-args/captures/real-try.scala @@ -0,0 +1,30 @@ +import language.experimental.saferExceptions + +class Ex1 extends Exception("Ex1") +class Ex2 extends Exception("Ex2") + +def foo(i: Int): (CanThrow[Ex1], CanThrow[Ex2]) ?-> Unit = + if i > 0 then throw new Ex1 else throw new Ex2 + +class Cell[+T](val x: T) + +def test() = + try // error + () => foo(1) + catch + case _: Ex1 => ??? + case _: Ex2 => ??? + + try // error + () => Cell(foo(1)) + catch + case _: Ex1 => ??? + case _: Ex2 => ??? + + val b = try // ok here, but error on use + Cell(() => foo(1))//: Cell[box {ev} () => Unit] <: Cell[box {*} () => Unit] + catch + case _: Ex1 => ??? + case _: Ex2 => ??? + + b.x // error diff --git a/tests/neg-custom-args/captures/stack-alloc.scala b/tests/neg-custom-args/captures/stack-alloc.scala new file mode 100644 index 000000000000..b646c0736f2c --- /dev/null +++ b/tests/neg-custom-args/captures/stack-alloc.scala @@ -0,0 +1,18 @@ +import scala.collection.mutable + +class Pooled + +val stack = mutable.ArrayBuffer[Pooled]() +var nextFree = 0 + +def withFreshPooled[T](op: ({*} Pooled) => T): T = + if nextFree >= stack.size then stack.append(new Pooled) + val pooled = stack(nextFree) + nextFree = nextFree + 1 + val ret = op(pooled) + nextFree = nextFree - 1 + ret + +def test() = + val pooledClosure = withFreshPooled(pooled => () => pooled.toString) // error + pooledClosure() \ No newline at end of file diff --git a/tests/neg-custom-args/captures/try.check b/tests/neg-custom-args/captures/try.check new file mode 100644 index 000000000000..30ebb910d34d --- /dev/null +++ b/tests/neg-custom-args/captures/try.check @@ -0,0 +1,42 @@ +-- [E007] Type Mismatch Error: tests/neg-custom-args/captures/try.scala:23:49 ------------------------------------------ +23 | val a = handle[Exception, CanThrow[Exception]] { // error + | ^ + | Found: ? ({*} CT[Exception]) -> {*} CT[? >: ? Exception <: ? Exception] + | Required: CanThrow[Exception] => box {*} CT[Exception] +24 | (x: CanThrow[Exception]) => x +25 | }{ + | + | longer explanation available when compiling with `-explain` +-- [E007] Type Mismatch Error: tests/neg-custom-args/captures/try.scala:29:43 ------------------------------------------ +29 | val b = handle[Exception, () -> Nothing] { // error + | ^ + | Found: ? (x: {*} CT[Exception]) -> {x} () -> ? Nothing + | Required: CanThrow[Exception] => () -> Nothing +30 | (x: CanThrow[Exception]) => () => raise(new Exception)(using x) +31 | } { + | + | longer explanation available when compiling with `-explain` +-- Error: tests/neg-custom-args/captures/try.scala:40:4 ---------------------------------------------------------------- +35 | val xx = handle { +36 | (x: CanThrow[Exception]) => +37 | () => +38 | raise(new Exception)(using x) +39 | 22 +40 | } { // error + | ^ + | The expression's type box {*} () -> Int is not allowed to capture the root capability `*`. + | This usually means that a capability persists longer than its allowed lifetime. +41 | (ex: Exception) => () => 22 +42 | } +-- Error: tests/neg-custom-args/captures/try.scala:52:2 ---------------------------------------------------------------- +47 |val global = handle { +48 | (x: CanThrow[Exception]) => +49 | () => +50 | raise(new Exception)(using x) +51 | 22 +52 |} { // error + | ^ + | The expression's type box {*} () -> Int is not allowed to capture the root capability `*`. + | This usually means that a capability persists longer than its allowed lifetime. +53 | (ex: Exception) => () => 22 +54 |} diff --git a/tests/neg-custom-args/captures/try.scala b/tests/neg-custom-args/captures/try.scala new file mode 100644 index 000000000000..35c7ea4829f2 --- /dev/null +++ b/tests/neg-custom-args/captures/try.scala @@ -0,0 +1,54 @@ +import annotation.retains +import language.experimental.erasedDefinitions + +class CT[E <: Exception] +type CanThrow[E <: Exception] = CT[E] @retains(*) +type Top = Any @retains(*) + +infix type throws[R, E <: Exception] = (erased CanThrow[E]) ?=> R + +class Fail extends Exception + +def raise[E <: Exception](e: E): Nothing throws E = throw e + +def foo(x: Boolean): Int throws Fail = + if x then 1 else raise(Fail()) + +def handle[E <: Exception, R <: Top](op: CanThrow[E] => R)(handler: E => R): R = + val x: CanThrow[E] = ??? + try op(x) + catch case ex: E => handler(ex) + +def test = + val a = handle[Exception, CanThrow[Exception]] { // error + (x: CanThrow[Exception]) => x + }{ + (ex: Exception) => ??? + } + + val b = handle[Exception, () -> Nothing] { // error + (x: CanThrow[Exception]) => () => raise(new Exception)(using x) + } { + (ex: Exception) => ??? + } + + val xx = handle { + (x: CanThrow[Exception]) => + () => + raise(new Exception)(using x) + 22 + } { // error + (ex: Exception) => () => 22 + } + val yy = xx :: Nil + yy // OK + + +val global = handle { + (x: CanThrow[Exception]) => + () => + raise(new Exception)(using x) + 22 +} { // error + (ex: Exception) => () => 22 +} \ No newline at end of file diff --git a/tests/neg-custom-args/captures/try3.scala b/tests/neg-custom-args/captures/try3.scala new file mode 100644 index 000000000000..8c5bc18bf3be --- /dev/null +++ b/tests/neg-custom-args/captures/try3.scala @@ -0,0 +1,27 @@ +import java.io.IOException + +class CT[E] +type CanThrow[E] = {*} CT[E] +type Top = {*} Any + +def handle[E <: Exception, T <: Top](op: CanThrow[E] ?=> T)(handler: E => T): T = + val x: CanThrow[E] = ??? + try op(using x) + catch case ex: E => handler(ex) + +def raise[E <: Exception](ex: E)(using CanThrow[E]): Nothing = + throw ex + +@main def Test: Int = + def f(a: Boolean) = + handle { + if !a then raise(IOException()) + (b: Boolean) => + if !b then raise(IOException()) + 0 + } { // error + ex => (b: Boolean) => -1 + } + val g = f(true) + g(false) // would raise an uncaught exception + f(true)(false) // would raise an uncaught exception diff --git a/tests/neg-custom-args/captures/unbox.scala b/tests/neg-custom-args/captures/unbox.scala new file mode 100644 index 000000000000..c615cf1d9176 --- /dev/null +++ b/tests/neg-custom-args/captures/unbox.scala @@ -0,0 +1,5 @@ +type Proc = {*} () => Unit + +val xs: List[Proc] = ??? + +val x = xs.head // error diff --git a/tests/neg-custom-args/captures/usingLogFile.check b/tests/neg-custom-args/captures/usingLogFile.check new file mode 100644 index 000000000000..03c413cbb3ef --- /dev/null +++ b/tests/neg-custom-args/captures/usingLogFile.check @@ -0,0 +1,35 @@ +-- Error: tests/neg-custom-args/captures/usingLogFile.scala:23:27 ------------------------------------------------------ +23 | val later = usingLogFile { f => () => f.write(0) } // error + | ^^^^^^^^^^^^^^^^^^^^^^^^^ + | {f, *} () -> Unit cannot be box-converted to box ? () -> Unit + | since one of their capture sets contains the root capability `*` +-- Error: tests/neg-custom-args/captures/usingLogFile.scala:29:9 ------------------------------------------------------- +29 | later2.x() // error + | ^^^^^^^^ + | The expression's type box {*} () -> Unit is not allowed to capture the root capability `*`. + | This usually means that a capability persists longer than its allowed lifetime. +-- Error: tests/neg-custom-args/captures/usingLogFile.scala:31:6 ------------------------------------------------------- +31 | var later3: () => Unit = () => () // error + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + | The mutable variable's type box {*} () -> Unit is not allowed to capture the root capability `*`. + | This usually means that a capability persists longer than its allowed lifetime. +-- Error: tests/neg-custom-args/captures/usingLogFile.scala:37:9 ------------------------------------------------------- +37 | later4.x() // error + | ^^^^^^^^ + | The expression's type box {*} () -> Unit is not allowed to capture the root capability `*`. + | This usually means that a capability persists longer than its allowed lifetime. +-- Error: tests/neg-custom-args/captures/usingLogFile.scala:47:27 ------------------------------------------------------ +47 | val later = usingLogFile { f => () => f.write(0) } // error + | ^^^^^^^^^^^^^^^^^^^^^^^^^ + | {f, *} () -> Unit cannot be box-converted to box ? () -> Unit + | since one of their capture sets contains the root capability `*` +-- Error: tests/neg-custom-args/captures/usingLogFile.scala:62:33 ------------------------------------------------------ +62 | val later = usingFile("out", f => (y: Int) => xs.foreach(x => f.write(x + y))) // error + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + | {f, *} (x$0: Int) -> Unit cannot be box-converted to box ? (x$0: Int) -> Unit + | since one of their capture sets contains the root capability `*` +-- Error: tests/neg-custom-args/captures/usingLogFile.scala:71:37 ------------------------------------------------------ +71 | val later = usingFile("logfile", usingLogger(_, l => () => l.log("test"))) // error + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + | {_$1, *} () -> Unit cannot be box-converted to box ? () -> Unit + | since one of their capture sets contains the root capability `*` diff --git a/tests/neg-custom-args/captures/usingLogFile.scala b/tests/neg-custom-args/captures/usingLogFile.scala new file mode 100644 index 000000000000..fb35b673d46e --- /dev/null +++ b/tests/neg-custom-args/captures/usingLogFile.scala @@ -0,0 +1,72 @@ +import java.io.* +import annotation.capability + +object Test1: + + def usingLogFile[T](op: FileOutputStream => T): T = + val logFile = FileOutputStream("log") + val result = op(logFile) + logFile.close() + result + + val later = usingLogFile { f => () => f.write(0) } + later() + +object Test2: + + def usingLogFile[T](op: ({*} FileOutputStream) => T): T = + val logFile = FileOutputStream("log") + val result = op(logFile) + logFile.close() + result + + val later = usingLogFile { f => () => f.write(0) } // error + later() + + class Cell[+T](val x: T) + + private val later2 = usingLogFile { f => Cell(() => f.write(0)) } + later2.x() // error + + var later3: () => Unit = () => () // error + usingLogFile { f => later3 = () => f.write(0) } + later3() + + var later4: Cell[() => Unit] = Cell(() => ()) + usingLogFile { f => later4 = Cell(() => f.write(0)) } + later4.x() // error + +object Test3: + + def usingLogFile[T](op: ({*} FileOutputStream) => T) = + val logFile = FileOutputStream("log") + val result = op(logFile) + logFile.close() + result + + val later = usingLogFile { f => () => f.write(0) } // error + +object Test4: + class Logger(f: {*} OutputStream): + def log(msg: String): Unit = ??? + + def usingFile[T](name: String, op: ({*} OutputStream) => T): T = + val f = new FileOutputStream(name) + val result = op(f) + f.close() + result + + val xs: List[Int] = ??? + def good = usingFile("out", f => xs.foreach(x => f.write(x))) + def fail = + val later = usingFile("out", f => (y: Int) => xs.foreach(x => f.write(x + y))) // error + later(1) + + + def usingLogger[T](f: {*} OutputStream, op: ({f} Logger) => T): T = + val logger = Logger(f) + op(logger) + + def test = + val later = usingFile("logfile", usingLogger(_, l => () => l.log("test"))) // error + later() diff --git a/tests/neg-custom-args/captures/vars.check b/tests/neg-custom-args/captures/vars.check new file mode 100644 index 000000000000..b5a01558cd2c --- /dev/null +++ b/tests/neg-custom-args/captures/vars.check @@ -0,0 +1,25 @@ +-- [E007] Type Mismatch Error: tests/neg-custom-args/captures/vars.scala:11:24 ----------------------------------------- +11 | val z2c: () -> Unit = z2 // error + | ^^ + | Found: (z2 : {x, cap1} () -> Unit) + | Required: () -> Unit + | + | longer explanation available when compiling with `-explain` +-- Error: tests/neg-custom-args/captures/vars.scala:13:6 --------------------------------------------------------------- +13 | var a: String => String = f // error + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^ + | The mutable variable's type box {*} String -> String is not allowed to capture the root capability `*`. + | This usually means that a capability persists longer than its allowed lifetime. +-- Error: tests/neg-custom-args/captures/vars.scala:15:4 --------------------------------------------------------------- +15 | b.head // error + | ^^^^^^ + | The expression's type box {*} String -> String is not allowed to capture the root capability `*`. + | This usually means that a capability persists longer than its allowed lifetime. +-- Error: tests/neg-custom-args/captures/vars.scala:30:8 --------------------------------------------------------------- +30 | local { cap3 => // error + | ^ + | The expression's type box {*} (x$0: ? String) -> ? String is not allowed to capture the root capability `*`. + | This usually means that a capability persists longer than its allowed lifetime. +31 | def g(x: String): String = if cap3 == cap3 then "" else "a" +32 | g +33 | } diff --git a/tests/neg-custom-args/captures/vars.scala b/tests/neg-custom-args/captures/vars.scala new file mode 100644 index 000000000000..5e413b7ea3fb --- /dev/null +++ b/tests/neg-custom-args/captures/vars.scala @@ -0,0 +1,40 @@ +class CC +type Cap = {*} CC + +def test(cap1: Cap, cap2: Cap) = + def f(x: String): String = if cap1 == cap1 then "" else "a" + var x = f + val y = x + val z = () => if x("") == "" then "a" else "b" + val zc: {cap1} () -> String = z + val z2 = () => { x = identity } + val z2c: () -> Unit = z2 // error + + var a: String => String = f // error + var b: List[String => String] = Nil // was error, now OK + b.head // error + + def scope = + val cap3: Cap = CC() + def g(x: String): String = if cap3 == cap3 then "" else "a" + a = g + b = List(g) + val gc = g + g + + val s = scope + val sc: String => String = scope + + def local[T](op: Cap -> T): T = op(CC()) + + local { cap3 => // error + def g(x: String): String = if cap3 == cap3 then "" else "a" + g + } + + class Ref: + var elem: {cap1} String -> String = null + + val r = Ref() + r.elem = f + val fc = r.elem diff --git a/tests/neg/cc-only-defs.scala b/tests/neg/cc-only-defs.scala new file mode 100644 index 000000000000..a9b480f9f590 --- /dev/null +++ b/tests/neg/cc-only-defs.scala @@ -0,0 +1,11 @@ +trait Test { + + val x: Int -> Int // error + val y: Int ?-> Int // error + + val z: *.type // error + + val b: ImpureFunction1[Int, Int] // error + + val a: {z} String // error +} // error diff --git a/tests/neg/i2887b.check b/tests/neg/i2887b.check index f5eb5c26dbaf..7b85d1a0223b 100644 --- a/tests/neg/i2887b.check +++ b/tests/neg/i2887b.check @@ -4,6 +4,7 @@ | Recursion limit exceeded. | Maybe there is an illegal cyclic reference? | If that's not the case, you could also try to increase the stacksize using the -Xss JVM option. + | For the unprocessed stack trace, compile with -Yno-decode-stacktraces. | A recurring operation is (inner to outer): | | try to instantiate Z[Z] diff --git a/tests/neg/i6635a.scala b/tests/neg/i6635a.scala new file mode 100644 index 000000000000..a79ea4e7c818 --- /dev/null +++ b/tests/neg/i6635a.scala @@ -0,0 +1,19 @@ +object Test { + abstract class ExprBase { s => + type A + } + + abstract class Lit extends ExprBase { s => + type A = Int + val n: A + } + + // It would be nice if the following could typecheck. We'd need to apply + // a reasoning like this: + // + // Since there is an argument `e2` of type `Lit & e1.type`, it follows that + // e1.type == e2.type Hence, e1.A == e2.A == Int. This looks similar + // to techniques used in GADTs. + // + def castTestFail2a(e1: ExprBase)(e2: Lit & e1.type)(x: e1.A): Int = x // error: Found: (x : e1.A) Required: Int +} diff --git a/tests/neg/polymorphic-functions1.check b/tests/neg/polymorphic-functions1.check new file mode 100644 index 000000000000..7374075de072 --- /dev/null +++ b/tests/neg/polymorphic-functions1.check @@ -0,0 +1,7 @@ +-- [E007] Type Mismatch Error: tests/neg/polymorphic-functions1.scala:1:53 --------------------------------------------- +1 |val f: [T] => (x: T) => x.type = [T] => (x: Int) => x // error + | ^ + | Found: [T] => (x: Int) => Int + | Required: [T] => (x: T) => x.type + | + | longer explanation available when compiling with `-explain` diff --git a/tests/neg/polymorphic-functions1.scala b/tests/neg/polymorphic-functions1.scala new file mode 100644 index 000000000000..de887f3b8c50 --- /dev/null +++ b/tests/neg/polymorphic-functions1.scala @@ -0,0 +1 @@ +val f: [T] => (x: T) => x.type = [T] => (x: Int) => x // error diff --git a/tests/pending/pos/i14575.scala b/tests/pending/pos/i14575.scala new file mode 100644 index 000000000000..ebc16018024a --- /dev/null +++ b/tests/pending/pos/i14575.scala @@ -0,0 +1,15 @@ + +class ann(xs: Any) extends annotation.StaticAnnotation + +class C + +class D: + def m: C = ??? + +trait Ops extends Any { + def m: C @ann(this) +} + +class Ops1(s: String) extends AnyVal with Ops: + def a = new D + def m = a.m diff --git a/tests/pos-custom-args/bounded1.scala b/tests/pos-custom-args/bounded1.scala new file mode 100644 index 000000000000..5fb7f0da904b --- /dev/null +++ b/tests/pos-custom-args/bounded1.scala @@ -0,0 +1,27 @@ +// To be revisited +class CC +type Cap = {*} CC + +def test(c: Cap) = + class B[X <: {c} Object](x: X): + def elem = x + def lateElem = () => x + + def f(x: Int): Int = if c == c then x else 0 + val b = new B(f) + val r1 = b.elem + val r1c: {c} Int -> Int = r1 + val r2 = b.lateElem + val r2c: () -> {c} Int -> Int = r2 // was error now OK + +def test2(c: Cap) = + class B[X <: {*} Any](x: X): + def elem = x + def lateElem = () => x + + def f(x: Int): Int = if c == c then x else 0 + val b = new B(f) + val r1 = b.elem + val r1c: {c} Int -> Int = r1 + val r2 = b.lateElem + val r2c: () -> {c} Int -> Int = r2 // was error now OK \ No newline at end of file diff --git a/tests/pos-custom-args/captures/bounded.scala b/tests/pos-custom-args/captures/bounded.scala new file mode 100644 index 000000000000..85c1a67387b5 --- /dev/null +++ b/tests/pos-custom-args/captures/bounded.scala @@ -0,0 +1,14 @@ +class CC +type Cap = {*} CC + +def test(c: Cap) = + class B[X <: {c} Object](x: X): + def elem = x + def lateElem = () => x + + def f(x: Int): Int = if c == c then x else 0 + val b = new B(f) + val r1 = b.elem + val r1c: {c} Int -> Int = r1 + val r2 = b.lateElem + val r2c: {c} () -> {c} Int -> Int = r2 \ No newline at end of file diff --git a/tests/pos-custom-args/captures/boxed1.scala b/tests/pos-custom-args/captures/boxed1.scala new file mode 100644 index 000000000000..ba198335f51d --- /dev/null +++ b/tests/pos-custom-args/captures/boxed1.scala @@ -0,0 +1,11 @@ +class Box[T](val x: T) + +@annotation.capability class Cap + +def foo(x: => Int): Unit = () + +def test(c: Cap) = + val f = () => { c; 1 } + val _: {c} () -> Int = f + val g = () => Box(f) + val _: () -> Box[{f} () -> Int] = g diff --git a/tests/pos-custom-args/captures/boxmap-paper.scala b/tests/pos-custom-args/captures/boxmap-paper.scala new file mode 100644 index 000000000000..aff4c38e1b9d --- /dev/null +++ b/tests/pos-custom-args/captures/boxmap-paper.scala @@ -0,0 +1,37 @@ + +type Cell[+T] = [K] -> (T => K) -> K + +def cell[T](x: T): Cell[T] = + [K] => (k: T => K) => k(x) + +def get[T](c: Cell[T]): T = c[T](identity) + +def map[A, B](c: Cell[A])(f: A => B): Cell[B] + = c[Cell[B]]((x: A) => cell(f(x))) + +def pureMap[A, B](c: Cell[A])(f: A -> B): Cell[B] + = c[Cell[B]]((x: A) => cell(f(x))) + +def lazyMap[A, B](c: Cell[A])(f: A => B): {f} () -> Cell[B] + = () => c[Cell[B]]((x: A) => cell(f(x))) + +trait IO: + def print(s: String): Unit + +def test(io: {*} IO) = + + val loggedOne: {io} () -> Int = () => { io.print("1"); 1 } + + val c: Cell[{io} () -> Int] + = cell[{io} () -> Int](loggedOne) + + val g = (f: {io} () -> Int) => + val x = f(); io.print(" + ") + val y = f(); io.print(s" = ${x + y}") + + val r = lazyMap[{io} () -> Int, Unit](c)(f => g(f)) + val r2 = lazyMap[{io} () -> Int, Unit](c)(g) + val r3 = lazyMap(c)(g) + val _ = r() + val _ = r2() + val _ = r3() diff --git a/tests/pos-custom-args/captures/boxmap.scala b/tests/pos-custom-args/captures/boxmap.scala new file mode 100644 index 000000000000..5642763b5511 --- /dev/null +++ b/tests/pos-custom-args/captures/boxmap.scala @@ -0,0 +1,19 @@ +import annotation.retains +type Top = Any @retains(*) + +type Box[+T <: Top] = ([K <: Top] -> (T => K) -> K) + +def box[T <: Top](x: T): Box[T] = + [K <: Top] => (k: T => K) => k(x) + +def map[A <: Top, B <: Top](b: Box[A])(f: A => B): Box[B] = + b[Box[B]]((x: A) => box(f(x))) + +def lazymap[A <: Top, B <: Top](b: Box[A])(f: A => B): (() -> Box[B]) = + () => b[Box[B]]((x: A) => box(f(x))) + +def test[A <: Top, B <: Top] = + def lazymap[A <: Top, B <: Top](b: Box[A])(f: A => B) = + () => b[Box[B]]((x: A) => box(f(x))) + val x: (b: Box[A]) -> (f: A => B) -> (() -> Box[B]) = lazymap[A, B] + () diff --git a/tests/pos-custom-args/captures/byname.scala b/tests/pos-custom-args/captures/byname.scala new file mode 100644 index 000000000000..35b8876d0058 --- /dev/null +++ b/tests/pos-custom-args/captures/byname.scala @@ -0,0 +1,13 @@ +import annotation.retainsByName +class CC +type Cap = {*} CC + +class I + +def test(cap1: Cap, cap2: Cap): {cap1} I = + def f() = if cap1 == cap1 then I() else I() + def h(x: {cap1}-> I) = x + h(f()) // OK + def hh(x: -> I @retainsByName(cap1)) = x + h(f()) + diff --git a/tests/pos-custom-args/captures/capt-capability.scala b/tests/pos-custom-args/captures/capt-capability.scala new file mode 100644 index 000000000000..4dbd6e32f2a4 --- /dev/null +++ b/tests/pos-custom-args/captures/capt-capability.scala @@ -0,0 +1,29 @@ +import annotation.capability + +@capability class Cap +def f1(c: Cap): {c} () -> c.type = () => c // ok + +def f2: Int = + val g: Boolean => Int = ??? + val x = g(true) + x + +def f3: Int = + def g: Boolean => Int = ??? + def h = g + val x = g.apply(true) + x + +def foo() = + val x: Cap = ??? + val y: Cap = x + val x2: {x} () -> Cap = ??? + val y2: {x} () -> Cap = x2 + + val z1: () => Cap = f1(x) + def h[X](a: X)(b: X) = a + + val z2 = + if x == null then () => x else () => Cap() + val _ = x + diff --git a/tests/pos-custom-args/captures/capt-depfun.scala b/tests/pos-custom-args/captures/capt-depfun.scala new file mode 100644 index 000000000000..861f4a0d1c14 --- /dev/null +++ b/tests/pos-custom-args/captures/capt-depfun.scala @@ -0,0 +1,21 @@ +import annotation.retains +class C +type Cap = C @retains(*) + +type T = (x: Cap) -> String @retains(x) + +type ID[X] = X + +val aa: ((x: Cap) -> String @retains(x)) = (x: Cap) => "" + +def f(y: Cap, z: Cap): String @retains(*) = + val a: ((x: Cap) -> String @retains(x)) = (x: Cap) => "" + val b = a(y) + val c: String @retains(y) = b + def g(): C @retains(y, z) = ??? + val d = a(g()) + + val ac: ((x: Cap) -> ID[String @retains(x) -> String @retains(x)]) = ??? + val bc: (({y} String) -> {y} String) = ac(y) + val dc: (String -> {y, z} String) = ac(g()) + c diff --git a/tests/pos-custom-args/captures/capt-depfun2.scala b/tests/pos-custom-args/captures/capt-depfun2.scala new file mode 100644 index 000000000000..837d143d5141 --- /dev/null +++ b/tests/pos-custom-args/captures/capt-depfun2.scala @@ -0,0 +1,9 @@ +import annotation.retains +class C +type Cap = C @retains(*) + +def f(y: Cap, z: Cap) = + def g(): C @retains(y, z) = ??? + val ac: ((x: Cap) -> Array[String @retains(x)]) = ??? + val dc: Array[? >: String <: {y, z} String] = ac(g()) // needs to be inferred + val ec = ac(y) diff --git a/tests/pos-custom-args/captures/capt-separate/Lib_1.scala b/tests/pos-custom-args/captures/capt-separate/Lib_1.scala new file mode 100644 index 000000000000..c620ebca3631 --- /dev/null +++ b/tests/pos-custom-args/captures/capt-separate/Lib_1.scala @@ -0,0 +1,6 @@ +object Lib: + extension [A](xs: Seq[A]) + def mapp[B](f: A => B): Seq[B] = + xs.map(f.asInstanceOf[A -> B]) + + diff --git a/tests/pos-custom-args/captures/capt-separate/Test_2.scala b/tests/pos-custom-args/captures/capt-separate/Test_2.scala new file mode 100644 index 000000000000..1a3b5bbd8571 --- /dev/null +++ b/tests/pos-custom-args/captures/capt-separate/Test_2.scala @@ -0,0 +1,15 @@ +import language.experimental.saferExceptions +import Lib.* + +class LimitExceeded extends Exception + +val limit = 10e9 + +def f(x: Double): Double throws LimitExceeded = + if x < limit then x * x else throw LimitExceeded() + +@main def test(xs: Double*) = + try println(xs.mapp(f).sum) + catch case ex: LimitExceeded => println("too large") + + diff --git a/tests/pos-custom-args/captures/capt-test.scala b/tests/pos-custom-args/captures/capt-test.scala new file mode 100644 index 000000000000..6ee0d2a4d9f4 --- /dev/null +++ b/tests/pos-custom-args/captures/capt-test.scala @@ -0,0 +1,35 @@ +abstract class LIST[+T]: + def isEmpty: Boolean + def head: T + def tail: LIST[T] + def map[U](f: T => U): LIST[U] = + if isEmpty then NIL + else CONS(f(head), tail.map(f)) + +class CONS[+T](x: T, xs: LIST[T]) extends LIST[T]: + def isEmpty = false + def head = x + def tail = xs +object NIL extends LIST[Nothing]: + def isEmpty = true + def head = ??? + def tail = ??? + +def map[A, B](f: A => B)(xs: LIST[A]): LIST[B] = + xs.map(f) + +class C +type Cap = {*} C + +def test(c: Cap, d: Cap) = + def f(x: Cap): Unit = if c == x then () + def g(x: Cap): Unit = if d == x then () + val y = f + val ys = CONS(y, NIL) + val zs = + val z = g + CONS(z, ys) + val zsc: LIST[{d, y} Cap -> Unit] = zs + + val a4 = zs.map(identity) + val a4c: LIST[{d, y} Cap -> Unit] = a4 diff --git a/tests/pos-custom-args/captures/capt0.scala b/tests/pos-custom-args/captures/capt0.scala new file mode 100644 index 000000000000..2544e8abe5f1 --- /dev/null +++ b/tests/pos-custom-args/captures/capt0.scala @@ -0,0 +1,7 @@ +object Test: + + def test() = + val x: {*} Any = "abc" + val y: Object @scala.annotation.retains(x) = ??? + val z: Object @scala.annotation.retains(x, *) = y: Object @annotation.retains(x) + diff --git a/tests/pos-custom-args/captures/capt1.scala b/tests/pos-custom-args/captures/capt1.scala new file mode 100644 index 000000000000..cc39790623d4 --- /dev/null +++ b/tests/pos-custom-args/captures/capt1.scala @@ -0,0 +1,27 @@ +class C +type Cap = {*} C +def f1(c: Cap): {c} () -> c.type = () => c // ok + +def f2: Int = + val g: {*} Boolean -> Int = ??? + val x = g(true) + x + +def f3: Int = + def g: Boolean => Int = ??? + def h = g + val x = g.apply(true) + x + +def foo(): {*} C = + val x: {*} C = ??? + val y: {x} C = x + val x2: {x} () -> C = ??? + val y2: {x} () -> {x} C = x2 + + val z1: () => Cap = f1(x) + def h[X](a: X)(b: X) = a + + val z2 = + if x == null then () => x else () => C() + x \ No newline at end of file diff --git a/tests/pos-custom-args/captures/capt2.scala b/tests/pos-custom-args/captures/capt2.scala new file mode 100644 index 000000000000..204310d21ddf --- /dev/null +++ b/tests/pos-custom-args/captures/capt2.scala @@ -0,0 +1,20 @@ +import annotation.retains +class C +type Cap = C @retains(*) + +def test1() = + val y: {*} String = "" + def x: Object @retains(y) = y + +def test2() = + val x: Cap = C() + val y = () => { x; () } + def z: (() -> Unit) @retains(x) = y + z: (() -> Unit) @retains(x) + def z2: (() -> Unit) @retains(y) = y + z2: (() -> Unit) @retains(y) + val p: {*} () -> String = () => "abc" + val q: {p} C = ??? + val _ = p: ({p} () -> String) + + diff --git a/tests/pos-custom-args/captures/caseclass.scala b/tests/pos-custom-args/captures/caseclass.scala new file mode 100644 index 000000000000..a845da181e9f --- /dev/null +++ b/tests/pos-custom-args/captures/caseclass.scala @@ -0,0 +1,34 @@ +@annotation.capability class C +object test1: + case class Ref(x: {*} String) + + def test(c: C) = + val x1 = Ref("hello") + val y = x1 match + case Ref(z) => z + val yc: String = y + +object test2: + case class Ref(x: () => Unit) + def test(c: C) = + + val pure: () -> Unit = () => () + val impure: () => Unit = pure + val mixed: {c} () -> Unit = pure + val x = Ref(impure) + val y0 = x.copy(pure) + val yc0: Ref = y0 + + val x2 = Ref(pure) + val _: Ref = x2 + val y2 = x2.copy() + val yc2: Ref = y2 + + val x3 = Ref(mixed) + val _: {c} Ref = x3 + val y3 = x3.copy() + val yc3: {c} Ref = y3 + + val y4 = y3 match + case Ref(xx) => xx + val y4c: {x3} () -> Unit = y4 diff --git a/tests/pos-custom-args/captures/cc-expand.scala b/tests/pos-custom-args/captures/cc-expand.scala new file mode 100644 index 000000000000..eba97f182385 --- /dev/null +++ b/tests/pos-custom-args/captures/cc-expand.scala @@ -0,0 +1,22 @@ +import annotation.retains +object Test: + + class A + class B + class C + class CTC + type CT = CTC @retains(*) + + def test(ct: CT, dt: CT) = + + def x0: A -> {ct} B = ??? + + def x1: A -> B @retains(ct) = ??? + def x2: A -> B -> C @retains(ct) = ??? + def x3: A -> () -> B -> C @retains(ct) = ??? + + def x4: (x: A @retains(ct)) -> B -> C = ??? + + def x5: A -> (x: B @retains(ct)) -> () -> C @retains(dt) = ??? + def x6: A -> (x: B @retains(ct)) -> (() -> C @retains(dt)) @retains(x, dt) = ??? + def x7: A -> (x: B @retains(ct)) -> (() -> C @retains(dt)) @retains(x) = ??? \ No newline at end of file diff --git a/tests/pos-custom-args/captures/cc-this.scala b/tests/pos-custom-args/captures/cc-this.scala new file mode 100644 index 000000000000..77414fa9b8c0 --- /dev/null +++ b/tests/pos-custom-args/captures/cc-this.scala @@ -0,0 +1,17 @@ +@annotation.capability class Cap + +def eff(using Cap): Unit = () + +def test(using Cap) = + + class C(val x: () => Int): + val y: {*} C = this + + def f = () => + eff + 1 + + def c1 = new C(f) + def c2 = c1 + def c3 = c2.y + val _ = c3: {*} C diff --git a/tests/pos-custom-args/captures/classes.scala b/tests/pos-custom-args/captures/classes.scala new file mode 100644 index 000000000000..f14a7e6dd84e --- /dev/null +++ b/tests/pos-custom-args/captures/classes.scala @@ -0,0 +1,35 @@ +import annotation.retains +class B +type Cap = {*} B +class C(val n: Cap): + this: {n} C => + def foo(): {n} B = n + + +def test(x: Cap, y: Cap, z: Cap) = + val c0 = C(x) + val c1: {x} C {val n: {x} B} = c0 + val d = c1.foo() + d: {x} B + + val c2 = if ??? then C(x) else C(y) + val c2a = identity(c2) + val c3: {x, y} C { val n: {x, y} B } = c2 + val d1 = c3.foo() + d1: B @retains(x, y) + + class Local: + + def this(a: Cap) = + this() + if a == z then println("?") + + val f = y + def foo = x + end Local + + val l = Local() + val l1: {x, y} Local = l + val l2 = Local(x) + val l3: {x, y, z} Local = l2 + diff --git a/tests/pos-custom-args/captures/compare-refined.scala b/tests/pos-custom-args/captures/compare-refined.scala new file mode 100644 index 000000000000..c60bfee602b3 --- /dev/null +++ b/tests/pos-custom-args/captures/compare-refined.scala @@ -0,0 +1,12 @@ +abstract class LIST[+T]: + def map[U](f: T => U): LIST[U] = ??? + +class C +type Cap = {*} C + +def test(d: Cap) = + val zsc: LIST[{d} Cap -> Unit] = ??? + val a4 = zsc.map[{d} Cap -> Unit]((x: {d} Cap -> Unit) => x) + val a5 = zsc.map[{d} Cap -> Unit](identity[{d} Cap -> Unit]) + val a6 = zsc.map(identity[{d} Cap -> Unit]) + val a7 = zsc.map(identity) diff --git a/tests/pos-custom-args/captures/curried-shorthands.scala b/tests/pos-custom-args/captures/curried-shorthands.scala new file mode 100644 index 000000000000..7c58729a3041 --- /dev/null +++ b/tests/pos-custom-args/captures/curried-shorthands.scala @@ -0,0 +1,24 @@ +object Test: + def map2(xs: List[Int])(f: Int => Int): List[Int] = xs.map(f) + val f1 = map2 + val fc1: List[Int] -> (Int => Int) -> List[Int] = f1 + + def map3(f: Int => Int)(xs: List[Int]): List[Int] = xs.map(f) + private val f2 = map3 + val fc2: (Int => Int) -> List[Int] -> List[Int] = f2 + + val f3 = (f: Int => Int) => + println(f(3)) + (xs: List[Int]) => xs.map(_ + 1) + val f3c: (Int => Int) -> {} List[Int] -> List[Int] = f3 + + class LL[A]: + def drop(n: Int): {this} LL[A] = ??? + + def test(ct: CanThrow[Exception]) = + def xs: {ct} LL[Int] = ??? + val ys = xs.drop(_) + val ysc: Int -> {ct} LL[Int] = ys + + + diff --git a/tests/pos-custom-args/captures/hk-param.scala b/tests/pos-custom-args/captures/hk-param.scala new file mode 100644 index 000000000000..b0e894d865e9 --- /dev/null +++ b/tests/pos-custom-args/captures/hk-param.scala @@ -0,0 +1,17 @@ +/** Concrete collection type: View */ +trait View[+A] extends Itable[A], ILike[A, [X] =>> {*} View[X]]: + override def fromIterable[B](c: {*} Itable[B]): {c} View[B] = ??? + +trait IPolyTransforms[+A, +C[A]] extends Any: + def fromIterable[B](coll: {*} Itable[B]): C[B] + +trait ILike[+A, +C[X] <: {*} Itable[X]] extends IPolyTransforms[A, C] + +/** Base trait for generic collections */ +trait Itable[+A] extends ItableOnce[A] with ILike[A, {*} Itable] + +/** Iterator can be used only once */ +trait ItableOnce[+A] { + this: {*} ItableOnce[A] => + def iterator: {this} Iterator[A] +} diff --git a/tests/pos-custom-args/captures/i13816.scala b/tests/pos-custom-args/captures/i13816.scala new file mode 100644 index 000000000000..235afef35f1c --- /dev/null +++ b/tests/pos-custom-args/captures/i13816.scala @@ -0,0 +1,63 @@ +import language.experimental.saferExceptions + +class Ex1 extends Exception("Ex1") +class Ex2 extends Exception("Ex2") + +def foo0(i: Int): (CanThrow[Ex1], CanThrow[Ex2]) ?-> Unit = + if i > 0 then throw new Ex1 else throw new Ex2 + +def foo01(i: Int): CanThrow[Ex1] ?-> CanThrow[Ex2] ?-> Unit = + if i > 0 then throw new Ex1 else throw new Ex2 + +def foo1(i: Int): Unit throws Ex1 throws Ex2 = + if i > 0 then throw new Ex1 else throw new Ex1 + +def foo2(i: Int): Unit throws Ex1 | Ex2 = + if i > 0 then throw new Ex1 else throw new Ex2 + +def foo3(i: Int): Unit throws (Ex1 | Ex2) = + if i > 0 then throw new Ex1 else throw new Ex2 + +def foo4(i: Int)(using CanThrow[Ex1], CanThrow[Ex2]): Unit = + if i > 0 then throw new Ex1 else throw new Ex2 + +def foo5(i: Int)(using CanThrow[Ex1])(using CanThrow[Ex2]): Unit = + if i > 0 then throw new Ex1 else throw new Ex2 + +def foo6(i: Int)(using CanThrow[Ex1 | Ex2]): Unit = + if i > 0 then throw new Ex1 else throw new Ex2 + +def foo7(i: Int)(using CanThrow[Ex1]): Unit throws Ex1 | Ex2 = + if i > 0 then throw new Ex1 else throw new Ex2 + +def foo8(i: Int)(using CanThrow[Ex2]): Unit throws Ex2 | Ex1 = + if i > 0 then throw new Ex1 else throw new Ex2 + +def test(): Unit = + try + foo1(1) + foo2(1) + foo3(1) + foo4(1) + foo5(1) + foo6(1) + foo7(1) + foo8(1) + catch + case _: Ex1 => + case _: Ex2 => + + try + try + foo1(1) + foo2(1) + foo3(1) + foo4(1) + foo5(1) + // foo6(1) // As explained in the docs this won't work until we find a way to aggregate capabilities + foo7(1) + foo8(1) + catch + case _: Ex1 => + catch + case _: Ex2 => diff --git a/tests/pos-custom-args/captures/impurefun.scala b/tests/pos-custom-args/captures/impurefun.scala new file mode 100644 index 000000000000..6e31008fe54a --- /dev/null +++ b/tests/pos-custom-args/captures/impurefun.scala @@ -0,0 +1,8 @@ +object Test: + + val f: ImpureFunction1[Int, Int] = (x: Int) => x + 1 + + val g: Int -> Int = (x: Int) => x + 1 + + val h: Int => Int = (x: Int) => x + 1 + diff --git a/tests/pos-custom-args/captures/iterators.scala b/tests/pos-custom-args/captures/iterators.scala new file mode 100644 index 000000000000..50be2012e25c --- /dev/null +++ b/tests/pos-custom-args/captures/iterators.scala @@ -0,0 +1,28 @@ +package cctest + +abstract class Iterator[T]: + thisIterator: {*} Iterator[T] => + + def hasNext: Boolean + def next: T + def map(f: {*} T => T): {f, this} Iterator[T] = new Iterator: + def hasNext = thisIterator.hasNext + def next = f(thisIterator.next) +end Iterator + +class C +type Cap = {*} C + +def map[T, U](it: {*} Iterator[T], f: {*} T => U): {it, f} Iterator[U] = new Iterator: + def hasNext = it.hasNext + def next = f(it.next) + +def test(c: Cap, d: Cap, e: Cap) = + val it = new Iterator[Int]: + private var ctr = 0 + def hasNext = ctr < 10 + def next = { ctr += 1; ctr } + + def f(x: Int): Int = if c == d then x else 10 + val it2 = it.map(f) + val it3 = map(it, f) \ No newline at end of file diff --git a/tests/pos-custom-args/captures/lazylists-exceptions.scala b/tests/pos-custom-args/captures/lazylists-exceptions.scala new file mode 100644 index 000000000000..2d4ebb245dca --- /dev/null +++ b/tests/pos-custom-args/captures/lazylists-exceptions.scala @@ -0,0 +1,89 @@ +import language.experimental.saferExceptions +import scala.compiletime.uninitialized + +trait LzyList[+A]: + def isEmpty: Boolean + def head: A + def tail: {this} LzyList[A] + +object LzyNil extends LzyList[Nothing]: + def isEmpty = true + def head = ??? + def tail = ??? + +final class LzyCons[+A](hd: A, tl: () => {*} LzyList[A]) extends LzyList[A]: + private var forced = false + private var cache: {this} LzyList[A] = uninitialized + private def force = + if !forced then { cache = tl(); forced = true } + cache + + def isEmpty = false + def head = hd + def tail: {this} LzyList[A] = force +end LzyCons + +extension [A](xs: {*} LzyList[A]) + def map[B](f: A => B): {xs, f} LzyList[B] = + if xs.isEmpty then LzyNil + else LzyCons(f(xs.head), () => xs.tail.map(f)) + + def filter(p: A => Boolean): {xs, p} LzyList[A] = + if xs.isEmpty then LzyNil + else if p(xs.head) then lazyCons(xs.head, xs.tail.filter(p)) + else xs.tail.filter(p) + + def concat(ys: {*} LzyList[A]): {xs, ys} LzyList[A] = + if xs.isEmpty then ys + else xs.head #: xs.tail.concat(ys) + + def drop(n: Int): {xs} LzyList[A] = + if n == 0 then xs else xs.tail.drop(n - 1) +end extension + +extension [A](x: A) + def #:(xs1: => {*} LzyList[A]): {xs1} LzyList[A] = + LzyCons(x, () => xs1) + +def lazyCons[A](x: A, xs1: => {*} LzyList[A]): {xs1} LzyList[A] = + LzyCons(x, () => xs1) + +def tabulate[A](n: Int)(gen: Int => A): {gen} LzyList[A] = + def recur(i: Int): {gen} LzyList[A] = + if i == n then LzyNil + else gen(i) #: recur(i + 1) + recur(0) + +class Ex1 extends Exception +class Ex2 extends Exception + +def test(using cap1: CanThrow[Ex1], cap2: CanThrow[Ex2]) = + val xs = 1 #: LzyNil + + def f(x: Int): Int throws Ex1 = + if x < 0 then throw Ex1() + x * x + + def g(x: Int): Int throws Ex1 = + if x < 0 then throw Ex1() + x * x + + def x1 = xs.map(f) + def x1c: {cap1} LzyList[Int] = x1 + + def x2 = x1.concat(xs.map(g).filter(_ > 0)) + def x2c: {cap1, cap2} LzyList[Int] = x2 + + val x3 = tabulate(10) { i => + if i > 9 then throw Ex1() + i * i + } + val x3c: {cap1} LzyList[Int] = x3 + +class LimitExceeded extends Exception + +def test2(n: Int)(using ct: CanThrow[LimitExceeded]) = + val xs = tabulate(10) { i => + if i > 9 then throw LimitExceeded() + i * i + } diff --git a/tests/pos-custom-args/captures/lazylists-mono.scala b/tests/pos-custom-args/captures/lazylists-mono.scala new file mode 100644 index 000000000000..44ab36ded6a2 --- /dev/null +++ b/tests/pos-custom-args/captures/lazylists-mono.scala @@ -0,0 +1,27 @@ +class CC +type Cap = {*} CC + +//------------------------------------------------- + +def test(E: Cap) = + + trait LazyList[+A]: + protected def contents: {E} () -> (A, {E} LazyList[A]) + def isEmpty: Boolean + def head: A = contents()._1 + def tail: {E} LazyList[A] = contents()._2 + + class LazyCons[+A](override val contents: {E} () -> (A, {E} LazyList[A])) + extends LazyList[A]: + def isEmpty: Boolean = false + + object LazyNil extends LazyList[Nothing]: + def contents: {E} () -> (Nothing, LazyList[Nothing]) = ??? + def isEmpty: Boolean = true + + extension [A](xs: {E} LazyList[A]) + def map[B](f: {E} A -> B): {E} LazyList[B] = + if xs.isEmpty then LazyNil + else + val cons = () => (f(xs.head), xs.tail.map(f)) + LazyCons(cons) diff --git a/tests/pos-custom-args/captures/lazylists.scala b/tests/pos-custom-args/captures/lazylists.scala new file mode 100644 index 000000000000..fd130c87cdea --- /dev/null +++ b/tests/pos-custom-args/captures/lazylists.scala @@ -0,0 +1,41 @@ +class CC +type Cap = {*} CC + +trait LazyList[+A]: + this: {*} LazyList[A] => + + def isEmpty: Boolean + def head: A + def tail: {this} LazyList[A] + +object LazyNil extends LazyList[Nothing]: + def isEmpty: Boolean = true + def head = ??? + def tail = ??? + +extension [A](xs: {*} LazyList[A]) + def map[B](f: A => B): {xs, f} LazyList[B] = + final class Mapped extends LazyList[B]: + this: {xs, f} Mapped => + + def isEmpty = false + def head: B = f(xs.head) + def tail: {this} LazyList[B] = xs.tail.map(f) // OK + if xs.isEmpty then LazyNil + else new Mapped + +def test(cap1: Cap, cap2: Cap) = + def f(x: String): String = if cap1 == cap1 then "" else "a" + def g(x: String): String = if cap2 == cap2 then "" else "a" + + val xs = + class Initial extends LazyList[String]: + this: {cap1} Initial => + + def isEmpty = false + def head = f("") + def tail = LazyNil + new Initial + val xsc: {cap1} LazyList[String] = xs + val ys = xs.map(g) + val ysc: {cap1, cap2} LazyList[String] = ys diff --git a/tests/pos-custom-args/captures/lazylists1.scala b/tests/pos-custom-args/captures/lazylists1.scala new file mode 100644 index 000000000000..a59e7c0da12f --- /dev/null +++ b/tests/pos-custom-args/captures/lazylists1.scala @@ -0,0 +1,39 @@ +class CC +type Cap = {*} CC + +trait LazyList[+A]: + + def isEmpty: Boolean + def head: A + def tail: {this} LazyList[A] + def concat[B >: A](other: {*} LazyList[B]): {this, other} LazyList[B] + +object LazyNil extends LazyList[Nothing]: + def isEmpty: Boolean = true + def head = ??? + def tail = ??? + def concat[B](other: {*} LazyList[B]): {other} LazyList[B] = other + +final class LazyCons[+A](x: A)(xs: () => {*} LazyList[A]) extends LazyList[A]: + def isEmpty = false + def head = x + def tail: {this} LazyList[A] = xs() + def concat[B >: A](other: {*} LazyList[B]): {this, other} LazyList[B] = + LazyCons(head)(() => tail.concat(other)) + +extension [A](xs: {*} LazyList[A]) + def map[B](f: A => B): {xs, f} LazyList[B] = + if xs.isEmpty then LazyNil + else LazyCons(f(xs.head))(() => xs.tail.map(f)) + +def test(cap1: Cap, cap2: Cap) = + def f(x: String): String = if cap1 == cap1 then "" else "a" + def g(x: String): String = if cap2 == cap2 then "" else "a" + + val xs = new LazyCons("")(() => if f("") == f("") then LazyNil else LazyNil) + val xsc: {cap1} LazyList[String] = xs + val ys = xs.map(g) + val ysc: {cap1, cap2} LazyList[String] = ys + val zs = new LazyCons("")(() => if g("") == g("") then LazyNil else LazyNil) + val as = xs.concat(zs) + val asc: {xs, zs} LazyList[String] = as diff --git a/tests/pos-custom-args/captures/lazyref.scala b/tests/pos-custom-args/captures/lazyref.scala new file mode 100644 index 000000000000..0d988dc3e17b --- /dev/null +++ b/tests/pos-custom-args/captures/lazyref.scala @@ -0,0 +1,24 @@ +@annotation.capability class Cap + +class LazyRef[T](val elem: () => T): + val get: {elem} () -> T = elem + def map[U](f: T => U): {f, this} LazyRef[U] = + new LazyRef(() => f(elem())) + +def map[A, B](ref: {*} LazyRef[A], f: A => B): {f, ref} LazyRef[B] = + new LazyRef(() => f(ref.elem())) + +def mapc[A, B]: (ref: {*} LazyRef[A], f: A => B) => {f, ref} LazyRef[B] = + (ref1, f1) => map[A, B](ref1, f1) + +def test(cap1: Cap, cap2: Cap) = + def f(x: Int) = if cap1 == cap1 then x else 0 + def g(x: Int) = if cap2 == cap2 then x else 0 + val ref1 = LazyRef(() => f(0)) + val ref1c: {cap1} LazyRef[Int] = ref1 + val ref2 = map(ref1, g) + val ref2c: {cap2, ref1} LazyRef[Int] = ref2 + val ref3 = ref1.map(g) + val ref3c: {cap2, ref1} LazyRef[Int] = ref3 + val ref4 = (if cap1 == cap2 then ref1 else ref2).map(g) + val ref4c: {cap1, cap2} LazyRef[Int] = ref4 diff --git a/tests/pos-custom-args/captures/list-encoding.scala b/tests/pos-custom-args/captures/list-encoding.scala new file mode 100644 index 000000000000..87630467023e --- /dev/null +++ b/tests/pos-custom-args/captures/list-encoding.scala @@ -0,0 +1,24 @@ +package listEncoding +import annotation.retains + +class Cap + +type Op[T, C] = + (v: T) => (s: C) => C + +type List[T] = + [C] -> (op: Op[T, C]) -> {op} (s: C) -> C + +def nil[T]: List[T] = + [C] => (op: Op[T, C]) => (s: C) => s + +def cons[T](hd: T, tl: List[T]): List[T] = + [C] => (op: Op[T, C]) => (s: C) => op(hd)(tl(op)(s)) + +def foo(c: {*} Cap) = + def f(x: String @retains(c), y: String @retains(c)) = + cons(x, cons(y, nil)) + def g(x: String @retains(c), y: Any) = + cons(x, cons(y, nil)) + def h(x: String, y: Any @retains(c)) = + cons(x, cons(y, nil)) diff --git a/tests/pos-custom-args/captures/lists.scala b/tests/pos-custom-args/captures/lists.scala new file mode 100644 index 000000000000..6389ec933b32 --- /dev/null +++ b/tests/pos-custom-args/captures/lists.scala @@ -0,0 +1,90 @@ +abstract class LIST[+T]: + def isEmpty: Boolean + def head: T + def tail: LIST[T] + def map[U](f: {*} T -> U): LIST[U] = + if isEmpty then NIL + else CONS(f(head), tail.map(f)) + +class CONS[+T](x: T, xs: LIST[T]) extends LIST[T]: + def isEmpty = false + def head = x + def tail = xs +object NIL extends LIST[Nothing]: + def isEmpty = true + def head = ??? + def tail = ??? + +def map[A, B](f: A => B)(xs: LIST[A]): LIST[B] = + xs.map(f) + +@annotation.capability class Cap + +def test(c: Cap, d: Cap, e: Cap) = + def f(x: Cap): Unit = if c == x then () + def g(x: Cap): Unit = if d == x then () + val y = f + val ys = CONS(y, NIL) + val zs = + val z = g + CONS(z, ys) + val zsc: LIST[{d, y} Cap -> Unit] = zs + val z1 = zs.head + val z1c: {y, d} Cap -> Unit = z1 + val ys1 = zs.tail + val y1 = ys1.head + + + def m1[A, B] = + (f: A => B) => (xs: LIST[A]) => xs.map(f) + + def m1c: (f: String => Int) -> {f} LIST[String] -> LIST[Int] = m1[String, Int] + + def m2 = [A, B] => + (f: A => B) => (xs: LIST[A]) => xs.map(f) + + def m2c: [A, B] -> (f: A => B) -> {f} LIST[A] -> LIST[B] = m2 + + def eff[A](x: A) = if x == e then x else x + + val eff2 = [A] => (x: A) => if x == e then x else x + + val a0 = identity[{d, y} Cap -> Unit] + val a0c: {d, y} ({d, y} Cap -> Unit) -> {d, y} Cap -> Unit = a0 + val a1 = zs.map[{d, y} Cap -> Unit](a0) + val a1c: LIST[{d, y} Cap -> Unit] = a1 + val a2 = zs.map[{d, y} Cap -> Unit](identity[{d, y} Cap -> Unit]) + val a2c: LIST[{d, y} Cap -> Unit] = a2 + val a3 = zs.map(identity[{d, y} Cap -> Unit]) + val a3c: LIST[{d, y} Cap -> Unit] = a3 + val a4 = zs.map(identity) + val a4c: LIST[{d, c} Cap -> Unit] = a4 + val a5 = map[{d, y} Cap -> Unit, {d, y} Cap -> Unit](identity)(zs) + val a5c: LIST[{d, c} Cap -> Unit] = a5 + val a6 = m1[{d, y} Cap -> Unit, {d, y} Cap -> Unit](identity)(zs) + val a6c: LIST[{d, c} Cap -> Unit] = a6 + + val b0 = eff[{d, y} Cap -> Unit] + val b0c: {e, d, y} ({d, y} Cap -> Unit) -> {d, y} Cap -> Unit = b0 + val b1 = zs.map[{d, y} Cap -> Unit](a0) + val b1c: {e} LIST[{d, y} Cap -> Unit] = b1 + val b2 = zs.map[{d, y} Cap -> Unit](eff[{d, y} Cap -> Unit]) + val b2c: {e} LIST[{d, y} Cap -> Unit] = b2 + val b3 = zs.map(eff[{d, y} Cap -> Unit]) + val b3c: {e} LIST[{d, y} Cap -> Unit] = b3 + val b4 = zs.map(eff) + val b4c: {e} LIST[{d, c} Cap -> Unit] = b4 + val b5 = map[{d, y} Cap -> Unit, {d, y} Cap -> Unit](eff)(zs) + val b5c: {e} LIST[{d, c} Cap -> Unit] = b5 + val b6 = m1[{d, y} Cap -> Unit, {d, y} Cap -> Unit](eff)(zs) + val b6c: {e} LIST[{d, c} Cap -> Unit] = b6 + + val c0 = eff2[{d, y} Cap -> Unit] + val c0c: {e, d, y} ({d, y} Cap -> Unit) -> {d, y} Cap -> Unit = c0 + val c1 = zs.map[{d, y} Cap -> Unit](a0) + val c1c: {e} LIST[{d, y} Cap -> Unit] = c1 + val c2 = zs.map[{d, y} Cap -> Unit](eff2[{d, y} Cap -> Unit]) + val c2c: {e} LIST[{d, y} Cap -> Unit] = c2 + val c3 = zs.map(eff2[{d, y} Cap -> Unit]) + val c3c: {e} LIST[{d, y} Cap -> Unit] = c3 + diff --git a/tests/pos-custom-args/captures/logger.scala b/tests/pos-custom-args/captures/logger.scala new file mode 100644 index 000000000000..e5b6c834ffe0 --- /dev/null +++ b/tests/pos-custom-args/captures/logger.scala @@ -0,0 +1,66 @@ +import annotation.capability +import language.experimental.saferExceptions + +@capability class FileSystem + +class Logger(using fs: FileSystem): + def log(s: String): Unit = ??? + +def test(using fs: FileSystem) = + val l: {fs} Logger = Logger(using fs) + l.log("hello world!") + val xs: {l} LazyList[Int] = + LazyList.from(1) + .map { i => + l.log(s"computing elem # $i") + i * i + } + +trait LazyList[+A]: + def isEmpty: Boolean + def head: A + def tail: {this} LazyList[A] + +object LazyNil extends LazyList[Nothing]: + def isEmpty: Boolean = true + def head = ??? + def tail = ??? + +final class LazyCons[+T](val x: T, val xs: () => {*} LazyList[T]) extends LazyList[T]: + def isEmpty = false + def head = x + def tail: {this} LazyList[T] = xs() +end LazyCons + +extension [A](x: A) + def #::(xs1: => {*} LazyList[A]): {xs1} LazyList[A] = + LazyCons(x, () => xs1) + +extension [A](xs: {*} LazyList[A]) + def map[B](f: A => B): {xs, f} LazyList[B] = + if xs.isEmpty then LazyNil + else f(xs.head) #:: xs.tail.map(f) + +object LazyList: + def from(start: Int): LazyList[Int] = + start #:: from(start + 1) + +class Pair[+A, +B](x: A, y: B): + def fst: A = x + def snd: B = y + +def test2(ct: CanThrow[Exception], fs: FileSystem) = + def x: {ct} Int -> String = ??? + def y: {fs} Logger = ??? + def p = Pair(x, y) + def f = () => p.fst + + +/* + val l1: {*} Int -> String = ??? + val l2: {c} Object = ??? + val pd = () => Pair(l1, l2) + val p2: Pair[{*} Int -> String, {c} Object] = pd() + val hd = () => p2.fst + +*/ \ No newline at end of file diff --git a/tests/pos-custom-args/captures/nonvariant-inf.scala b/tests/pos-custom-args/captures/nonvariant-inf.scala new file mode 100644 index 000000000000..6569f35042e8 --- /dev/null +++ b/tests/pos-custom-args/captures/nonvariant-inf.scala @@ -0,0 +1,9 @@ + +trait Iterable[+A] + +/** Base trait for instances that can construct a collection from an iterable */ +trait FromIterable { + type C[X] <: {*} Iterable[X] + def fromIterable[B](it: {*} Iterable[B]): {it} C[B] + def empty[A]: C[A] = fromIterable(??? : Iterable[A]) +} diff --git a/tests/pos-custom-args/captures/null-logger.scala b/tests/pos-custom-args/captures/null-logger.scala new file mode 100644 index 000000000000..0b32d045778c --- /dev/null +++ b/tests/pos-custom-args/captures/null-logger.scala @@ -0,0 +1,9 @@ +import annotation.capability +import annotation.constructorOnly + +@capability class FileSystem + +class NullLogger(using @constructorOnly fs: FileSystem) + +def test2(using fs: FileSystem): NullLogger = NullLogger() + diff --git a/tests/pos-custom-args/captures/pairs.scala b/tests/pos-custom-args/captures/pairs.scala new file mode 100644 index 000000000000..9c8ec003d28d --- /dev/null +++ b/tests/pos-custom-args/captures/pairs.scala @@ -0,0 +1,32 @@ + +@annotation.capability class Cap + +object Generic: + + class Pair[+A, +B](x: A, y: B): + def fst: A = x + def snd: B = y + + def test(c: Cap, d: Cap) = + def f(x: Cap): Unit = if c == x then () + def g(x: Cap): Unit = if d == x then () + val p = Pair(f, g) + val x1 = p.fst + val x1c: {c} Cap -> Unit = x1 + val y1 = p.snd + val y1c: {d} Cap -> Unit = y1 + +object Monomorphic: + + class Pair(x: Cap => Unit, y: {*} Cap -> Unit): + def fst: {x} Cap -> Unit = x + def snd: {y} Cap -> Unit = y + + def test(c: Cap, d: Cap) = + def f(x: Cap): Unit = if c == x then () + def g(x: Cap): Unit = if d == x then () + val p = Pair(f, g) + val x1 = p.fst + val x1c: {c} Cap -> Unit = x1 + val y1 = p.snd + val y1c: {d} Cap -> Unit = y1 diff --git a/tests/pos-custom-args/captures/saferExceptions.scala b/tests/pos-custom-args/captures/saferExceptions.scala new file mode 100644 index 000000000000..47793c7450c8 --- /dev/null +++ b/tests/pos-custom-args/captures/saferExceptions.scala @@ -0,0 +1,18 @@ +import language.experimental.saferExceptions + +class LimitExceeded extends Exception + +val limit = 10e9 + +extension [A](xs: Seq[A]) + def mapp[B](f: A => B): Seq[B] = + xs.map(f.asInstanceOf[A -> B]) + +def f(x: Double): Double throws LimitExceeded = + if x < limit then x * x else throw LimitExceeded() + +@main def test(xs: Double*) = + try println(xs.mapp(f).sum + xs.map(f).sum) + catch case ex: LimitExceeded => println("too large") + + diff --git a/tests/pos-custom-args/captures/stack-alloc.scala b/tests/pos-custom-args/captures/stack-alloc.scala new file mode 100644 index 000000000000..03b6708a3119 --- /dev/null +++ b/tests/pos-custom-args/captures/stack-alloc.scala @@ -0,0 +1,17 @@ +import scala.collection.mutable + +class Pooled + +val stack = mutable.ArrayBuffer[Pooled]() +var nextFree = 0 + +def withFreshPooled[T](op: ({*} Pooled) => T): T = + if nextFree >= stack.size then stack.append(new Pooled) + val pooled = stack(nextFree) + nextFree = nextFree + 1 + val ret = op(pooled) + nextFree = nextFree - 1 + ret + +def test() = + withFreshPooled(pooled => pooled.toString) \ No newline at end of file diff --git a/tests/pos-custom-args/captures/strictlists.scala b/tests/pos-custom-args/captures/strictlists.scala new file mode 100644 index 000000000000..cb58a09a161f --- /dev/null +++ b/tests/pos-custom-args/captures/strictlists.scala @@ -0,0 +1,41 @@ + +trait StrictList[+A]: + def isEmpty: Boolean + def head: A + def tail: StrictList[A] + +object StrictNil extends StrictList[Nothing]: + def isEmpty = true + def head = ??? + def tail = ??? + +final class StrictCons[+A](hd: A, tl: StrictList[A]) extends StrictList[A]: + def isEmpty = false + def head = hd + def tail: StrictList[A] = tl +end StrictCons + +extension [A](xs: StrictList[A]) + def map[B](f: A => B): StrictList[B] = + if xs.isEmpty then StrictNil + else StrictCons(f(xs.head),xs.tail.map(f)) + + def filter(p: A => Boolean): StrictList[A] = + if xs.isEmpty then StrictNil + else if p(xs.head) then xs.head #: xs.tail.filter(p) + else xs.tail.filter(p) + + def concat(ys: StrictList[A]): StrictList[A] = + if xs.isEmpty then ys + else xs.head #: xs.tail.concat(ys) +end extension + +extension [A](x: A) + def #:(xs1: StrictList[A]): StrictList[A] = + StrictCons(x, xs1) + +def tabulate[A](n: Int)(gen: Int => A) = + def recur(i: Int): StrictList[A] = + if i == n then StrictNil + else gen(i) #: recur(i + 1) + recur(0) \ No newline at end of file diff --git a/tests/pos-custom-args/captures/try.scala b/tests/pos-custom-args/captures/try.scala new file mode 100644 index 000000000000..dbc952cad3c0 --- /dev/null +++ b/tests/pos-custom-args/captures/try.scala @@ -0,0 +1,27 @@ +import annotation.retains +import language.experimental.erasedDefinitions + +class CT[E <: Exception] +type CanThrow[E <: Exception] = CT[E] @retains(*) + +infix type throws[R, E <: Exception] = (erased CanThrow[E]) ?-> R + +class Fail extends Exception + +def raise[E <: Exception](e: E): Nothing throws E = throw e + +def foo(x: Boolean): Int throws Fail = + if x then 1 else raise(Fail()) + +def handle[E <: Exception, R](op: (erased CanThrow[E]) -> R)(handler: E -> R): R = + erased val x: CanThrow[E] = ??? : CanThrow[E] + try op(x) + catch case ex: E => handler(ex) + +val _ = handle { (erased x) => + if true then + raise(new Exception)(using x) + 22 + else + 11 + } \ No newline at end of file diff --git a/tests/pos-custom-args/captures/try3.scala b/tests/pos-custom-args/captures/try3.scala new file mode 100644 index 000000000000..b8937bec00f3 --- /dev/null +++ b/tests/pos-custom-args/captures/try3.scala @@ -0,0 +1,50 @@ +import language.experimental.erasedDefinitions +import annotation.capability +import java.io.IOException + +@annotation.capability class CanThrow[-E] + +def handle[E <: Exception, T](op: CanThrow[E] ?=> T)(handler: E => T): T = + val x: CanThrow[E] = ??? + try op(using x) + catch case ex: E => handler(ex) + +def raise[E <: Exception](ex: E)(using CanThrow[E]): Nothing = + throw ex + +def test1: Int = + def f(a: Boolean): Boolean -> CanThrow[IOException] ?-> Int = + handle { + if !a then raise(IOException()) + (b: Boolean) => (_: CanThrow[IOException]) ?=> + if !b then raise(IOException()) + 0 + } { + ex => (b: Boolean) => (_: CanThrow[IOException]) ?=> -1 + } + handle { + val g = f(true) + g(false) // can raise an exception + f(true)(false) // can raise an exception + } { + ex => -1 + } +/* +def test2: Int = + def f(a: Boolean): Boolean => CanThrow[IOException] ?=> Int = + handle { // error + if !a then raise(IOException()) + (b: Boolean) => + if !b then raise(IOException()) + 0 + } { + ex => (b: Boolean) => -1 + } + handle { + val g = f(true) + g(false) // would raise an uncaught exception + f(true)(false) // would raise an uncaught exception + } { + ex => -1 + } +*/ \ No newline at end of file diff --git a/tests/pos-custom-args/captures/vars.scala b/tests/pos-custom-args/captures/vars.scala new file mode 100644 index 000000000000..12721158a2bb --- /dev/null +++ b/tests/pos-custom-args/captures/vars.scala @@ -0,0 +1,17 @@ +@annotation.capability class Cap + +def test(cap1: Cap, cap2: Cap) = + def f(x: String): String = if cap1 == cap1 then "" else "a" + var x = f + val y = x + val z = () => if x("") == "" then "a" else "b" + val zc: {cap1} () -> String = z + val z2 = () => { x = identity } + val z2c: {cap1} () -> Unit = z2 + + class Ref: + var elem: {cap1} String -> String = null + + val r = Ref() + r.elem = f + val fc: {cap1} String -> String = r.elem diff --git a/tests/pos-deep-subtype/i4036.scala b/tests/pos-deep-subtype/i4036.scala index 1784a9189fab..08ff248caf9d 100644 --- a/tests/pos-deep-subtype/i4036.scala +++ b/tests/pos-deep-subtype/i4036.scala @@ -11,9 +11,6 @@ object A { x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x. x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x. x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x. - x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x. - x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x. - x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x. x.type 1: v. @@ -25,16 +22,10 @@ object A { x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x. x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x. x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x. - x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x. - x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x. - x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x. x.T val u = new B { type T = Int } u: u. - x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x. - x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x. - x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x. x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x. x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x. x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x. @@ -55,8 +46,5 @@ object A { T#T#T#T#T#T#T#T#T#T#T#T#T#T#T#T# T#T#T#T#T#T#T#T#T#T#T#T#T#T#T#T# T#T#T#T#T#T#T#T#T#T#T#T#T#T#T#T# - T#T#T#T#T#T#T#T#T#T#T#T#T#T#T#T# - T#T#T#T#T#T#T#T#T#T#T#T#T#T#T#T# - T#T#T#T#T#T#T#T#T#T#T#T#T#T#T#T# T#T#T#T#T#T#T#T#T#T#T#T#T#T#T#T } diff --git a/tests/pos/i12723.scala b/tests/pos/i12723.scala index d1cab3ede638..022a3a458f04 100644 --- a/tests/pos/i12723.scala +++ b/tests/pos/i12723.scala @@ -1,10 +1,10 @@ class Fun[|*|[_, _]] { - enum ->[A, B] { - case BiId[X, Y]() extends ((X |*| Y) -> (X |*| Y)) + enum -->[A, B] { + case BiId[X, Y]() extends ((X |*| Y) --> (X |*| Y)) } - def go[A, B](f: A -> B): Unit = + def go[A, B](f: A --> B): Unit = f match { - case ->.BiId() => () + case -->.BiId() => () } } diff --git a/tests/pos/i14782.scala b/tests/pos/i14782.scala new file mode 100644 index 000000000000..61a6d2117b03 --- /dev/null +++ b/tests/pos/i14782.scala @@ -0,0 +1,8 @@ +object BugExample { + val urls: List[String] = List("any_url") + val closures: List[Unit => Unit] = + urls.map(url => _ => { + println("Scraping " + url) + }) + for (closure <- closures) closure(()) +} \ No newline at end of file diff --git a/tests/pos/i6635.scala b/tests/pos/i6635.scala index dacd1ef5cd8b..406eee6251e6 100644 --- a/tests/pos/i6635.scala +++ b/tests/pos/i6635.scala @@ -27,11 +27,12 @@ object Test { def castTest5a(e1: ExprBase)(e2: LitU with e1.type)(x: e2.A): e1.A = x def castTest5b(e1: ExprBase)(e2: LitL with e1.type)(x: e2.A): e1.A = x - //fail: def castTestFail1(e1: ExprBase)(e2: Lit with e1.type)(x: e2.A): e1.A = x // this is like castTest5a/b, but with Lit instead of LitU/LitL - // the other direction never works: - def castTestFail2a(e1: ExprBase)(e2: Lit with e1.type)(x: e1.A): e2.A = x + + // The next example fails rechecking. It is repeated in i6635a.scala + // def castTestFail2a(e1: ExprBase)(e2: Lit with e1.type)(x: e1.A): e2.A = x def castTestFail2b(e1: ExprBase)(e2: LitL with e1.type)(x: e1.A): e2.A = x + def castTestFail2c(e1: ExprBase)(e2: LitU with e1.type)(x: e1.A): e2.A = x // the problem isn't about order of intersections. diff --git a/tests/pos/i6635a.scala b/tests/pos/i6635a.scala new file mode 100644 index 000000000000..9454e03e3a4a --- /dev/null +++ b/tests/pos/i6635a.scala @@ -0,0 +1,14 @@ +object Test { + abstract class ExprBase { s => + type A + } + + abstract class Lit extends ExprBase { s => + type A = Int + val n: A + } + + // Fails recheck since the result type e2.A is converted to Int to avoid + // a false dependency on e2. + def castTestFail2a(e1: ExprBase)(e2: Lit with e1.type)(x: e1.A): e2.A = x +} diff --git a/tests/pos/patmatch-in-catch.scala b/tests/pos/patmatch-in-catch.scala new file mode 100644 index 000000000000..c83a3eb78eb5 --- /dev/null +++ b/tests/pos/patmatch-in-catch.scala @@ -0,0 +1,15 @@ +sealed abstract class Action +case object MainReturn extends Action +case object ExceptionNormalExit extends Action +case class CaughtException(action: Action) extends RuntimeException + +def driver(action: Action): Action = + val result = + try action + catch case CaughtException(action) => + action match + case ExceptionNormalExit => + action + case _ => + ??? + result \ No newline at end of file diff --git a/tests/run/colltest5.check b/tests/run-custom-args/captures/colltest5.check similarity index 100% rename from tests/run/colltest5.check rename to tests/run-custom-args/captures/colltest5.check diff --git a/tests/run-custom-args/captures/colltest5/CollectionStrawManCC5_1.scala b/tests/run-custom-args/captures/colltest5/CollectionStrawManCC5_1.scala new file mode 100644 index 000000000000..796dc62bdf22 --- /dev/null +++ b/tests/run-custom-args/captures/colltest5/CollectionStrawManCC5_1.scala @@ -0,0 +1,601 @@ +package colltest5 +package strawman.collections + +import Predef.{augmentString as _, wrapString as _, *} +import scala.reflect.ClassTag +import annotation.unchecked.uncheckedVariance +import annotation.tailrec + +/** A strawman architecture for new collections. It contains some + * example collection classes and methods with the intent to expose + * some key issues. It would be good to compare this to other + * implementations of the same functionality, to get an idea of the + * strengths and weaknesses of different collection architectures. + * + * For a test file, see tests/run/CollectionTests.scala. + */ +object CollectionStrawMan5 { + + /* ------------ Base Traits -------------------------------- */ + + /** Iterator can be used only once */ + trait IterableOnce[+A] { + this: {*} IterableOnce[A] => + def iterator: {this} Iterator[A] + } + + /** Base trait for instances that can construct a collection from an iterable */ + trait FromIterable { + type C[X] <: {*} Iterable[X] + def fromIterable[B](it: {*} Iterable[B]): {it} C[B] + } + + type FromIterableOf[+CC[X] <: {*} Iterable[X]] = FromIterable { + type C[X] <: CC[X] + } + + /** Base trait for companion objects of collections */ + trait IterableFactory extends FromIterable { + def empty[A]: C[A] = fromIterable(View.Empty) + def apply[A](xs: A*): C[A] = fromIterable(View.Elems(xs*)) + } + + /** Base trait for generic collections */ + trait Iterable[+A] extends IterableOnce[A] with IterableLike[A] { + this: {*} Iterable[A] => + type C[X] <: {*} Iterable[X] + protected def coll: {this} Iterable[A] = this + def knownLength: Int = -1 + } + + /** Base trait for sequence collections */ + trait Seq[+A] extends Iterable[A] with SeqLike[A] { + this: Seq[A] => + type C[X] <: Seq[X] + def apply(i: Int): A + def length: Int + } + + trait SeqFactory extends IterableFactory { + type C[X] <: Seq[X] + def fromIterable[B](it: {*} Iterable[B]): C[B] + } + + /** Base trait for strict collections */ + trait Buildable[+A] extends Iterable[A] { + protected[this] def newBuilder: Builder[A, Repr] @uncheckedVariance + override def partition(p: A => Boolean): (Repr, Repr) = { + val l, r = newBuilder + iterator.foreach(x => (if (p(x)) l else r) += x) + (l.result, r.result) + } + // one might also override other transforms here to avoid generating + // iterators if it helps efficiency. + } + + /** Base trait for collection builders */ + trait Builder[-A, +To] { + def +=(x: A): this.type + def result: To + + def ++=(xs: {*} IterableOnce[A]): this.type = { + xs.iterator.foreach(+=) + this + } + } + + /* ------------ Operations ----------------------------------- */ + + /** Base trait for Iterable operations + * + * VarianceNote + * ============ + * + * We require that for all child classes of Iterable the variance of + * the child class and the variance of the `C` parameter passed to `IterableLike` + * are the same. We cannot express this since we lack variance polymorphism. That's + * why we have to resort at some places to write `C[A @uncheckedVariance]`. + * + */ + trait IterableLike[+A] + extends FromIterable + with IterableOps[A] + with IterablePolyTransforms[A] + with IterableMonoTransforms[A] { // sound bcs of VarianceNote + type Repr = C[A] @uncheckedVariance + protected[this] def fromLikeIterable(coll: {*} Iterable[A] @uncheckedVariance): {coll} Repr @uncheckedVariance = + fromIterable(coll) + } + + /** Base trait for Seq operations */ + trait SeqLike[+A] + extends IterableLike[A], SeqMonoTransforms[A], SeqPolyTransforms[A]: // sound bcs of VarianceNote + this: SeqLike[A] => + type C[X] <: Seq[X] + def fromIterable[B](coll: {*} Iterable[B]): C[B] + override protected[this] def fromLikeIterable(coll: {*} Iterable[A] @uncheckedVariance): Repr = + fromIterable(coll) + + trait IterableOps[+A] extends Any { + this: {*} IterableOps[A] => + def iterator: {this} Iterator[A] + def foreach(f: A => Unit): Unit = iterator.foreach(f) + def foldLeft[B](z: B)(op: (B, A) => B): B = iterator.foldLeft(z)(op) + def foldRight[B](z: B)(op: (A, B) => B): B = iterator.foldRight(z)(op) + def indexWhere(p: A => Boolean): Int = iterator.indexWhere(p) + def isEmpty: Boolean = !iterator.hasNext + def head: A = iterator.next() + def view: {this} View[A] = View.fromIterator(iterator) + } + + trait IterableMonoTransforms[+A] extends Any { + this: {*} IterableMonoTransforms[A] => + type Repr + protected def coll: {this} Iterable[A] + protected[this] def fromLikeIterable(coll: {*} Iterable[A] @uncheckedVariance): {coll} Repr + def filter(p: A => Boolean): {this, p} Repr = fromLikeIterable(View.Filter(coll, p)) + + def partition(p: A => Boolean): ({this, p} Repr, {this, p} Repr) = { + val pn = View.Partition(coll, p) + (fromLikeIterable(pn.left), fromLikeIterable(pn.right)) + } + def drop(n: Int): {this} Repr = fromLikeIterable(View.Drop(coll, n)) + + def to[C[X] <: Iterable[X]](fi: FromIterableOf[C]): {this} C[A @uncheckedVariance] = + // variance seems sound because `to` could just as well have been added + // as a decorator. We should investigate this further to be sure. + fi.fromIterable(coll) + } + + trait IterablePolyTransforms[+A] extends Any { + this: {*} IterablePolyTransforms[A] => + type C[A] + protected def coll: {this} Iterable[A] + def fromIterable[B](coll: {*} Iterable[B]): {coll} C[B] + def map[B](f: A => B): {this, f} C[B] = fromIterable(View.Map(coll, f)) + def flatMap[B](f: A => {*} IterableOnce[B]): {this, f} C[B] = fromIterable(View.FlatMap(coll, f)) + def ++[B >: A](xs: {*} IterableOnce[B]): {this, xs} C[B] = fromIterable(View.Concat(coll, xs)) + def zip[B](xs: {*} IterableOnce[B]): {this, xs} C[(A @uncheckedVariance, B)] = fromIterable(View.Zip(coll, xs)) + // sound bcs of VarianceNote + } + + trait SeqMonoTransforms[+A] extends Any, IterableMonoTransforms[A] { + this: SeqMonoTransforms[A] => + def reverse: Repr = + var xs: List[A] = Nil + var it = coll.iterator + while (it.hasNext) xs = new Cons(it.next(), xs) + fromLikeIterable(xs) + + override protected[this] def fromLikeIterable(coll: {*} Iterable[A] @uncheckedVariance): Repr + + override def filter(p: A => Boolean): Repr = fromLikeIterable(View.Filter(coll, p)) + + override def partition(p: A => Boolean): (Repr, Repr) = { + val pn = View.Partition(coll, p) + (fromLikeIterable(pn.left), fromLikeIterable(pn.right)) + } + override def drop(n: Int): Repr = fromLikeIterable(View.Drop(coll, n)) + + override def to[C[X] <: Iterable[X]](fi: FromIterableOf[C]): C[A @uncheckedVariance] = + // variance seems sound because `to` could just as well have been added + // as a decorator. We should investigate this further to be sure. + fi.fromIterable(coll) + } + + trait SeqPolyTransforms[+A] extends Any, IterablePolyTransforms[A]: + this: SeqPolyTransforms[A] => + type C[A] + override def fromIterable[B](coll: {*} Iterable[B]): C[B] + override def map[B](f: A => B): C[B] = fromIterable(View.Map(coll, f)) + override def flatMap[B](f: A => {*} IterableOnce[B]): C[B] = fromIterable(View.FlatMap(coll, f)) + override def ++[B >: A](xs: {*} IterableOnce[B]): C[B] = fromIterable(View.Concat(coll, xs)) + override def zip[B](xs: {*} IterableOnce[B]): C[(A @uncheckedVariance, B)] = fromIterable(View.Zip(coll, xs)) + + + /* --------- Concrete collection types ------------------------------- */ + + /** Concrete collection type: List */ + sealed trait List[+A] extends Seq[A] with SeqLike[A] with Buildable[A] { self => + type C[X] = List[X] + def isEmpty: Boolean + def head: A + def tail: List[A] + def iterator = new Iterator[A] { + private[this] var current = self + def hasNext = !current.isEmpty + def next() = { val r = current.head; current = current.tail; r } + } + def fromIterable[B](c: {*} Iterable[B]): List[B] = List.fromIterable(c) + def apply(i: Int): A = { + require(!isEmpty) + if (i == 0) head else tail.apply(i - 1) + } + def length: Int = + if (isEmpty) 0 else 1 + tail.length + protected[this] def newBuilder = new ListBuffer[A] @uncheckedVariance + def ++:[B >: A](prefix: List[B]): List[B] = + if (prefix.isEmpty) this + else Cons(prefix.head, prefix.tail ++: this) + override def ++[B >: A](xs: {*} IterableOnce[B]): List[B] = xs match { + case xs: List[B] => this ++: xs + case _ => fromIterable(View.Concat(this, xs)) + } + @tailrec final override def drop(n: Int) = + if (n > 0) tail.drop(n - 1) else this + } + + case class Cons[+A](x: A, private[collections] var next: List[A @uncheckedVariance]) // sound because `next` is used only locally + extends List[A] { + override def isEmpty = false + override def head = x + def tail = next + } + + case object Nil extends List[Nothing] { + override def isEmpty = true + override def head = ??? + def tail = ??? + } + + object List extends SeqFactory { + type C[X] = List[X] + def fromIterable[B](coll: {*} Iterable[B]): List[B] = coll match { + case coll: List[B] => coll + case _ => ListBuffer.fromIterable(coll).result + } + } + + /** Concrete collection type: ListBuffer */ + class ListBuffer[A] extends Seq[A] with SeqLike[A] with Builder[A, List[A]] { + type C[X] = ListBuffer[X] + private var first, last: List[A] = Nil + private var aliased = false + def iterator = first.iterator + def fromIterable[B](coll: {*} Iterable[B]): ListBuffer[B] = ListBuffer.fromIterable(coll) + def apply(i: Int) = first.apply(i) + def length = first.length + + private def copyElems(): Unit = { + val buf = ListBuffer.fromIterable(result) + first = buf.first + last = buf.last + aliased = false + } + def result = { + aliased = true + first + } + def +=(elem: A) = { + if (aliased) copyElems() + val last1 = Cons(elem, Nil) + last match { + case last: Cons[A] => last.next = last1 + case _ => first = last1 + } + last = last1 + this + } + override def toString: String = + if (first.isEmpty) "ListBuffer()" + else { + val b = new StringBuilder("ListBuffer(").append(first.head) + first.tail.foldLeft(b)(_.append(", ").append(_)).append(")").toString + } + } + + object ListBuffer extends SeqFactory { + type C[X] = ListBuffer[X] + def fromIterable[B](coll: {*} Iterable[B]): ListBuffer[B] = new ListBuffer[B] ++= coll + } + + /** Concrete collection type: ArrayBuffer */ + class ArrayBuffer[A] private (initElems: Array[AnyRef], initLength: Int) + extends Seq[A] with SeqLike[A] with Builder[A, ArrayBuffer[A]] { + this: ArrayBuffer[A] => + type C[X] = ArrayBuffer[X] + def this() = this(new Array[AnyRef](16), 0) + private var elems: Array[AnyRef] = initElems + private var start = 0 + private var end = initLength + def apply(n: Int) = elems(start + n).asInstanceOf[A] + def length = end - start + override def knownLength = length + override def view = new ArrayBufferView(elems, start, end) + def iterator = view.iterator + def fromIterable[B](it: {*} Iterable[B]): ArrayBuffer[B] = + ArrayBuffer.fromIterable(it) + def +=(elem: A): this.type = { + if (end == elems.length) { + if (start > 0) { + Array.copy(elems, start, elems, 0, length) + end -= start + start = 0 + } + else { + val newelems = new Array[AnyRef](end * 2) + Array.copy(elems, 0, newelems, 0, end) + elems = newelems + } + } + elems(end) = elem.asInstanceOf[AnyRef] + end += 1 + this + } + def result = this + def trimStart(n: Int): Unit = start += (n max 0) + override def ++[B >: A](xs: {*} IterableOnce[B]): ArrayBuffer[B] = xs match { + case xs: ArrayBuffer[B] @unchecked => + val elems = new Array[AnyRef](length + xs.length) + Array.copy(this.elems, this.start, elems, 0, this.length) + Array.copy(xs.elems, xs.start, elems, this.length, xs.length) + new ArrayBuffer(elems, elems.length) + case _ => fromIterable(View.Concat(this, xs)) + } + + override def toString = s"ArrayBuffer(${elems.slice(start, end).mkString(", ")})" + } + + object ArrayBuffer extends SeqFactory { + type C[X] = ArrayBuffer[X] + def fromIterable[B](coll: {*} Iterable[B]): ArrayBuffer[B] = + if (coll.knownLength >= 0) { + val elems = new Array[AnyRef](coll.knownLength) + val it = coll.iterator + for (i <- 0 until elems.length) elems(i) = it.next().asInstanceOf[AnyRef] + new ArrayBuffer[B](elems, elems.length): ArrayBuffer[B] + } + else { + val buf = new ArrayBuffer[B] + val it = coll.iterator + while (it.hasNext) buf += it.next() + buf: ArrayBuffer[B] + } + } + + class ArrayBufferView[A](val elems: Array[AnyRef], val start: Int, val end: Int) extends RandomAccessView[A] { + this: ArrayBufferView[A] => + def apply(n: Int) = elems(start + n).asInstanceOf[A] + } + + /** Concrete collection type: String */ + implicit class StringOps(val s: String) + extends IterableOps[Char] + with SeqMonoTransforms[Char] + with IterablePolyTransforms[Char] { + this: StringOps => + type Repr = String + type C[X] = List[X] + protected def coll = new StringView(s) + def iterator = coll.iterator + protected def fromLikeIterable(coll: {*} Iterable[Char]): String = { + val sb = new StringBuilder + for (ch <- coll) sb.append(ch) + sb.toString + } + def fromIterable[B](coll: {*} Iterable[B]): List[B] = List.fromIterable(coll) + def map(f: Char => Char): String = { + val sb = new StringBuilder + for (ch <- s) sb.append(f(ch)) + sb.toString + } + def flatMap(f: Char => String): String = { + val sb = new StringBuilder + for (ch <- s) sb.append(f(ch)) + sb.toString + } + def ++(xs: {*} IterableOnce[Char]): String = { + val sb = new StringBuilder(s) + for (ch <- xs.iterator) sb.append(ch) + sb.toString + } + def ++(xs: String): String = s + xs + } + + case class StringView(s: String) extends RandomAccessView[Char] { + val start = 0 + val end = s.length + def apply(n: Int) = s.charAt(n) + } + +/* ---------- Views -------------------------------------------------------*/ + + /** Concrete collection type: View */ + trait View[+A] extends Iterable[A] with IterableLike[A] { + this: {*} View[A] => + type C[X] = {this} View[X] + override def view: this.type = this + override def fromIterable[B](c: {*} Iterable[B]): {this, c} View[B] = { + c match { + case c: View[B] => c + case _ => View.fromIterator(c.iterator) + } + } + } + + /** View defined in terms of indexing a range */ + trait RandomAccessView[+A] extends View[A] { + def start: Int + def end: Int + def apply(i: Int): A + def iterator: Iterator[A] = new Iterator[A] { + private var current = start + def hasNext = current < end + def next(): A = { + val r = apply(current) + current += 1 + r + } + } + override def knownLength = end - start max 0 + } + + object View { + def fromIterator[A](it: => {*} Iterator[A]): {it} View[A] = new View[A]: + def iterator: {this} Iterator[A] = it + + case object Empty extends View[Nothing] { + def iterator: Iterator[Nothing] = Iterator.empty + override def knownLength = 0 + } + + case class Elems[A](xs: A*) extends View[A] { + def iterator: Iterator[A] = Iterator(xs*) + override def knownLength = xs.length + } + + case class Filter[A](val underlying: {*} Iterable[A], p: A => Boolean) extends View[A] { + this: {underlying, p} Filter[A] => + def iterator: {this} Iterator[A] = underlying.iterator.filter(p) + } + case class Partition[A](val underlying: {*} Iterable[A], p: A => Boolean) { + self: {underlying, p} Partition[A] => + + class Partitioned(expected: Boolean) extends View[A]: + this: {self} Partitioned => + def iterator: {this} Iterator[A] = + underlying.iterator.filter((x: A) => p(x) == expected) + + val left: {self} Partitioned = Partitioned(true) + val right: {self} Partitioned = Partitioned(false) + } + + case class Drop[A](underlying: {*} Iterable[A], n: Int) extends View[A] { + this: {underlying} Drop[A] => + def iterator: {this} Iterator[A] = underlying.iterator.drop(n) + override def knownLength = + if (underlying.knownLength >= 0) underlying.knownLength - n max 0 else -1 + } + + case class Map[A, B](underlying: {*} Iterable[A], f: A => B) extends View[B] { + this: {underlying, f} Map[A, B] => + def iterator: {this} Iterator[B] = underlying.iterator.map(f) + override def knownLength = underlying.knownLength + } + + case class FlatMap[A, B](underlying: {*} Iterable[A], f: A => {*} IterableOnce[B]) extends View[B] { + this: {underlying, f} FlatMap[A, B] => + def iterator: {this} Iterator[B] = underlying.iterator.flatMap(f) + } + + case class Concat[A](underlying: {*} Iterable[A], other: {*} IterableOnce[A]) extends View[A] { + this: {underlying, other} Concat[A] => + def iterator: {this} Iterator[A] = underlying.iterator ++ other + override def knownLength = other match { + case other: Iterable[_] if underlying.knownLength >= 0 && other.knownLength >= 0 => + underlying.knownLength + other.knownLength + case _ => + -1 + } + } + + case class Zip[A, B](underlying: {*} Iterable[A], other: {*} IterableOnce[B]) extends View[(A, B)] { + this: {underlying, other} Zip[A, B] => + def iterator: {this} Iterator[(A, B)] = underlying.iterator.zip(other) + override def knownLength = other match { + case other: Iterable[_] if underlying.knownLength >= 0 && other.knownLength >= 0 => + underlying.knownLength min other.knownLength + case _ => + -1 + } + } + } + +/* ---------- Iterators ---------------------------------------------------*/ + + /** A core Iterator class */ + trait Iterator[+A] extends IterableOnce[A] { self: {*} Iterator[A] => + def hasNext: Boolean + def next(): A + def iterator: this.type = this + def foldLeft[B](z: B)(op: (B, A) => B): B = + if (hasNext) foldLeft(op(z, next()))(op) else z + def foldRight[B](z: B)(op: (A, B) => B): B = + if (hasNext) op(next(), foldRight(z)(op)) else z + def foreach(f: A => Unit): Unit = + while (hasNext) f(next()) + def indexWhere(p: A => Boolean): Int = { + var i = 0 + while (hasNext) { + if (p(next())) return i + i += 1 + } + -1 + } + def filter(p: A => Boolean): {this, p} Iterator[A] = new Iterator[A] { + private var hd: A = compiletime.uninitialized + private var hdDefined: Boolean = false + + def hasNext: Boolean = hdDefined || { + while { + if (!self.hasNext) return false + hd = self.next() + !p(hd) + } do () + hdDefined = true + true + } + + def next() = + if (hasNext) { + hdDefined = false + hd + } + else Iterator.empty.next() + } + + def map[B](f: A => B): {this, f} Iterator[B] = new Iterator[B] { + def hasNext = self.hasNext + def next() = f(self.next()) + } + + def flatMap[B](f: A => {*} IterableOnce[B]): {this, f} Iterator[B] = new Iterator[B] { + private var myCurrent: {this} Iterator[B] = Iterator.empty + private def current = { + while (!myCurrent.hasNext && self.hasNext) + myCurrent = f(self.next()).iterator + myCurrent + } + def hasNext = current.hasNext + def next() = current.next() + } + def ++[B >: A](xs: {*} IterableOnce[B]): {this, xs} Iterator[B] = new Iterator[B] { + private var myCurrent: {self, xs} Iterator[B] = self + private var first = true + private def current = { + if (!myCurrent.hasNext && first) { + myCurrent = xs.iterator + first = false + } + myCurrent + } + def hasNext = current.hasNext + def next() = current.next() + } + def drop(n: Int): {this} Iterator[A] = { + var i = 0 + while (i < n && hasNext) { + next() + i += 1 + } + this + } + def zip[B](that: {*} IterableOnce[B]): {this, that} Iterator[(A, B)] = new Iterator[(A, B)] { + val thatIterator = that.iterator + def hasNext = self.hasNext && thatIterator.hasNext + def next() = (self.next(), thatIterator.next()) + } + } + + object Iterator { + val empty: Iterator[Nothing] = new Iterator[Nothing] { + def hasNext = false + def next() = throw new NoSuchElementException("next on empty iterator") + } + def apply[A](xs: A*): Iterator[A] = new RandomAccessView[A] { + val start = 0 + val end = xs.length + def apply(n: Int) = xs(n) + }.iterator + } +} diff --git a/tests/run-custom-args/captures/colltest5/Test_2.scala b/tests/run-custom-args/captures/colltest5/Test_2.scala new file mode 100644 index 000000000000..934223a9840b --- /dev/null +++ b/tests/run-custom-args/captures/colltest5/Test_2.scala @@ -0,0 +1,185 @@ +import Predef.{augmentString as _, wrapString as _, *} +import scala.reflect.ClassTag + +object Test { + import colltest5.strawman.collections.* + import CollectionStrawMan5.* + + def seqOps(xs: Seq[Int]) = { // try with {*} Seq[Int] + val strPlusInt: (String, Int) => String = _ + _ + val intPlusStr: (Int, String) => String = _ + _ + val isEven: Int => Boolean = _ % 2 == 0 + val isNonNeg: Int => Boolean = _ > 0 + val flips: Int => List[Int] = x => Cons(x, Cons(-x, Nil)) + val x1 = xs.foldLeft("")(strPlusInt) + val y1: String = x1 + val x2 = xs.foldRight("")(intPlusStr) + val y2: String = x2 + val x3 = xs.indexWhere(isEven) + val y3: Int = x3 + val x4 = xs.head + val y4: Int = x4 + val x5 = xs.to(List) + val y5: List[Int] = x5 + val (xs6, xs7) = xs.partition(isEven) + val ys6: Seq[Int] = xs6 + val ys7: Seq[Int] = xs7 + val xs8 = xs.drop(2) + val ys8: Seq[Int] = xs8 + val xs9 = xs.map(isNonNeg) + val ys9: Seq[Boolean] = xs9 + val xs10 = xs.flatMap(flips) + val ys10: Seq[Int] = xs10 + val xs11 = xs ++ xs + val ys11: Seq[Int] = xs11 + val xs12 = xs ++ Nil + val ys12: Seq[Int] = xs12 + val xs13 = Nil ++ xs + val ys13: Seq[Int] = xs13 + val xs14 = xs.++[Any](Cons("a", Nil)) // !!! + val ys14: Seq[Any] = xs14 + val xs15 = xs.zip(xs9) + val ys15: Seq[(Int, Boolean)] = xs15 + val xs16 = xs.reverse + val ys16: Seq[Int] = xs16 + println("-------") + println(x1) + println(x2) + println(x3) + println(x4) + println(x5) + println(xs6) + println(xs7) + println(xs8) + println(xs9) + println(xs10) + println(xs11) + println(xs12) + println(xs13) + println(xs14) + println(xs15) + println(xs16) + } + + def viewOps(xs: {*} View[Int]) = { + val strPlusInt: (String, Int) => String = _ + _ + val intPlusStr: (Int, String) => String = _ + _ + val isEven: Int => Boolean = _ % 2 == 0 + val isNonNeg: Int => Boolean = _ > 0 + val flips: Int => List[Int] = x => Cons(x, Cons(-x, Nil)) + val x1 = xs.foldLeft("")(strPlusInt) + val y1: String = x1 + val x2 = xs.foldRight("")(intPlusStr) + val y2: String = x2 + val x3 = xs.indexWhere(isEven) + val y3: Int = x3 + val x4 = xs.head + val y4: Int = x4 + val x5 = xs.to(List) + val y5: {x5} List[Int] = x5 + val (xs6, xs7) = xs.partition(isEven) + val ys6: {xs6, isEven} View[Int] = xs6 + val ys7: {xs7, isEven} View[Int] = xs7 + val (xs6a, xs7a) = xs.partition(_ % 2 == 0) + val ys6a: {xs6} View[Int] = xs6 + val ys7a: {xs7} View[Int] = xs7 + val xs8 = xs.drop(2) + val ys8: {xs8} View[Int] = xs8 + val xs9 = xs.map(isNonNeg) + val ys9: {xs9} View[Boolean] = xs9 + val xs10 = xs.flatMap(flips) + val ys10: {xs10} View[Int] = xs10 + val xs11 = xs ++ xs + val ys11: {xs11} View[Int] = xs11 + val xs12 = xs ++ Nil + val ys12: {xs12} View[Int] = xs12 + val xs13 = Nil ++ xs + val ys13: List[Int] = xs13 + val xs14 = xs ++ Cons("a", Nil) + val ys14: {xs14} View[Any] = xs14 + val xs15 = xs.zip(xs9) + val ys15: {xs15} View[(Int, Boolean)] = xs15 + println("-------") + println(x1) + println(x2) + println(x3) + println(x4) + println(x5) + println(xs6.to(List)) + println(xs7.to(List)) + println(xs8.to(List)) + println(xs9.to(List)) + println(xs10.to(List)) + println(xs11.to(List)) + println(xs12.to(List)) + println(xs13.to(List)) + println(xs14.to(List)) + println(xs15.to(List)) + } + + def stringOps(xs: String) = { + val x1 = xs.foldLeft("")(_ + _) + val y1: String = x1 + val x2 = xs.foldRight("")(_ + _) + val y2: String = x2 + val x3 = xs.indexWhere(_ % 2 == 0) + val y3: Int = x3 + val x4 = xs.head + val y4: Int = x4 + val x5 = xs.to(List) + val y5: List[Char] = x5 + val (xs6, xs7) = xs.partition(_ % 2 == 0) + val ys6: String = xs6 + val ys7: String = xs7 + val xs8 = xs.drop(2) + val ys8: String = xs8 + val xs9 = xs.map(_ + 1) + val ys9: Seq[Int] = xs9 + val xs9a = xs.map(_.toUpper) + val ys9a: String = xs9a + val xs10 = xs.flatMap((x: Char) => s"$x,$x") + val ys10: String = xs10 + val xs11 = xs ++ xs + val ys11: String = xs11 + val xs11a = xs ++ List('x', 'y') // Cons('x', Cons('y', Nil)) + val ys11a: String = xs11a + val xs12 = xs ++ Nil + val ys12: String = xs12 + val xs13 = Nil ++ xs.iterator + val ys13: List[Char] = xs13 + val xs14 = xs ++ Cons("xyz", Nil) + val ys14: Seq[Any] = xs14 + val xs15 = xs.zip(xs9) + val ys15: Seq[(Char, Int)] = xs15 + println("-------") + println(x1) + println(x2) + println(x3) + println(x4) + println(x5) + println(xs6) + println(xs7) + println(xs8) + println(xs9) + println(xs9a) + println(xs10) + println(xs11) + println(xs11a) + println(xs12) + println(xs13) + println(xs14) + println(xs15) + } + + def main(args: Array[String]) = { + val ints = Cons(1, Cons(2, Cons(3, Nil))) + val intsBuf = ints.to(ArrayBuffer) + val intsListBuf = ints.to(ListBuffer) + val intsView = ints.view + seqOps(ints) + seqOps(intsBuf) + seqOps(intsListBuf) + viewOps(intsView) + stringOps("abc") + } +} diff --git a/tests/run-custom-args/tasty-inspector/stdlibExperimentalDefinitions.scala b/tests/run-custom-args/tasty-inspector/stdlibExperimentalDefinitions.scala index a65a50ab54ad..e8e2f0c08179 100644 --- a/tests/run-custom-args/tasty-inspector/stdlibExperimentalDefinitions.scala +++ b/tests/run-custom-args/tasty-inspector/stdlibExperimentalDefinitions.scala @@ -46,6 +46,13 @@ val experimentalDefinitionInLibrary = Set( "scala.annotation.newMain.Help$", "scala.annotation.newMain.Names", + //// New feature: capture checking + "scala.annotation.capability", + "scala.annotation.internal.CaptureChecked", + "scala.annotation.internal.requiresCapability", + "scala.annotation.retains", + "scala.annotation.retainsByName", + //// New APIs: Mirror // Can be stabilized in 3.3.0 or later. "scala.deriving.Mirror$.fromProductTyped", // This API is a bit convoluted. We may need some more feedback before we can stabilize it. diff --git a/tests/semanticdb/metac.expect b/tests/semanticdb/metac.expect index 5e206505137b..4090ce6b6001 100644 --- a/tests/semanticdb/metac.expect +++ b/tests/semanticdb/metac.expect @@ -359,7 +359,6 @@ Text => empty Language => Scala Symbols => 14 entries Occurrences => 30 entries -Synthetics => 2 entries Symbols: example/Anonymous# => class Anonymous extends Object { self: Anonymous & Anonymous => +6 decls } @@ -409,10 +408,6 @@ Occurrences: [18:6..18:9): foo <- example/Anonymous#foo. [18:16..18:19): Foo -> example/Anonymous#Foo# -Synthetics: -[10:2..10:9):locally => *[Unit] -[13:2..13:9):locally => *[Unit] - expect/AnonymousGiven.scala --------------------------- @@ -447,7 +442,6 @@ Text => empty Language => Scala Symbols => 109 entries Occurrences => 113 entries -Synthetics => 2 entries Symbols: classes/C1# => final class C1 extends AnyVal { self: C1 => +2 decls } @@ -675,10 +669,6 @@ Occurrences: [53:4..53:9): local -> local4 [53:10..53:11): + -> scala/Int#`+`(+4). -Synthetics: -[51:16..51:27):List(1).map => *[Int] -[51:16..51:20):List => *.apply[Int] - expect/Empty.scala ------------------ @@ -896,7 +886,7 @@ Text => empty Language => Scala Symbols => 181 entries Occurrences => 148 entries -Synthetics => 10 entries +Synthetics => 8 entries Symbols: _empty_/Enums. => final object Enums extends Object { self: Enums.type => +30 decls } @@ -1239,8 +1229,6 @@ Synthetics: [52:31..52:50):identity[Option[B]] => *[Function1[A, Option[B]]] [54:14..54:18):Some => *.apply[Some[Int]] [54:14..54:34):Some(Some(1)).unwrap => *(given_<:<_T_T[Option[Int]]) -[54:19..54:23):Some => *.apply[Int] -[54:28..54:34):unwrap => *[Some[Int], Int] [56:52..56:64):Enum[Planet] => *[Planet] expect/EtaExpansion.scala @@ -1253,7 +1241,7 @@ Text => empty Language => Scala Symbols => 3 entries Occurrences => 8 entries -Synthetics => 5 entries +Synthetics => 1 entries Symbols: example/EtaExpansion# => class EtaExpansion extends Object { self: EtaExpansion => +1 decls } @@ -1271,11 +1259,7 @@ Occurrences: [4:25..4:26): + -> java/lang/String#`+`(). Synthetics: -[3:2..3:13):Some(1).map => *[Int] -[3:2..3:6):Some => *.apply[Int] -[3:14..3:22):identity => *[Int] [4:2..4:18):List(1).foldLeft => *[String] -[4:2..4:6):List => *.apply[Int] expect/Example.scala -------------------- @@ -1425,7 +1409,7 @@ Text => empty Language => Scala Symbols => 13 entries Occurrences => 52 entries -Synthetics => 6 entries +Synthetics => 2 entries Symbols: example/ForComprehension# => class ForComprehension extends Object { self: ForComprehension => +1 decls } @@ -1497,10 +1481,6 @@ Occurrences: [41:6..41:7): f -> local10 Synthetics: -[4:9..4:13):List => *.apply[Int] -[5:9..5:13):List => *.apply[Int] -[10:9..10:13):List => *.apply[Int] -[11:9..11:13):List => *.apply[Int] [19:9..19:13):List => *.apply[Tuple2[Int, Int]] [33:9..33:13):List => *.apply[Tuple4[Int, Int, Int, Int]] @@ -1514,7 +1494,6 @@ Text => empty Language => Scala Symbols => 29 entries Occurrences => 65 entries -Synthetics => 3 entries Symbols: a/b/Givens. => final object Givens extends Object { self: Givens.type => +12 decls } @@ -1614,11 +1593,6 @@ Occurrences: [26:57..26:58): A -> a/b/Givens.foo().(A) [26:59..26:64): empty -> a/b/Givens.Monoid#empty(). -Synthetics: -[12:17..12:25):sayHello => *[Int] -[13:19..13:29):sayGoodbye => *[Int] -[14:18..14:27):saySoLong => *[Int] - expect/ImplicitConversion.scala ------------------------------- @@ -2018,7 +1992,6 @@ Text => empty Language => Scala Symbols => 6 entries Occurrences => 10 entries -Synthetics => 1 entries Symbols: example/Local# => class Local extends Object { self: Local => +2 decls } @@ -2040,9 +2013,6 @@ Occurrences: [4:25..4:26): a -> local1 [5:4..5:6): id -> local2 -Synthetics: -[5:4..5:6):id => *[Int] - expect/Locals.scala ------------------- @@ -2053,7 +2023,6 @@ Text => empty Language => Scala Symbols => 3 entries Occurrences => 6 entries -Synthetics => 1 entries Symbols: local0 => val local x: Int @@ -2068,9 +2037,6 @@ Occurrences: [5:4..5:8): List -> scala/package.List. [5:9..5:10): x -> local0 -Synthetics: -[5:4..5:8):List => *.apply[Int] - expect/MatchType.scala ---------------------- @@ -2222,7 +2188,7 @@ Text => empty Language => Scala Symbols => 3 entries Occurrences => 80 entries -Synthetics => 2 entries +Synthetics => 1 entries Symbols: example/MethodUsages# => class MethodUsages extends Object { self: MethodUsages => +2 decls } @@ -2312,7 +2278,6 @@ Occurrences: [34:8..34:9): m -> example/Methods#m17.m(). Synthetics: -[13:2..13:6):m.m7 => *[Int] [13:2..13:26):m.m7(m, new m.List[Int]) => *(Int) expect/Methods.scala @@ -3165,7 +3130,7 @@ Text => empty Language => Scala Symbols => 52 entries Occurrences => 132 entries -Synthetics => 36 entries +Synthetics => 23 entries Symbols: example/Synthetic# => class Synthetic extends Object { self: Synthetic => +23 decls } @@ -3356,26 +3321,17 @@ Occurrences: [58:6..58:9): foo -> example/Synthetic#Contexts.foo(). Synthetics: -[5:2..5:13):List(1).map => *[Int] -[5:2..5:6):List => *.apply[Int] [6:2..6:18):Array.empty[Int] => intArrayOps(*) [7:2..7:8):"fooo" => augmentString(*) [10:13..10:24):"name:(.*)" => augmentString(*) -[11:17..11:25):LazyList => *.apply[Int] -[13:4..13:28):#:: 2 #:: LazyList.empty => *[Int] [13:8..13:28):2 #:: LazyList.empty => toDeferrer[Int](*) -[13:10..13:28):#:: LazyList.empty => *[Int] [13:14..13:28):LazyList.empty => toDeferrer[Nothing](*) [13:14..13:28):LazyList.empty => *[Nothing] -[15:25..15:33):LazyList => *.apply[Int] -[17:14..17:38):#:: 2 #:: LazyList.empty => *[Int] [17:18..17:38):2 #:: LazyList.empty => toDeferrer[Int](*) -[17:20..17:38):#:: LazyList.empty => *[Int] [17:24..17:38):LazyList.empty => toDeferrer[Nothing](*) [17:24..17:38):LazyList.empty => *[Nothing] [19:12..19:13):1 => intWrapper(*) [19:26..19:27):0 => intWrapper(*) -[19:46..19:50):x -> => *[Int] [19:46..19:47):x => ArrowAssoc[Int](*) [20:12..20:13):1 => intWrapper(*) [20:26..20:27):0 => intWrapper(*) @@ -3384,10 +3340,6 @@ Synthetics: [32:35..32:49):Array.empty[T] => *(evidence$1) [36:22..36:27):new F => orderingToOrdered[F](*) [36:22..36:27):new F => *(ordering) -[40:9..40:43):scala.concurrent.Future.successful => *[Int] -[41:9..41:43):scala.concurrent.Future.successful => *[Int] -[44:9..44:43):scala.concurrent.Future.successful => *[Int] -[45:9..45:43):scala.concurrent.Future.successful => *[Int] [51:24..51:30):foo(0) => *(x$1) [52:27..52:33):foo(0) => *(x) [55:6..55:12):foo(x) => *(x) @@ -3403,7 +3355,6 @@ Text => empty Language => Scala Symbols => 2 entries Occurrences => 5 entries -Synthetics => 2 entries Symbols: example/Tabs$package. => final package object example extends Object { self: example.type => +2 decls } @@ -3416,11 +3367,6 @@ Occurrences: [4:3..4:6): map -> scala/collection/immutable/List#map(). [4:9..4:10): + -> scala/Int#`+`(+4). -Synthetics: -[3:1..4:6):List(1,2,3) - .map => *[Int] -[3:1..3:5):List => *.apply[Int] - expect/Traits.scala ------------------- @@ -3471,7 +3417,7 @@ Text => empty Language => Scala Symbols => 22 entries Occurrences => 46 entries -Synthetics => 7 entries +Synthetics => 2 entries Symbols: example/ValPattern# => class ValPattern extends Object { self: ValPattern => +14 decls } @@ -3546,13 +3492,8 @@ Occurrences: [40:10..40:18): rightVar -> local4 Synthetics: -[6:4..6:8):Some => *.apply[Int] [8:6..8:10):List => *.unapplySeq[Nothing] [8:11..8:15):Some => *.unapply[Nothing] -[12:4..12:8):Some => *.apply[Int] -[25:4..25:11):locally => *[Unit] -[28:8..28:12):Some => *.apply[Int] -[32:8..32:12):Some => *.apply[Int] expect/Vals.scala ----------------- @@ -4171,7 +4112,6 @@ Text => empty Language => Scala Symbols => 3 entries Occurrences => 6 entries -Synthetics => 1 entries Symbols: example/`local-file`# => class local-file extends Object { self: local-file => +1 decls } @@ -4186,9 +4126,6 @@ Occurrences: [5:4..5:9): local -> local0 [5:10..5:11): + -> scala/Int#`+`(+4). -Synthetics: -[3:2..3:9):locally => *[Int] - expect/nullary.scala -------------------- @@ -4199,7 +4136,6 @@ Text => empty Language => Scala Symbols => 17 entries Occurrences => 29 entries -Synthetics => 1 entries Symbols: _empty_/Concrete# => class Concrete extends NullaryTest[Int, List] { self: Concrete => +3 decls } @@ -4251,9 +4187,6 @@ Occurrences: [18:7..18:15): Concrete -> _empty_/Concrete# [18:17..18:25): nullary3 -> _empty_/Concrete#nullary3(). -Synthetics: -[13:17..13:21):List => *.apply[Int] - expect/recursion.scala ---------------------- @@ -4542,7 +4475,6 @@ Text => empty Language => Scala Symbols => 144 entries Occurrences => 225 entries -Synthetics => 1 entries Symbols: local0 => abstract method k => Int @@ -4917,9 +4849,6 @@ Occurrences: [119:32..119:38): Option -> scala/Option# [119:39..119:42): Int -> scala/Int# -Synthetics: -[68:20..68:24):@ann => *[Int] - expect/semanticdb-extract.scala ------------------------------- @@ -4930,7 +4859,7 @@ Text => empty Language => Scala Symbols => 18 entries Occurrences => 20 entries -Synthetics => 3 entries +Synthetics => 2 entries Symbols: _empty_/AnObject. => final object AnObject extends Object { self: AnObject.type => +6 decls } @@ -4975,7 +4904,6 @@ Occurrences: [16:20..16:23): Int -> scala/Int# Synthetics: -[11:2..11:6):List => *.apply[Int] [12:2..12:12):List.apply => *[Nothing] [13:2..13:14):List.`apply` => *[Nothing] @@ -4989,7 +4917,7 @@ Text => empty Language => Scala Symbols => 18 entries Occurrences => 43 entries -Synthetics => 2 entries +Synthetics => 1 entries Symbols: _empty_/MyProgram# => final class MyProgram extends Object { self: MyProgram => +2 decls } @@ -5057,6 +4985,5 @@ Occurrences: [7:30..7:33): foo -> _empty_/toplevel$package.foo(). Synthetics: -[5:40..5:60):(1 to times) foreach => *[Unit] [5:41..5:42):1 => intWrapper(*)