From b9f8737180edc1d73ea46313fcaedcd4e8d31ce2 Mon Sep 17 00:00:00 2001 From: Guillaume Carraux Date: Thu, 7 Aug 2025 13:34:55 +0000 Subject: [PATCH 1/2] add lastUse annotation --- .../tools/backend/jvm/BCodeBodyBuilder.scala | 42 ++++-- .../tools/backend/jvm/BCodeSkelBuilder.scala | 4 + compiler/src/dotty/tools/dotc/Compiler.scala | 3 +- .../dotty/tools/dotc/core/Definitions.scala | 2 + .../tools/dotc/transform/LambdaLift.scala | 5 + .../tools/dotc/transform/PostTyper.scala | 33 +++++ .../transform/VerifyLastUseAnnotations.scala | 132 ++++++++++++++++++ .../backend/jvm/DottyBytecodeTests.scala | 35 +++++ library/src/scala/annotation/lastUse.scala | 8 ++ tests/neg/early-reclamation/verifier.scala | 62 ++++++++ tests/pos/early-reclamation/verifier.scala | 33 +++++ 11 files changed, 343 insertions(+), 16 deletions(-) create mode 100644 compiler/src/dotty/tools/dotc/transform/VerifyLastUseAnnotations.scala create mode 100644 library/src/scala/annotation/lastUse.scala create mode 100644 tests/neg/early-reclamation/verifier.scala create mode 100644 tests/pos/early-reclamation/verifier.scala diff --git a/compiler/src/dotty/tools/backend/jvm/BCodeBodyBuilder.scala b/compiler/src/dotty/tools/backend/jvm/BCodeBodyBuilder.scala index 1eba6c0b1bf8..c66ed96969dc 100644 --- a/compiler/src/dotty/tools/backend/jvm/BCodeBodyBuilder.scala +++ b/compiler/src/dotty/tools/backend/jvm/BCodeBodyBuilder.scala @@ -25,6 +25,8 @@ import dotty.tools.dotc.core.Phases.* import dotty.tools.dotc.core.Decorators.em import dotty.tools.dotc.report import dotty.tools.dotc.ast.Trees.SyntheticUnit +import dotty.tools.dotc.transform.PostTyper.{lastUseAttachment, methodLastUses} +import scala.annotation.lastUse /* * @@ -249,7 +251,7 @@ trait BCodeBodyBuilder extends BCodeSkelBuilder { genLoadTo(elsep, expectedType, dest) else lineNumber(tree.cond) - genAdaptAndSendToDest(UNIT, expectedType, dest) + genAdaptAndSendToDest(UNIT, expectedType, dest, tree.symbol) expectedType end if } @@ -436,7 +438,14 @@ trait BCodeBodyBuilder extends BCodeSkelBuilder { case None => if (!sym.is(Package)) { if (sym.is(Module)) genLoadModule(sym) - else locals.load(sym) + else + locals.load(sym) + if t.hasAttachment(lastUseAttachment) then + emit(asm.Opcodes.ACONST_NULL) + + if(!locals.contains(sym)) println("/!\\ annotation on a local that didn't exist !")//you're very good if this triggers + val idx = locals.getOrMakeLocal(sym).idx + bc.store(idx, tk) } case Some(t) => genLoad(t, generatedType) @@ -487,10 +496,10 @@ trait BCodeBodyBuilder extends BCodeSkelBuilder { // emit conversion and send to the right destination if generatedDest == LoadDestination.FallThrough then - genAdaptAndSendToDest(generatedType, expectedType, dest) + genAdaptAndSendToDest(generatedType, expectedType, dest, tree.symbol) end genLoadTo - def genAdaptAndSendToDest(generatedType: BType, expectedType: BType, dest: LoadDestination): Unit = + def genAdaptAndSendToDest(generatedType: BType, expectedType: BType, dest: LoadDestination, topSymbol: Symbol): Unit = if generatedType != expectedType then adapt(generatedType, expectedType) @@ -507,6 +516,12 @@ trait BCodeBodyBuilder extends BCodeSkelBuilder { bc.store(loc.idx, expectedType) bc dropMany stackDiff bc.load(loc.idx, expectedType) + + if lastUses.contains(topSymbol) then + report.debuglog(s"removing copy of $topSymbol from genAdapt") + emit(asm.Opcodes.ACONST_NULL) + bc.store(loc.idx, expectedType) + end if bc goTo label case LoadDestination.Return => @@ -1713,26 +1728,23 @@ trait BCodeBodyBuilder extends BCodeSkelBuilder { genCZJUMP(success, failure, Primitives.NE, BOOL, targetIfNoJump) } else { // l == r -> if (l eq null) r eq null else l.equals(r) - val eqEqTempLocal = locals.makeLocal(ObjectRef, nme.EQEQ_LOCAL_VAR.mangledString, defn.ObjectType, r.span) val lNull = new asm.Label val lNonNull = new asm.Label - genLoad(l, ObjectRef) + genLoad(r, ObjectRef) // load rhs --> (r) stack.push(ObjectRef) - genLoad(r, ObjectRef) + genLoad(l, ObjectRef) // load lhs --> (l,r) stack.pop() - locals.store(eqEqTempLocal) - bc dup ObjectRef - genCZJUMP(lNull, lNonNull, Primitives.EQ, ObjectRef, targetIfNoJump = lNull) + bc dup ObjectRef // duplicate top stack variable --> (l,l,r) + genCZJUMP(lNull, lNonNull, Primitives.EQ, ObjectRef, targetIfNoJump = lNull) // compare lhs with NULL --> (l,r) markProgramPoint(lNull) - bc drop ObjectRef - locals.load(eqEqTempLocal) - genCZJUMP(success, failure, Primitives.EQ, ObjectRef, targetIfNoJump = lNonNull) + bc drop ObjectRef // drop top stack variable --> (r) + genCZJUMP(success, failure, Primitives.EQ, ObjectRef, targetIfNoJump = lNonNull) // --> (-) markProgramPoint(lNonNull) - locals.load(eqEqTempLocal) - genCallMethod(defn.Any_equals, InvokeStyle.Virtual) + emit(asm.Opcodes.SWAP) //swap l and r for correct .equals ordering --> (r,l) + genCallMethod(defn.Any_equals, InvokeStyle.Virtual) // --> (-) genCZJUMP(success, failure, Primitives.NE, BOOL, targetIfNoJump) } } diff --git a/compiler/src/dotty/tools/backend/jvm/BCodeSkelBuilder.scala b/compiler/src/dotty/tools/backend/jvm/BCodeSkelBuilder.scala index 5390626eb2cc..c81be0e26413 100644 --- a/compiler/src/dotty/tools/backend/jvm/BCodeSkelBuilder.scala +++ b/compiler/src/dotty/tools/backend/jvm/BCodeSkelBuilder.scala @@ -20,6 +20,7 @@ import dotty.tools.dotc.core.Types.* import dotty.tools.dotc.core.Contexts.* import dotty.tools.dotc.util.Spans.* import dotty.tools.dotc.report +import dotty.tools.dotc.transform.PostTyper.methodLastUses /* @@ -432,6 +433,8 @@ trait BCodeSkelBuilder extends BCodeHelpers { val stack = new BTypesStack // line numbers var lastEmittedLineNr = -1 + // lastUse annotated variables + var lastUses = Set.empty[Symbol] object bc extends JCodeMethodN { override def jmethod = PlainSkelBuilder.this.mnode @@ -648,6 +651,7 @@ trait BCodeSkelBuilder extends BCodeHelpers { val rhs = dd.rhs locals.reset(isStaticMethod = methSymbol.isStaticMember) jumpDest = immutable.Map.empty + lastUses = dd.getAttachment(methodLastUses).getOrElse(Set.empty[Symbol]) // check previous invocation of genDefDef exited as many varsInScope as it entered. assert(varsInScope == null, "Unbalanced entering/exiting of GenBCode's genBlock().") diff --git a/compiler/src/dotty/tools/dotc/Compiler.scala b/compiler/src/dotty/tools/dotc/Compiler.scala index f82f7956b34b..23921f12bae1 100644 --- a/compiler/src/dotty/tools/dotc/Compiler.scala +++ b/compiler/src/dotty/tools/dotc/Compiler.scala @@ -143,7 +143,8 @@ class Compiler { new sjs.JUnitBootstrappers, // Generate JUnit-specific bootstrapper classes for Scala.js (not enabled by default) new CollectEntryPoints, // Collect all entry points and save them in the context new CollectSuperCalls, // Find classes that are called with super - new RepeatableAnnotations) :: // Aggregate repeatable annotations + new RepeatableAnnotations, + new VerifyLastUseAnnotations) :: // Aggregate repeatable annotations Nil /** Generate the output of the compilation */ diff --git a/compiler/src/dotty/tools/dotc/core/Definitions.scala b/compiler/src/dotty/tools/dotc/core/Definitions.scala index c5dd4662a7b8..d9a7db72d382 100644 --- a/compiler/src/dotty/tools/dotc/core/Definitions.scala +++ b/compiler/src/dotty/tools/dotc/core/Definitions.scala @@ -1103,6 +1103,8 @@ class Definitions { @tu lazy val PublicInBinaryAnnot: ClassSymbol = requiredClass("scala.annotation.publicInBinary") @tu lazy val WitnessNamesAnnot: ClassSymbol = requiredClass("scala.annotation.internal.WitnessNames") @tu lazy val StableNullAnnot: ClassSymbol = requiredClass("scala.annotation.stableNull") + @tu lazy val LastUseAnnot: ClassSymbol = requiredClass("scala.annotation.lastUse") + @tu lazy val JavaRepeatableAnnot: ClassSymbol = requiredClass("java.lang.annotation.Repeatable") diff --git a/compiler/src/dotty/tools/dotc/transform/LambdaLift.scala b/compiler/src/dotty/tools/dotc/transform/LambdaLift.scala index af168b563048..2e5ac464dab9 100644 --- a/compiler/src/dotty/tools/dotc/transform/LambdaLift.scala +++ b/compiler/src/dotty/tools/dotc/transform/LambdaLift.scala @@ -19,6 +19,8 @@ import util.Store import collection.mutable.{HashMap, LinkedHashMap, ListBuffer} import scala.compiletime.uninitialized +import dotty.tools.dotc.transform.PostTyper.lastUseAttachment +import dotty.tools.dotc.core.Flags object LambdaLift: import ast.tpd.* @@ -284,6 +286,9 @@ class LambdaLift extends MiniPhase with IdentityDenotTransformer { thisPhase => val lft = lifter if (prefix eq NoPrefix) if (sym.enclosure != lft.currentEnclosure && !sym.isStatic) + if tree.hasAttachment(lastUseAttachment) && sym.is(Flags.Local) then + tree.removeAttachment(lastUseAttachment)//this will still get analyzed bc the posttyper map stores it. I cant drop it. but this is needed to not apply the lastUse + report.error(s"cannot annotate free local variable ${sym.name} @lastUse in a lambda",tree.sourcePos) (if (sym is Method) lft.memberRef(sym) else lft.proxyRef(sym)).withSpan(tree.span) else if (sym.owner.isClass) // sym was lifted out ref(sym).withSpan(tree.span) diff --git a/compiler/src/dotty/tools/dotc/transform/PostTyper.scala b/compiler/src/dotty/tools/dotc/transform/PostTyper.scala index 9f79c063dc03..5a378cad47c6 100644 --- a/compiler/src/dotty/tools/dotc/transform/PostTyper.scala +++ b/compiler/src/dotty/tools/dotc/transform/PostTyper.scala @@ -22,10 +22,18 @@ import NameKinds.WildcardParamName import cc.* import dotty.tools.dotc.transform.MacroAnnotations.hasMacroAnnotation import dotty.tools.dotc.core.NameKinds.DefaultGetterName +import dotty.tools.dotc.util.Property.StickyKey +import dotty.tools.dotc.transform.PostTyper.methodLastUses object PostTyper { val name: String = "posttyper" val description: String = "additional checks and cleanups after type checking" + + val lastUseAttachment = StickyKey[Unit] //to attach on the variables themselves, which one is lastUsed + + // method -> symbols with @lastUse annotation + val methodLastUses = StickyKey[Set[Symbol]] + } /** A macro transform that runs immediately after typer and that performs the following functions: @@ -56,6 +64,8 @@ object PostTyper { * (11) Minimizes `call` fields of `Inlined` nodes to just point to the toplevel * class from which code was inlined. * + * (12) Replaces @lastUse annotation with an attachment, so it survives the erasure phase. + * * The reason for making this a macro transform is that some functions (in particular * super and protected accessors and instantiation checks) are naturally top-down and * don't lend themselves to the bottom-up approach of a mini phase. The other two functions @@ -569,6 +579,29 @@ class PostTyper extends MacroTransform with InfoTransformer { thisPhase => case tree: TypeTree => val tpe = if tree.isInferred then CleanupRetains()(tree.tpe) else tree.tpe tree.withType(transformAnnotsIn(tpe)) + case Typed(t, tpt: TypeTree) if tpt.tpe.hasAnnotation(defn.LastUseAnnot) => + + t match//notice that they look very similar, could be merged ? + case id: Ident => + id.putAttachment(PostTyper.lastUseAttachment, ())//id is muted + val tpt_clean = tpt.withType(tpt.tpe.dropAnnot(defn.LastUseAnnot)) + + if (tree.tpe.typeSymbol.isPrimitiveValueClass || tree.symbol.is(Flags.Method)) + report.error("`@lastUse` annotation cannot be used on primitives and methods", tpt.sourcePos) + + // PostTyper.lastUses.add(t.symbol) + + println(s"source method for sym ${t.symbol.name}: ${ctx.tree}") //ctx.tree is always correct ! + + val others = ctx.tree.getAttachment(methodLastUses).getOrElse(Set.empty) + ctx.tree.putAttachment(methodLastUses, others + t.symbol) + + Typed(id, tpt_clean) + + case _ => + report.error("`@lastUse` annotation can only be applied on local variables", tpt.srcPos) + tree + case Typed(Ident(nme.WILDCARD), _) => withMode(Mode.Pattern)(super.transform(tree)) // The added mode signals that bounds in a pattern need not diff --git a/compiler/src/dotty/tools/dotc/transform/VerifyLastUseAnnotations.scala b/compiler/src/dotty/tools/dotc/transform/VerifyLastUseAnnotations.scala new file mode 100644 index 000000000000..62b5bcd45de1 --- /dev/null +++ b/compiler/src/dotty/tools/dotc/transform/VerifyLastUseAnnotations.scala @@ -0,0 +1,132 @@ +package dotty.tools.dotc +package transform + +import core.* +import ast.tpd.* +import Contexts.* +import MegaPhase.* +import Annotations.* +import Symbols.defn +import Constants.* +import Types.* +import Decorators.* +import Flags.* +import Symbols.Symbol +import dotty.tools.dotc.report + +import scala.collection.mutable +import dotty.tools.dotc.util.SourcePosition +import dotty.tools.dotc.core.StdNames.nme +import dotty.tools.dotc.core.Names.Name +import dotty.tools.dotc.transform.PostTyper.methodLastUses + +class VerifyLastUseAnnotations extends MiniPhase: + + override def phaseName: String = VerifyLastUseAnnotations.name + override def description: String = VerifyLastUseAnnotations.description + + + + case class FoldInfo(allowVar: Boolean, allowAnnot: Boolean){ + def afterAnnot = FoldInfo(false, allowAnnot) + def merge(other: FoldInfo) = FoldInfo(allowVar && other.allowVar, allowAnnot) + def inLoop = FoldInfo(allowVar, false) + } + + private class LastUseVerifier(target: Symbol, method: Tree) extends TreeAccumulator[FoldInfo]{ + override def apply(accInfo: FoldInfo, tree: Tree)(using Context): FoldInfo = traverse(tree)(accInfo) + + def verify(using Context): Unit = + traverse(method)(FoldInfo(true, true)) + + private def traverseExclusiveCases(cases: List[Tree])(info: FoldInfo)(using Context): FoldInfo = + cases + .map(c => traverse(c)(info))//traverse all of them with same source info + .reduce((a,b)=> a.merge(b))//merge all info, return it + + private def traverse(tree: Tree)(accInfo: FoldInfo)(using Context): FoldInfo = { + var info = accInfo//used to mutate and pass the information + + //by default, accInfo is propagated in the order of the arguments. so block is already good, and everything that has linear reading + tree match + case _: Ident if tree.symbol == target => + if !info.allowVar then + report.error(s"reuse of variable ${tree.symbol.name} after @lastUse", tree.sourcePos) + if tree.hasAttachment(PostTyper.lastUseAttachment) then + if info.allowAnnot then info.afterAnnot else + report.error(s"cannot use @lastUse annotation in a loop", tree.sourcePos) + tree.removeAttachment(PostTyper.lastUseAttachment) + info.afterAnnot + else + info + + case If(cond, thenp, elsep) => + info = traverse(cond)(info) + val info_branch1 = traverse(thenp)(info)//both branches are exclusive + val info_branch2 = traverse(elsep)(info)//so we combine the info at the end + + info_branch1.merge(info_branch2) + + case WhileDo(cond, body) => + //everything is run in a loop, so I cannot allow a lastUse here + info = info.inLoop + traverse(cond)(info)//I do not care about the result, since I cannot annotate anything inside + traverse(body)(info)//I just need to check if there is any illegal variable use + + case Match(selector, cases) => + info = traverse(selector)(info) + + //each case are exclusive, merge at the end + traverseExclusiveCases(cases)(info) + + case Try(expr, cases, finalizer) => + info = traverse(expr)(info) + info = traverseExclusiveCases(cases)(info) + traverse(finalizer)(info) + + // case Closure(env, _, _) => + // env.find(_.symbol == target) match + // case None => () + // case Some(t) => if(info.allowVar){ + // report.warning(s"creating a lambda using the variable ${target.name} (annotated later with @lastUse) creates a new reference to the variable, making the annotation useless", t.sourcePos) + // report.echo("Note that changing the lambda to a function solves the problem\n") + // } + + + + // foldOver(info, tree) + + // case vd @ ValDef(name, tpt, rhs) => + // def maybeAlias(tree: Tree): Boolean = tree match//short check that it is not creating an obvious alias + // case Ident(_) => tree.symbol == target + // case Block(_, expr) => maybeAlias(expr) + // case Typed(expr, _) => maybeAlias(expr) + // case _ => false + + // if maybeAlias(vd.rhs) then + // report.warning("Creating a possible alias to @lastUse annotated variable. this will keep the object alive longer", tree.sourcePos) + + // foldOver(info, tree)//maybe creating a closure + + // info + + + case _ => foldOver(info, tree)//visit other trees + } + } + + override def transformDefDef(method: DefDef)(using Context): Tree = + + if method.hasAttachment(methodLastUses) then + val lastUses = method.getAttachment(methodLastUses).get + + lastUses.foreach(sym => + val verifier = LastUseVerifier(sym, method) + verifier.verify + ) + + method + +object VerifyLastUseAnnotations: + val name: String = "verifyLastUseAnnotations" + val description: String = "verify that @lastUse annotations are use correctly" diff --git a/compiler/test/dotty/tools/backend/jvm/DottyBytecodeTests.scala b/compiler/test/dotty/tools/backend/jvm/DottyBytecodeTests.scala index 2e48b33ec624..68ea9bb0594c 100644 --- a/compiler/test/dotty/tools/backend/jvm/DottyBytecodeTests.scala +++ b/compiler/test/dotty/tools/backend/jvm/DottyBytecodeTests.scala @@ -2009,6 +2009,41 @@ class DottyBytecodeTests extends DottyBytecodeTest { assertEquals(expected, clsNode.interfaces.asScala) } } + + @Test def lastUseNullify(): Unit = { + val source = + s""" + |import scala.annotation.lastUse + |class A + |object Foo { + | def foo = + | val x = A() + | println(x: @lastUse) + |}""".stripMargin + + + + + checkBCode(source){ dir => + val fooClass = loadClassNode(dir.lookupName("Foo$.class", directory = false).input) + + val fooMeth = getMethod(fooClass, "foo") + + assertSameCode(fooMeth, List( + TypeOp(NEW, "A"), + Op(DUP), + Invoke(INVOKESPECIAL, "A", "", "()V", itf = false), + VarOp(ASTORE, 1), + Field(GETSTATIC, "scala/Predef$", "MODULE$", "Lscala/Predef$;"), + VarOp(ALOAD, 1), + Op(ACONST_NULL), + VarOp(ASTORE, 1), + Invoke(INVOKEVIRTUAL, "scala/Predef$", "println", "(Ljava/lang/Object;)V", itf = false), + Op(RETURN) + )) + + } + } } object invocationReceiversTestCode { diff --git a/library/src/scala/annotation/lastUse.scala b/library/src/scala/annotation/lastUse.scala new file mode 100644 index 000000000000..24352ed95696 --- /dev/null +++ b/library/src/scala/annotation/lastUse.scala @@ -0,0 +1,8 @@ +package scala.annotation + +import java.lang.annotation.{Target} + +/** An annotation that indicates that an element will never be used again after this evaluation, + and can be removed from the stack by force + */ +final class lastUse extends annotation.StaticAnnotation diff --git a/tests/neg/early-reclamation/verifier.scala b/tests/neg/early-reclamation/verifier.scala new file mode 100644 index 000000000000..b660b8137d89 --- /dev/null +++ b/tests/neg/early-reclamation/verifier.scala @@ -0,0 +1,62 @@ +import scala.annotation.lastUse + +class A + +object Verif: + def test1 = + val a = A() + + println(a: @lastUse) + println(a) // error + + def test2 = + val a = A() + + if "hi".length() == 1 then + println(a: @lastUse) + else + println("hello") + + println(a) // error + + def test3 = + val a = A() + + while true do + println(a: @lastUse) // error + + def test4 = + val a = A() + List(1).foreach(x => println(a: @lastUse)) // error + println(a) + + def test5 = + val a = A() + for x <- 1 to 10 do + println(a: @lastUse) // error + + def test6 = + val a = A() + + def f = () => a + + f() + + println(a: @lastUse) + + f() // error + + def test7 = + val a = A() + + println(a: @lastUse) + + val f = () => a // error + + def test8 = + val a = A() + + val f = () => a + + println(a: @lastUse) + f() \ No newline at end of file diff --git a/tests/pos/early-reclamation/verifier.scala b/tests/pos/early-reclamation/verifier.scala new file mode 100644 index 000000000000..33abdaf94420 --- /dev/null +++ b/tests/pos/early-reclamation/verifier.scala @@ -0,0 +1,33 @@ +import scala.annotation.lastUse + +class Obj{} + + + +object Verif: + + private def use(o: Obj): Unit = + val x = o + + + def test1 = + val o = Obj() + + use(o) + val y = o + use(o) + use(o: @lastUse) + + def test2 = + val o = Obj() + + use(o) + if "hi".length() == 2 then + use(o: @lastUse) + else + if "hello".length == 3 then + use(o) + else + use(o: @lastUse) + + // what else ??? \ No newline at end of file From 450e83a875566f4adf4121a15f31c7fde3c05978 Mon Sep 17 00:00:00 2001 From: Guillaume Carraux Date: Wed, 24 Sep 2025 07:46:48 +0000 Subject: [PATCH 2/2] clean implementation of lastUse annotation --- .../tools/backend/jvm/BCodeBodyBuilder.scala | 49 +++-- compiler/src/dotty/tools/dotc/Compiler.scala | 4 +- .../dotty/tools/dotc/core/Definitions.scala | 1 - .../tools/dotc/transform/LambdaLift.scala | 5 +- .../tools/dotc/transform/PostTyper.scala | 42 ++-- .../transform/VerifyLastUseAnnotations.scala | 195 ++++++++++++------ library/src/scala/annotation/lastUse.scala | 2 +- project/Build.scala | 4 +- tests/neg/early-reclamation/verifier.scala | 31 ++- tests/pos/early-reclamation/verifier.scala | 38 +++- 10 files changed, 246 insertions(+), 125 deletions(-) diff --git a/compiler/src/dotty/tools/backend/jvm/BCodeBodyBuilder.scala b/compiler/src/dotty/tools/backend/jvm/BCodeBodyBuilder.scala index c66ed96969dc..04b5056efe15 100644 --- a/compiler/src/dotty/tools/backend/jvm/BCodeBodyBuilder.scala +++ b/compiler/src/dotty/tools/backend/jvm/BCodeBodyBuilder.scala @@ -25,8 +25,7 @@ import dotty.tools.dotc.core.Phases.* import dotty.tools.dotc.core.Decorators.em import dotty.tools.dotc.report import dotty.tools.dotc.ast.Trees.SyntheticUnit -import dotty.tools.dotc.transform.PostTyper.{lastUseAttachment, methodLastUses} -import scala.annotation.lastUse +import dotty.tools.dotc.transform.PostTyper.lastUseAttachment /* * @@ -251,7 +250,7 @@ trait BCodeBodyBuilder extends BCodeSkelBuilder { genLoadTo(elsep, expectedType, dest) else lineNumber(tree.cond) - genAdaptAndSendToDest(UNIT, expectedType, dest, tree.symbol) + genAdaptAndSendToDest(UNIT, expectedType, dest, lastUses.contains(tree.symbol)) expectedType end if } @@ -395,6 +394,11 @@ trait BCodeBodyBuilder extends BCodeSkelBuilder { } else { mnode.visitVarInsn(asm.Opcodes.ALOAD, 0) + + if tree.hasAttachment(lastUseAttachment) then + emit(asm.Opcodes.ACONST_NULL) + mnode.visitVarInsn(asm.Opcodes.ASTORE, 0) + // When compiling Array.scala, the constructor invokes `Array.this.super.`. The expectedType // is `[Object` (computed by typeToBType, the type of This(Array) is `Array[T]`). If we would set // the generatedType to `Array` below, the call to adapt at the end would fail. The situation is @@ -442,8 +446,6 @@ trait BCodeBodyBuilder extends BCodeSkelBuilder { locals.load(sym) if t.hasAttachment(lastUseAttachment) then emit(asm.Opcodes.ACONST_NULL) - - if(!locals.contains(sym)) println("/!\\ annotation on a local that didn't exist !")//you're very good if this triggers val idx = locals.getOrMakeLocal(sym).idx bc.store(idx, tk) } @@ -496,10 +498,10 @@ trait BCodeBodyBuilder extends BCodeSkelBuilder { // emit conversion and send to the right destination if generatedDest == LoadDestination.FallThrough then - genAdaptAndSendToDest(generatedType, expectedType, dest, tree.symbol) + genAdaptAndSendToDest(generatedType, expectedType, dest, lastUses.contains(tree.symbol)) end genLoadTo - def genAdaptAndSendToDest(generatedType: BType, expectedType: BType, dest: LoadDestination, topSymbol: Symbol): Unit = + def genAdaptAndSendToDest(generatedType: BType, expectedType: BType, dest: LoadDestination, cleanSyntheticCopy: Boolean): Unit = if generatedType != expectedType then adapt(generatedType, expectedType) @@ -516,12 +518,9 @@ trait BCodeBodyBuilder extends BCodeSkelBuilder { bc.store(loc.idx, expectedType) bc dropMany stackDiff bc.load(loc.idx, expectedType) - - if lastUses.contains(topSymbol) then - report.debuglog(s"removing copy of $topSymbol from genAdapt") + if cleanSyntheticCopy then emit(asm.Opcodes.ACONST_NULL) bc.store(loc.idx, expectedType) - end if bc goTo label case LoadDestination.Return => @@ -1728,23 +1727,35 @@ trait BCodeBodyBuilder extends BCodeSkelBuilder { genCZJUMP(success, failure, Primitives.NE, BOOL, targetIfNoJump) } else { // l == r -> if (l eq null) r eq null else l.equals(r) + val eqEqTempLocal = locals.makeLocal(ObjectRef, nme.EQEQ_LOCAL_VAR.mangledString, defn.ObjectType, r.span) val lNull = new asm.Label val lNonNull = new asm.Label - genLoad(r, ObjectRef) // load rhs --> (r) + def cleanSyntheticCopy = + if lastUses.contains(r.symbol) then + emit(asm.Opcodes.ACONST_NULL) + locals.store(eqEqTempLocal) + + + + genLoad(l, ObjectRef) stack.push(ObjectRef) - genLoad(l, ObjectRef) // load lhs --> (l,r) + genLoad(r, ObjectRef) stack.pop() - bc dup ObjectRef // duplicate top stack variable --> (l,l,r) - genCZJUMP(lNull, lNonNull, Primitives.EQ, ObjectRef, targetIfNoJump = lNull) // compare lhs with NULL --> (l,r) + locals.store(eqEqTempLocal) + bc dup ObjectRef + genCZJUMP(lNull, lNonNull, Primitives.EQ, ObjectRef, targetIfNoJump = lNull) markProgramPoint(lNull) - bc drop ObjectRef // drop top stack variable --> (r) - genCZJUMP(success, failure, Primitives.EQ, ObjectRef, targetIfNoJump = lNonNull) // --> (-) + bc drop ObjectRef + locals.load(eqEqTempLocal) + cleanSyntheticCopy + genCZJUMP(success, failure, Primitives.EQ, ObjectRef, targetIfNoJump = lNonNull) markProgramPoint(lNonNull) - emit(asm.Opcodes.SWAP) //swap l and r for correct .equals ordering --> (r,l) - genCallMethod(defn.Any_equals, InvokeStyle.Virtual) // --> (-) + locals.load(eqEqTempLocal) + cleanSyntheticCopy + genCallMethod(defn.Any_equals, InvokeStyle.Virtual) genCZJUMP(success, failure, Primitives.NE, BOOL, targetIfNoJump) } } diff --git a/compiler/src/dotty/tools/dotc/Compiler.scala b/compiler/src/dotty/tools/dotc/Compiler.scala index 23921f12bae1..d272b70242e4 100644 --- a/compiler/src/dotty/tools/dotc/Compiler.scala +++ b/compiler/src/dotty/tools/dotc/Compiler.scala @@ -60,6 +60,7 @@ class Compiler { List(new InstrumentCoverage) :: // Perform instrumentation for code coverage (if -coverage-out is set) List(new CrossVersionChecks, // Check issues related to deprecated and experimental new FirstTransform, // Some transformations to put trees into a canonical form + new VerifyLastUseAnnotations, // Well actually it has its own tree traverser, so it does not make that much sense to be here new CheckReentrant, // Internal use only: Check that compiled program has no data races involving global vars new ElimPackagePrefixes, // Eliminate references to package prefixes in Select nodes new CookComments, // Cook the comments: expand variables, doc, etc. @@ -143,8 +144,7 @@ class Compiler { new sjs.JUnitBootstrappers, // Generate JUnit-specific bootstrapper classes for Scala.js (not enabled by default) new CollectEntryPoints, // Collect all entry points and save them in the context new CollectSuperCalls, // Find classes that are called with super - new RepeatableAnnotations, - new VerifyLastUseAnnotations) :: // Aggregate repeatable annotations + new RepeatableAnnotations) :: // Aggregate repeatable annotations Nil /** Generate the output of the compilation */ diff --git a/compiler/src/dotty/tools/dotc/core/Definitions.scala b/compiler/src/dotty/tools/dotc/core/Definitions.scala index d9a7db72d382..fe17cea73049 100644 --- a/compiler/src/dotty/tools/dotc/core/Definitions.scala +++ b/compiler/src/dotty/tools/dotc/core/Definitions.scala @@ -1105,7 +1105,6 @@ class Definitions { @tu lazy val StableNullAnnot: ClassSymbol = requiredClass("scala.annotation.stableNull") @tu lazy val LastUseAnnot: ClassSymbol = requiredClass("scala.annotation.lastUse") - @tu lazy val JavaRepeatableAnnot: ClassSymbol = requiredClass("java.lang.annotation.Repeatable") // Initialization annotations diff --git a/compiler/src/dotty/tools/dotc/transform/LambdaLift.scala b/compiler/src/dotty/tools/dotc/transform/LambdaLift.scala index 2e5ac464dab9..3e40944cab84 100644 --- a/compiler/src/dotty/tools/dotc/transform/LambdaLift.scala +++ b/compiler/src/dotty/tools/dotc/transform/LambdaLift.scala @@ -286,10 +286,7 @@ class LambdaLift extends MiniPhase with IdentityDenotTransformer { thisPhase => val lft = lifter if (prefix eq NoPrefix) if (sym.enclosure != lft.currentEnclosure && !sym.isStatic) - if tree.hasAttachment(lastUseAttachment) && sym.is(Flags.Local) then - tree.removeAttachment(lastUseAttachment)//this will still get analyzed bc the posttyper map stores it. I cant drop it. but this is needed to not apply the lastUse - report.error(s"cannot annotate free local variable ${sym.name} @lastUse in a lambda",tree.sourcePos) - (if (sym is Method) lft.memberRef(sym) else lft.proxyRef(sym)).withSpan(tree.span) + (if (sym is Method) lft.memberRef(sym) else lft.proxyRef(sym)).withSpan(tree.span) else if (sym.owner.isClass) // sym was lifted out ref(sym).withSpan(tree.span) else diff --git a/compiler/src/dotty/tools/dotc/transform/PostTyper.scala b/compiler/src/dotty/tools/dotc/transform/PostTyper.scala index 5a378cad47c6..1704317b498c 100644 --- a/compiler/src/dotty/tools/dotc/transform/PostTyper.scala +++ b/compiler/src/dotty/tools/dotc/transform/PostTyper.scala @@ -22,16 +22,19 @@ import NameKinds.WildcardParamName import cc.* import dotty.tools.dotc.transform.MacroAnnotations.hasMacroAnnotation import dotty.tools.dotc.core.NameKinds.DefaultGetterName -import dotty.tools.dotc.util.Property.StickyKey +import dotty.tools.dotc.util.Property.{Key, StickyKey} +import dotty.tools.dotc.transform.PostTyper.enclosingMethod import dotty.tools.dotc.transform.PostTyper.methodLastUses object PostTyper { val name: String = "posttyper" val description: String = "additional checks and cleanups after type checking" - val lastUseAttachment = StickyKey[Unit] //to attach on the variables themselves, which one is lastUsed + val lastUseAttachment = StickyKey[Unit] - // method -> symbols with @lastUse annotation + val enclosingMethod = Key[DefDef[?]] + + /** Attachment on a Method that stores local variables annotated with @lastUse */ val methodLastUses = StickyKey[Set[Symbol]] } @@ -489,13 +492,14 @@ class PostTyper extends MacroTransform with InfoTransformer { thisPhase => val tree1 = cpy.ValDef(tree)(tpt = makeOverrideTypeDeclared(tree.symbol, tree.tpt)) if tree1.removeAttachment(desugar.UntupledParam).isDefined then checkStableSelection(tree.rhs) + processValOrDefDef(super.transform(tree1)) case tree: DefDef => registerIfHasMacroAnnotations(tree) Checking.checkPolyFunctionType(tree.tpt) annotateContextResults(tree) val tree1 = cpy.DefDef(tree)(tpt = makeOverrideTypeDeclared(tree.symbol, tree.tpt)) - processValOrDefDef(superAcc.wrapDefDef(tree1)(super.transform(tree1).asInstanceOf[DefDef])) + processValOrDefDef(superAcc.wrapDefDef(tree1)(super.transform(tree1)(using ctx.withProperty(enclosingMethod, Some(tree1))).asInstanceOf[DefDef]))//I dont think I need a stickykey since i will never copy the defdef itself when managing its components right ? AND I dont need it in the next phases case tree: TypeDef => registerIfHasMacroAnnotations(tree) val sym = tree.symbol @@ -580,27 +584,17 @@ class PostTyper extends MacroTransform with InfoTransformer { thisPhase => val tpe = if tree.isInferred then CleanupRetains()(tree.tpe) else tree.tpe tree.withType(transformAnnotsIn(tpe)) case Typed(t, tpt: TypeTree) if tpt.tpe.hasAnnotation(defn.LastUseAnnot) => - - t match//notice that they look very similar, could be merged ? - case id: Ident => - id.putAttachment(PostTyper.lastUseAttachment, ())//id is muted - val tpt_clean = tpt.withType(tpt.tpe.dropAnnot(defn.LastUseAnnot)) - - if (tree.tpe.typeSymbol.isPrimitiveValueClass || tree.symbol.is(Flags.Method)) - report.error("`@lastUse` annotation cannot be used on primitives and methods", tpt.sourcePos) - - // PostTyper.lastUses.add(t.symbol) - - println(s"source method for sym ${t.symbol.name}: ${ctx.tree}") //ctx.tree is always correct ! - - val others = ctx.tree.getAttachment(methodLastUses).getOrElse(Set.empty) - ctx.tree.putAttachment(methodLastUses, others + t.symbol) - - Typed(id, tpt_clean) - + t match + case _: Ident => + t.putAttachment(PostTyper.lastUseAttachment, ()) + val enclosing = ctx.property(enclosingMethod).get + val others = enclosing.getAttachment(methodLastUses).getOrElse(Set.empty) + //attach the symbol to the method + enclosing.putAttachment(methodLastUses, others + t.symbol) + Typed(t, tpt) case _ => - report.error("`@lastUse` annotation can only be applied on local variables", tpt.srcPos) - tree + report.error("`@lastUse` annotation can only be applied on local variables", tree.srcPos) + Typed(t, tpt) case Typed(Ident(nme.WILDCARD), _) => withMode(Mode.Pattern)(super.transform(tree)) diff --git a/compiler/src/dotty/tools/dotc/transform/VerifyLastUseAnnotations.scala b/compiler/src/dotty/tools/dotc/transform/VerifyLastUseAnnotations.scala index 62b5bcd45de1..78678e3b998e 100644 --- a/compiler/src/dotty/tools/dotc/transform/VerifyLastUseAnnotations.scala +++ b/compiler/src/dotty/tools/dotc/transform/VerifyLastUseAnnotations.scala @@ -19,114 +19,183 @@ import dotty.tools.dotc.util.SourcePosition import dotty.tools.dotc.core.StdNames.nme import dotty.tools.dotc.core.Names.Name import dotty.tools.dotc.transform.PostTyper.methodLastUses +import dotty.tools.dotc.util.Property +import dotty.tools.dotc.transform.VerifyLastUseAnnotations.{VariableStatus, VariableStatusMap} +import dotty.tools.dotc.transform.PostTyper.lastUseAttachment +import dotty.tools.dotc.transform.VerifyLastUseAnnotations.lastUseVariables +import dotty.tools.dotc.transform.VerifyLastUseAnnotations.capturedVariables +object VerifyLastUseAnnotations: + val name: String = "verifyLastUseAnnotations" + val description: String = "verify that @lastUse annotations are use correctly" + + case class VariableStatus(allowVar: Boolean, allowAnnot: Boolean, isAfterValDef: Boolean /*find better name*/){ + + def afterAnnot = VariableStatus(false, allowAnnot, isAfterValDef) + def afterValDef = VariableStatus(allowVar, allowAnnot, true) + def enterLoop = VariableStatus(allowVar, !isAfterValDef, isAfterValDef)//when I enter a loop, I only accept an annotation if did not define the variable yet. this means the variable cannot be annotated many times + def merge(other: VariableStatus) = VariableStatus(allowVar && other.allowVar, allowAnnot && other.allowAnnot, isAfterValDef || other.isAfterValDef)//aftervd should not matter a valdef in a branch has no impact on the outside + } + + //TODO find a better name + type VariableStatusMap = Map[Symbol, VariableStatus] + + extension(ss: VariableStatusMap) + def merge(other: VariableStatusMap): VariableStatusMap = + ss.map((sym, state) => (sym -> state.merge(other(sym)))) + + def enterLoop: VariableStatusMap = + ss.map((sym, state) => (sym -> state.enterLoop)) + + + + object VariableStatus { + def start: VariableStatus = VariableStatus(allowVar = true, allowAnnot = true, isAfterValDef = false) + } + + private val lastUseVariables = Property.Key[Set[Symbol]] + private val capturedVariables = mutable.Map.empty[Symbol, Set[Symbol]] //method => lastUse symbols from outside it is using + + +/* + Verifies the use of the @lastUse annotation + following rules are applied: + 1. the variable cannot be reused after being annotated + 2. the variable cannot be annotated in a loop (or in a lambda) TODO modify + 3. exclusive branches (if-else, match cases) do not impact each other. + + the implementation goes as follows (considering nested functions): + we follow all variables with a @lastUse annotation + PrepareForDefDef: the miniphase dives in the deepest method in the tree, storing in context the lastUse variable from enclosing methods ("captured") + transformDefDef: each method (children first) are verified. If it contains a lastUse captured variable, it is marked as such (in "capturedVariables") + + this allows illegal function calls to be detected, but all function definitions stay legal. +*/ class VerifyLastUseAnnotations extends MiniPhase: override def phaseName: String = VerifyLastUseAnnotations.name override def description: String = VerifyLastUseAnnotations.description + //A TreeAccumulator is more expensive than a miniphase transforms, but I need to do custom information transfer and a miniphase is not flexible enough + private class LastUseVerifier(method: DefDef) extends TreeAccumulator[VariableStatusMap]{ + override def apply(accInfo: VariableStatusMap, tree: Tree)(using Context): VariableStatusMap = traverse(tree)(accInfo) - case class FoldInfo(allowVar: Boolean, allowAnnot: Boolean){ - def afterAnnot = FoldInfo(false, allowAnnot) - def merge(other: FoldInfo) = FoldInfo(allowVar && other.allowVar, allowAnnot) - def inLoop = FoldInfo(allowVar, false) - } + def verify(using Context): Unit = + method.getAttachment(methodLastUses) match + case None if ctx.property(lastUseVariables).get.nonEmpty => + // enclosing method has lastUse variables => check if this one uses any lastUse variable + traverse(method.rhs)(Map.empty) + case None => () + case Some(symbols) => + val ss = symbols.map(s => (s -> VariableStatus.start)).toMap + traverse(method.rhs)(ss) + + private def checkVarValidity(tree: Tree)(using Context): Unit = + if tree.symbol.is(Lazy) then + report.warning("@lastUse annotation on a lazy val does not work (yet)", tree.sourcePos) + + if (tree.tpe.typeSymbol.isPrimitiveValueClass || tree.symbol.is(Flags.Method)) + report.error("`@lastUse` annotation cannot be used on primitives and methods", tree.sourcePos) + if tree.symbol.owner != method.symbol && !tree.isInstanceOf[This] then + report.error(s"@lastUse annotation can only be used in a local context, ${tree.symbol.owner}, ${method.symbol}", tree.sourcePos) - private class LastUseVerifier(target: Symbol, method: Tree) extends TreeAccumulator[FoldInfo]{ - override def apply(accInfo: FoldInfo, tree: Tree)(using Context): FoldInfo = traverse(tree)(accInfo) - def verify(using Context): Unit = - traverse(method)(FoldInfo(true, true)) - private def traverseExclusiveCases(cases: List[Tree])(info: FoldInfo)(using Context): FoldInfo = + private def traverseExclusiveCases(cases: List[Tree])(info: VariableStatusMap)(using Context): VariableStatusMap = cases .map(c => traverse(c)(info))//traverse all of them with same source info .reduce((a,b)=> a.merge(b))//merge all info, return it - private def traverse(tree: Tree)(accInfo: FoldInfo)(using Context): FoldInfo = { + private def traverseCaseDefs(casedefs: List[CaseDef])(accInfo: VariableStatusMap)(using Context): VariableStatusMap = + var mergedBodies = accInfo //contains the output after each body, 'ored' together + var info = accInfo + + casedefs.foreach(cd => + info = traverse(cd.pat)(info) + info = traverse(cd.guard)(info) + val afterBody = traverse(cd.body)(info) + + mergedBodies = mergedBodies.merge(afterBody) + ) + + mergedBodies + + private def traverse(tree: Tree)(accInfo: VariableStatusMap)(using Context): VariableStatusMap = { var info = accInfo//used to mutate and pass the information + val sym = tree.symbol //by default, accInfo is propagated in the order of the arguments. so block is already good, and everything that has linear reading tree match - case _: Ident if tree.symbol == target => - if !info.allowVar then - report.error(s"reuse of variable ${tree.symbol.name} after @lastUse", tree.sourcePos) + case vd: ValDef if info.contains(sym) => // definition of a lastUse variable + val info1 = info(sym) + info = info.updated(sym, info1.afterValDef) + foldOver(info, vd) + + case _: Ident | This(_) if info.contains(sym) => //lastUse variable in current method + val info1 = info(sym) + if !info1.allowVar then + report.error(s"reuse of variable ${tree.symbol.name} after @lastUse", tree.sourcePos) //reuse after annot if tree.hasAttachment(PostTyper.lastUseAttachment) then - if info.allowAnnot then info.afterAnnot else - report.error(s"cannot use @lastUse annotation in a loop", tree.sourcePos) + checkVarValidity(tree) + if info1.allowAnnot then info.updated(sym, info1.afterAnnot) else //annot here + report.error(s"cannot use @lastUse annotation in a loop", tree.sourcePos) //annot here but illegal tree.removeAttachment(PostTyper.lastUseAttachment) - info.afterAnnot + info else info + case _: Ident if ctx.property(lastUseVariables).get.contains(sym) =>//lastUse variable from an enclosing method + val others = capturedVariables.getOrElse(method.symbol, Set.empty[Symbol]) + capturedVariables.update(method.symbol, others + sym) + info + + case _: Ident if capturedVariables.contains(sym) =>// calling a method which uses a lastUse variable + val freeVars = capturedVariables.apply(sym) + freeVars.foreach(fv => + info.get(fv) match + case Some(value) => if !value.allowVar then report.error(s"calling function ${sym.name}, using variable ${fv.name} previously annotated with @lastUse", tree.sourcePos) + case None => //lastUse variable is defined in an enclosing method. mark the current method as using it + val others = capturedVariables.getOrElse(method.symbol, Set.empty[Symbol]) + capturedVariables.update(method.symbol, others + fv) + ) + info + case If(cond, thenp, elsep) => info = traverse(cond)(info) val info_branch1 = traverse(thenp)(info)//both branches are exclusive val info_branch2 = traverse(elsep)(info)//so we combine the info at the end - info_branch1.merge(info_branch2) case WhileDo(cond, body) => - //everything is run in a loop, so I cannot allow a lastUse here - info = info.inLoop - traverse(cond)(info)//I do not care about the result, since I cannot annotate anything inside - traverse(body)(info)//I just need to check if there is any illegal variable use + info = info.enterLoop + info = traverse(cond)(info) + // cannot be annotated or valdef'd inside the loop => no information to propagate further. + traverse(body)(info) case Match(selector, cases) => info = traverse(selector)(info) - - //each case are exclusive, merge at the end - traverseExclusiveCases(cases)(info) + traverseCaseDefs(cases)(info) case Try(expr, cases, finalizer) => info = traverse(expr)(info) - info = traverseExclusiveCases(cases)(info) + info = traverseCaseDefs(cases)(info) traverse(finalizer)(info) - // case Closure(env, _, _) => - // env.find(_.symbol == target) match - // case None => () - // case Some(t) => if(info.allowVar){ - // report.warning(s"creating a lambda using the variable ${target.name} (annotated later with @lastUse) creates a new reference to the variable, making the annotation useless", t.sourcePos) - // report.echo("Note that changing the lambda to a function solves the problem\n") - // } - - - - // foldOver(info, tree) - - // case vd @ ValDef(name, tpt, rhs) => - // def maybeAlias(tree: Tree): Boolean = tree match//short check that it is not creating an obvious alias - // case Ident(_) => tree.symbol == target - // case Block(_, expr) => maybeAlias(expr) - // case Typed(expr, _) => maybeAlias(expr) - // case _ => false - - // if maybeAlias(vd.rhs) then - // report.warning("Creating a possible alias to @lastUse annotated variable. this will keep the object alive longer", tree.sourcePos) - - // foldOver(info, tree)//maybe creating a closure - - // info - + case DefDef(name, paramss, tpt, preRhs) => info//already checked, do not enter it case _ => foldOver(info, tree)//visit other trees } } - override def transformDefDef(method: DefDef)(using Context): Tree = - if method.hasAttachment(methodLastUses) then - val lastUses = method.getAttachment(methodLastUses).get + override def prepareForDefDef(tree: DefDef)(using Context): Context = + val freeVars = ctx.property(lastUseVariables).getOrElse(Set.empty[Symbol]) ++ tree.getAttachment(methodLastUses).getOrElse(Set.empty[Symbol]) + ctx.withProperty(lastUseVariables, Some(freeVars)) - lastUses.foreach(sym => - val verifier = LastUseVerifier(sym, method) - verifier.verify - ) - method + override def transformDefDef(mdef: DefDef)(using Context): Tree = + LastUseVerifier(mdef).verify + mdef + -object VerifyLastUseAnnotations: - val name: String = "verifyLastUseAnnotations" - val description: String = "verify that @lastUse annotations are use correctly" diff --git a/library/src/scala/annotation/lastUse.scala b/library/src/scala/annotation/lastUse.scala index 24352ed95696..66837eecf604 100644 --- a/library/src/scala/annotation/lastUse.scala +++ b/library/src/scala/annotation/lastUse.scala @@ -5,4 +5,4 @@ import java.lang.annotation.{Target} /** An annotation that indicates that an element will never be used again after this evaluation, and can be removed from the stack by force */ -final class lastUse extends annotation.StaticAnnotation +final class lastUse extends scala.annotation.StaticAnnotation diff --git a/project/Build.scala b/project/Build.scala index 38d9520d1e87..e87e25fc1a3b 100644 --- a/project/Build.scala +++ b/project/Build.scala @@ -1142,6 +1142,7 @@ object Build { file(s"${baseDirectory.value}/src/scala/annotation/unroll.scala"), file(s"${baseDirectory.value}/src/scala/annotation/targetName.scala"), file(s"${baseDirectory.value}/src/scala/annotation/stableNull.scala"), + file(s"${baseDirectory.value}/src/scala/annotation/lastUse.scala"), file(s"${baseDirectory.value}/src/scala/deriving/Mirror.scala"), file(s"${baseDirectory.value}/src/scala/compiletime/package.scala"), file(s"${baseDirectory.value}/src/scala/quoted/Type.scala"), @@ -1279,7 +1280,8 @@ object Build { file(s"${baseDirectory.value}/src/scala/annotation/init.scala"), file(s"${baseDirectory.value}/src/scala/annotation/unroll.scala"), file(s"${baseDirectory.value}/src/scala/annotation/targetName.scala"), - file(s"${baseDirectory.value}/src/scala/annotation/stableNull.scala"), + file(s"${baseDirectory.value}/src/scala/annotation/stableNull.scala"), + file(s"${baseDirectory.value}/src/scala/annotation/lastUse.scala"), file(s"${baseDirectory.value}/src/scala/deriving/Mirror.scala"), file(s"${baseDirectory.value}/src/scala/compiletime/package.scala"), file(s"${baseDirectory.value}/src/scala/quoted/Type.scala"), diff --git a/tests/neg/early-reclamation/verifier.scala b/tests/neg/early-reclamation/verifier.scala index b660b8137d89..39b97a355e0d 100644 --- a/tests/neg/early-reclamation/verifier.scala +++ b/tests/neg/early-reclamation/verifier.scala @@ -3,11 +3,14 @@ import scala.annotation.lastUse class A object Verif: + val global = A() + def test1 = val a = A() println(a: @lastUse) - println(a) // error + println(a) // error + def test2 = val a = A() @@ -53,10 +56,28 @@ object Verif: val f = () => a // error - def test8 = + def test8 = val a = A() + + def f = + def g = println(a) + g + + println(a: @lastUse) - val f = () => a + f // error + + def test9 = + val l = List(1,2) + val i = 1 + def f = () => 1 + + val j = i: @lastUse // error + val g = f: @lastUse // error + val g2 = global: @lastUse // error + + def test10 = + val a = A() + + def f = println(a: @lastUse) // error - println(a: @lastUse) - f() \ No newline at end of file diff --git a/tests/pos/early-reclamation/verifier.scala b/tests/pos/early-reclamation/verifier.scala index 33abdaf94420..c2605df7c7e9 100644 --- a/tests/pos/early-reclamation/verifier.scala +++ b/tests/pos/early-reclamation/verifier.scala @@ -7,7 +7,7 @@ class Obj{} object Verif: private def use(o: Obj): Unit = - val x = o + println(o) def test1 = @@ -16,18 +16,46 @@ object Verif: use(o) val y = o use(o) - use(o: @lastUse) + use(o: @lastUse) def test2 = val o = Obj() use(o) if "hi".length() == 2 then - use(o: @lastUse) + use(o: @lastUse) else if "hello".length == 3 then use(o) else - use(o: @lastUse) + use(o: @lastUse) - // what else ??? \ No newline at end of file + def test3 = + val o = Obj() + + use(o) + + val x = Some(1) + + x match + case None => + use(o: @lastUse) + case Some(x) => + use(o: @lastUse) + + def test4 = + val o = Obj() + + def test5 = + val o = Obj() + + f() + + use(o) + + def f() = + use(o) + + + + \ No newline at end of file