diff --git a/.gitignore b/.gitignore index 7ee4342439be..aa5ce1d5baa0 100644 --- a/.gitignore +++ b/.gitignore @@ -62,7 +62,6 @@ testlogs/ # Put local stuff here local/ -compiler/test/debug/Gen.jar /bin/.cp diff --git a/compiler/src/dotty/tools/debug/ExpressionCompiler.scala b/compiler/src/dotty/tools/debug/ExpressionCompiler.scala new file mode 100644 index 000000000000..83c20b0f54a7 --- /dev/null +++ b/compiler/src/dotty/tools/debug/ExpressionCompiler.scala @@ -0,0 +1,31 @@ +package dotty.tools.debug + +import dotty.tools.dotc.Compiler +import dotty.tools.dotc.core.Contexts.Context +import dotty.tools.dotc.core.Phases.Phase +import dotty.tools.dotc.transform.ElimByName + +/** + * The expression compiler powers the debug console in Metals and the IJ Scala plugin, + * enabling evaluation of arbitrary Scala expressions at runtime (even macros). + * It produces class files that can be loaded by the running Scala program, + * to compute the evaluation output. + * + * To do so, it extends the Compiler with 3 phases: + * - InsertExpression: parses and inserts the expression in the original source tree + * - ExtractExpression: extract the typed expression and places it in the new expression class + * - ResolveReflectEval: resolves local variables or inacessible members using reflection calls + */ +class ExpressionCompiler(config: ExpressionCompilerConfig) extends Compiler: + + override protected def frontendPhases: List[List[Phase]] = + val parser :: others = super.frontendPhases: @unchecked + parser :: List(InsertExpression(config)) :: others + + override protected def transformPhases: List[List[Phase]] = + val store = ExpressionStore() + // the ExtractExpression phase should be after ElimByName and ExtensionMethods, and before LambdaLift + val transformPhases = super.transformPhases + val index = transformPhases.indexWhere(_.exists(_.phaseName == ElimByName.name)) + val (before, after) = transformPhases.splitAt(index + 1) + (before :+ List(ExtractExpression(config, store))) ++ (after :+ List(ResolveReflectEval(config, store))) diff --git a/compiler/src/dotty/tools/debug/ExpressionCompilerBridge.scala b/compiler/src/dotty/tools/debug/ExpressionCompilerBridge.scala new file mode 100644 index 000000000000..3ffbdc860abe --- /dev/null +++ b/compiler/src/dotty/tools/debug/ExpressionCompilerBridge.scala @@ -0,0 +1,38 @@ +package dotty.tools.debug + +import java.nio.file.Path +import java.util.function.Consumer +import java.{util => ju} +import scala.jdk.CollectionConverters.* +import scala.util.control.NonFatal +import dotty.tools.dotc.reporting.StoreReporter +import dotty.tools.dotc.core.Contexts.Context +import dotty.tools.dotc.Driver + +class ExpressionCompilerBridge: + def run( + outputDir: Path, + classPath: String, + options: Array[String], + sourceFile: Path, + config: ExpressionCompilerConfig + ): Boolean = + val args = Array( + "-d", + outputDir.toString, + "-classpath", + classPath, + "-Yskip:pureStats" + // Debugging: Print the tree after phases of the debugger + // "-Vprint:insert-expression,resolve-reflect-eval", + ) ++ options :+ sourceFile.toString + val driver = new Driver: + protected override def newCompiler(using Context): ExpressionCompiler = ExpressionCompiler(config) + val reporter = ExpressionReporter(error => config.errorReporter.accept(error)) + try + driver.process(args, reporter) + !reporter.hasErrors + catch + case NonFatal(cause) => + cause.printStackTrace() + throw cause diff --git a/compiler/src/dotty/tools/debug/ExpressionCompilerConfig.scala b/compiler/src/dotty/tools/debug/ExpressionCompilerConfig.scala new file mode 100644 index 000000000000..895489143f9e --- /dev/null +++ b/compiler/src/dotty/tools/debug/ExpressionCompilerConfig.scala @@ -0,0 +1,65 @@ +package dotty.tools.debug + +import dotty.tools.dotc.ast.tpd.* +import dotty.tools.dotc.core.Symbols.* +import dotty.tools.dotc.core.Types.* +import dotty.tools.dotc.core.Names.* +import dotty.tools.dotc.core.Flags.* +import dotty.tools.dotc.core.Contexts.* +import dotty.tools.dotc.core.SymUtils + +import java.{util => ju} +import ju.function.Consumer + +class ExpressionCompilerConfig private[debug] ( + packageName: String, + outputClassName: String, + private[debug] val breakpointLine: Int, + private[debug] val expression: String, + private[debug] val localVariables: ju.Set[String], + private[debug] val errorReporter: Consumer[String], + private[debug] val testMode: Boolean +): + def this() = this( + packageName = "", + outputClassName = "", + breakpointLine = -1, + expression = "", + localVariables = ju.Collections.emptySet, + errorReporter = _ => (), + testMode = false, + ) + + def withPackageName(packageName: String): ExpressionCompilerConfig = copy(packageName = packageName) + def withOutputClassName(outputClassName: String): ExpressionCompilerConfig = copy(outputClassName = outputClassName) + def withBreakpointLine(breakpointLine: Int): ExpressionCompilerConfig = copy(breakpointLine = breakpointLine) + def withExpression(expression: String): ExpressionCompilerConfig = copy(expression = expression) + def withLocalVariables(localVariables: ju.Set[String]): ExpressionCompilerConfig = copy(localVariables = localVariables) + def withErrorReporter(errorReporter: Consumer[String]): ExpressionCompilerConfig = copy(errorReporter = errorReporter) + + private[debug] val expressionTermName: TermName = termName(outputClassName.toLowerCase.toString) + private[debug] val expressionClassName: TypeName = typeName(outputClassName) + + private[debug] def expressionClass(using Context): ClassSymbol = + if packageName.isEmpty then requiredClass(outputClassName) + else requiredClass(s"$packageName.$outputClassName") + + private[debug] def evaluateMethod(using Context): Symbol = + expressionClass.info.decl(termName("evaluate")).symbol + + private def copy( + packageName: String = packageName, + outputClassName: String = outputClassName, + breakpointLine: Int = breakpointLine, + expression: String = expression, + localVariables: ju.Set[String] = localVariables, + errorReporter: Consumer[String] = errorReporter, + ) = new ExpressionCompilerConfig( + packageName, + outputClassName, + breakpointLine, + expression, + localVariables, + errorReporter, + testMode + ) diff --git a/compiler/src/dotty/tools/debug/ExpressionReporter.scala b/compiler/src/dotty/tools/debug/ExpressionReporter.scala new file mode 100644 index 000000000000..1de1c37bae15 --- /dev/null +++ b/compiler/src/dotty/tools/debug/ExpressionReporter.scala @@ -0,0 +1,17 @@ +package dotty.tools.debug + +import dotty.tools.dotc.core.Contexts.* +import dotty.tools.dotc.reporting.AbstractReporter +import dotty.tools.dotc.reporting.Diagnostic + +private class ExpressionReporter(reportError: String => Unit) extends AbstractReporter: + override def doReport(dia: Diagnostic)(using Context): Unit = + // Debugging: println(messageAndPos(dia)) + dia match + case error: Diagnostic.Error => + val newPos = error.pos.source.positionInUltimateSource(error.pos) + val errorWithNewPos = new Diagnostic.Error(error.msg, newPos) + reportError(stripColor(messageAndPos(errorWithNewPos))) + case _ => + // TODO report the warnings in the expression + () diff --git a/compiler/src/dotty/tools/debug/ExpressionStore.scala b/compiler/src/dotty/tools/debug/ExpressionStore.scala new file mode 100644 index 000000000000..3151c43b9a7a --- /dev/null +++ b/compiler/src/dotty/tools/debug/ExpressionStore.scala @@ -0,0 +1,24 @@ +package dotty.tools.debug + +import dotty.tools.dotc.ast.tpd.* +import dotty.tools.dotc.core.Symbols.* +import dotty.tools.dotc.core.Types.* +import dotty.tools.dotc.core.Names.* +import dotty.tools.dotc.core.Flags.* +import dotty.tools.dotc.core.Contexts.* +import dotty.tools.dotc.core.SymUtils + +private class ExpressionStore: + var symbol: TermSymbol | Null = null + // To resolve captured variables, we store: + // - All classes in the chain of owners of the expression + // - The first local method enclosing the expression + var classOwners: Seq[ClassSymbol] = Seq.empty + var capturingMethod: Option[TermSymbol] = None + + def store(exprSym: Symbol)(using Context): Unit = + symbol = exprSym.asTerm + classOwners = exprSym.ownersIterator.collect { case cls: ClassSymbol => cls }.toSeq + capturingMethod = exprSym.ownersIterator + .find(sym => (sym.isClass || sym.is(Method)) && sym.enclosure.is(Method)) // the first local class or method + .collect { case sym if sym.is(Method) => sym.asTerm } // if it is a method diff --git a/compiler/src/dotty/tools/debug/ExtractExpression.scala b/compiler/src/dotty/tools/debug/ExtractExpression.scala new file mode 100644 index 000000000000..151d75270c6e --- /dev/null +++ b/compiler/src/dotty/tools/debug/ExtractExpression.scala @@ -0,0 +1,368 @@ +package dotty.tools.debug + +import dotty.tools.dotc.core.SymUtils +import dotty.tools.dotc.ast.tpd.* +import dotty.tools.dotc.core.Constants.Constant +import dotty.tools.dotc.core.Contexts.* +import dotty.tools.dotc.core.Flags.* +import dotty.tools.dotc.core.Names.* +import dotty.tools.dotc.core.Symbols.* +import dotty.tools.dotc.core.Types.* +import dotty.tools.dotc.core.DenotTransformers.DenotTransformer +import dotty.tools.dotc.core.Denotations.SingleDenotation +import dotty.tools.dotc.core.SymDenotations.SymDenotation +import dotty.tools.dotc.transform.MacroTransform +import dotty.tools.dotc.core.Phases.* +import dotty.tools.dotc.report +import dotty.tools.dotc.util.SrcPos +import scala.annotation.nowarn + +/** + * This phase extracts the typed expression from the source tree, transfoms it and places it + * in the evaluate method of the Expression class. + * + * Before: + * package example: + * class A: + * def m: T = + * val expression = + * println("") + * typed_expr + * body + * + * class Expression(thisObject: Any, names: Array[String], values: Array[Any]): + * def evaluate(): Any = () + * + * After: + * package example: + * class A: + * def m: T = body + * + * class Expression(thisObject: Any, names: Array[String], values: Array[Any]): + * def evaluate(): Any = + { + * transformed_expr + * } + * + * Every access to a local variable, or an inaccessible member is transformed into a temporary reflectEval call. + * A ReflectEvalStrategy is attached to each reflectEval call to describe what should be evaluated and how. + * When printing trees for debugging, the ReflectEvalStrategy appears as a String literal argument. + * + * Examples: + * + * 1. Get local variable `a`: + * reflectEval(null, "ReflectEvalStrategy.LocalValue(a)", []) + * + * 2. Call private method `a.m(x1, x2)`: + * reflectEval(a, "ReflectEvalStrategy.MethodCall(m)", [x1, x2]) + * + * 3. Set private field `a.b = c`: + * reflectEval(a, "ReflectEvalStrategy.FieldAssign(b)", [c]) + * + * etc + * + */ +private class ExtractExpression( + config: ExpressionCompilerConfig, + expressionStore: ExpressionStore +) extends MacroTransform with DenotTransformer: + override def phaseName: String = ExtractExpression.name + + /** Update the owner of the symbols inserted into `evaluate`. */ + override def transform(ref: SingleDenotation)(using Context): SingleDenotation = + ref match + case ref: SymDenotation if isExpressionVal(ref.symbol.maybeOwner) => + // update owner of the symDenotation, e.g. local vals + // that are extracted out of the expression val to the evaluate method + ref.copySymDenotation(owner = config.evaluateMethod) + case _ => + ref + + override def transformPhase(using Context): Phase = this.next + + override protected def newTransformer(using Context): Transformer = new ExtractExpressionTransformer + + private class ExtractExpressionTransformer extends Transformer: + private var expressionTree: Tree | Null = null + override def transform(tree: Tree)(using Context): Tree = + tree match + case PackageDef(pid, stats) => + val (evaluationClassDef, others) = stats.partition(_.symbol == config.expressionClass) + val transformedStats = (others ++ evaluationClassDef).map(transform) + cpy.PackageDef(tree)(pid, transformedStats) + case tree: ValDef if isExpressionVal(tree.symbol) => + expressionTree = tree.rhs + expressionStore.store(tree.symbol) + unitLiteral + case tree: DefDef if tree.symbol == config.evaluateMethod => + val transformedExpr = ExpressionTransformer.transform(expressionTree.nn) + cpy.DefDef(tree)(rhs = transformedExpr) + case tree => + super.transform(tree) + + private object ExpressionTransformer extends TreeMap: + override def transform(tree: Tree)(using Context): Tree = + tree match + case tree: ImportOrExport => tree + case tree if isLocalToExpression(tree.symbol) => super.transform(tree) + + // static object + case tree: (Ident | Select | This) if isStaticObject(tree.symbol) => + getStaticObject(tree)(tree.symbol.moduleClass) + + // non static this or outer this + case tree: This if !tree.symbol.is(Package) => + thisOrOuterValue(tree)(tree.symbol.enclosingClass.asClass) + + // non-static object + case tree: (Ident | Select) if isNonStaticObject(tree.symbol) => + callMethod(tree)(getTransformedQualifier(tree), tree.symbol.asTerm, Nil) + + // local variable + case tree: Ident if isLocalVariable(tree.symbol) => + if tree.symbol.is(Lazy) then + report.error(s"Evaluation of local lazy val not supported", tree.srcPos) + tree + else + getCapturer(tree.symbol.asTerm) match + case Some(capturer) => + if capturer.isClass then getClassCapture(tree)(tree.symbol, capturer.asClass) + else getMethodCapture(tree)(tree.symbol, capturer.asTerm) + case None => getLocalValue(tree)(tree.symbol) + + // assignment to local variable + case tree @ Assign(lhs, _) if isLocalVariable(lhs.symbol) => + val variable = lhs.symbol.asTerm + val rhs = transform(tree.rhs) + getCapturer(variable) match + case Some(capturer) => + if capturer.isClass then setClassCapture(tree)(variable, capturer.asClass, rhs) + else setMethodCapture(tree)(variable, capturer.asTerm, rhs) + case None => setLocalValue(tree)(variable, rhs) + + // inaccessible fields + case tree: (Ident | Select) if tree.symbol.isField && !isAccessibleMember(tree) => + if tree.symbol.is(JavaStatic) then getField(tree)(nullLiteral, tree.symbol.asTerm) + else getField(tree)(getTransformedQualifier(tree), tree.symbol.asTerm) + + // assignment to inaccessible fields + case tree @ Assign(lhs, rhs) if lhs.symbol.isField && !isAccessibleMember(lhs) => + if lhs.symbol.is(JavaStatic) then setField(tree)(nullLiteral, lhs.symbol.asTerm, transform(rhs)) + else setField(tree)(getTransformedQualifier(lhs), lhs.symbol.asTerm, transform(rhs)) + + // inaccessible constructors + case tree: (Select | Apply | TypeApply) + if tree.symbol.isConstructor && (!tree.symbol.owner.isStatic || !isAccessibleMember(tree)) => + callConstructor(tree)(getTransformedQualifierOfNew(tree), tree.symbol.asTerm, getTransformedArgs(tree)) + + // inaccessible methods + case tree: (Ident | Select | Apply | TypeApply) if tree.symbol.isRealMethod && !isAccessibleMember(tree) => + val args = getTransformedArgs(tree) + if tree.symbol.is(JavaStatic) then callMethod(tree)(nullLiteral, tree.symbol.asTerm, args) + else callMethod(tree)(getTransformedQualifier(tree), tree.symbol.asTerm, args) + + // accessible members + case tree: (Ident | Select) if !tree.symbol.isStatic => + val qualifier = getTransformedQualifier(tree) + val qualifierType = widenDealiasQualifierType(tree) + val castQualifier = if qualifier.tpe <:< qualifierType then qualifier else + qualifier.select(defn.Any_asInstanceOf).appliedToType(qualifierType) + cpy.Select(tree)(castQualifier, tree.name) + + case Typed(tree, tpt) if tpt.symbol.isType && !isTypeAccessible(tpt.tpe) => transform(tree) + case tree => super.transform(tree) + + private def getCapturer(variable: TermSymbol)(using Context): Option[Symbol] = + // a local variable can be captured by a class or method + val candidates = expressionStore.symbol.nn.ownersIterator + .takeWhile(_ != variable.owner) + .filter(s => s.isClass || s.is(Method)) + .toSeq + candidates + .findLast(_.isClass) + .orElse(candidates.find(_.is(Method))) + + private def getTransformedArgs(tree: Tree)(using Context): List[Tree] = + tree match + case _: (Ident | Select) => Nil + case Apply(fun, args) => getTransformedArgs(fun) ++ args.map(transform) + case TypeApply(fun, _) => getTransformedArgs(fun) + + private def getTransformedQualifier(tree: Tree)(using Context): Tree = + tree match + case Ident(_) => + tree.tpe match + case TermRef(NoPrefix, _) => + // it's a local method, it can capture its outer value + thisOrOuterValue(tree)(tree.symbol.enclosingClass.asClass) + case TermRef(prefix: NamedType, _) => transform(ref(prefix)) + case TermRef(prefix: ThisType, _) => transform(This(prefix.cls)) + case Select(qualifier, _) => transform(qualifier) + case Apply(fun, _) => getTransformedQualifier(fun) + case TypeApply(fun, _) => getTransformedQualifier(fun) + + private def getTransformedQualifierOfNew(tree: Tree)(using Context): Tree = + tree match + case Select(New(tpt), _) => getTransformedPrefix(tpt) + case Apply(fun, _) => getTransformedQualifierOfNew(fun) + case TypeApply(fun, _) => getTransformedQualifierOfNew(fun) + + private def getTransformedPrefix(typeTree: Tree)(using Context): Tree = + def transformPrefix(prefix: Type): Tree = + prefix match + case NoPrefix => + // it's a local class, it can capture its outer value + thisOrOuterValue(typeTree)(typeTree.symbol.owner.enclosingClass.asClass) + case prefix: ThisType => thisOrOuterValue(typeTree)(prefix.cls) + case ref: TermRef => transform(Ident(ref).withSpan(typeTree.span)) + def rec(tpe: Type): Tree = + tpe match + case TypeRef(prefix, _) => transformPrefix(prefix) + case AppliedType(tycon, _) => rec(tycon) + rec(typeTree.tpe) + end ExpressionTransformer + + private def isExpressionVal(sym: Symbol)(using Context): Boolean = + sym.name == config.expressionTermName + + // symbol can be a class or a method + private def thisOrOuterValue(tree: Tree)(cls: ClassSymbol)(using Context): Tree = + val ths = getThis(tree)(expressionStore.classOwners.head) + val target = expressionStore.classOwners.indexOf(cls) + if target >= 0 then + expressionStore.classOwners + .drop(1) + .take(target) + .foldLeft(ths) { (innerObj, outerSym) => + if innerObj == ths && config.localVariables.contains("$outer") then getLocalOuter(tree)(outerSym) + else getOuter(tree)(innerObj, outerSym) + } + else nullLiteral + + private def getThis(tree: Tree)(cls: ClassSymbol)(using Context): Tree = + reflectEval(tree)(nullLiteral, ReflectEvalStrategy.This(cls), Nil) + + private def getLocalOuter(tree: Tree)(outerCls: ClassSymbol)(using Context): Tree = + val strategy = ReflectEvalStrategy.LocalOuter(outerCls) + reflectEval(tree)(nullLiteral, strategy, Nil) + + private def getOuter(tree: Tree)(qualifier: Tree, outerCls: ClassSymbol)(using Context): Tree = + val strategy = ReflectEvalStrategy.Outer(outerCls) + reflectEval(tree)(qualifier, strategy, Nil) + + private def getLocalValue(tree: Tree)(variable: Symbol)(using Context): Tree = + val isByName = isByNameParam(variable.info) + val strategy = ReflectEvalStrategy.LocalValue(variable.asTerm, isByName) + reflectEval(tree)(nullLiteral, strategy, Nil) + + private def isByNameParam(tpe: Type)(using Context): Boolean = + tpe match + case _: ExprType => true + case ref: TermRef => isByNameParam(ref.symbol.info) + case _ => false + + private def setLocalValue(tree: Tree)(variable: Symbol, rhs: Tree)(using Context): Tree = + val strategy = ReflectEvalStrategy.LocalValueAssign(variable.asTerm) + reflectEval(tree)(nullLiteral, strategy, List(rhs)) + + private def getClassCapture(tree: Tree)(variable: Symbol, cls: ClassSymbol)(using Context): Tree = + val byName = isByNameParam(variable.info) + val strategy = ReflectEvalStrategy.ClassCapture(variable.asTerm, cls, byName) + val qualifier = thisOrOuterValue(tree)(cls) + reflectEval(tree)(qualifier, strategy, Nil) + + private def setClassCapture(tree: Tree)(variable: Symbol, cls: ClassSymbol, value: Tree)(using Context) = + val strategy = ReflectEvalStrategy.ClassCaptureAssign(variable.asTerm, cls) + val qualifier = thisOrOuterValue(tree)(cls) + reflectEval(tree)(qualifier, strategy, List(value)) + + private def getMethodCapture(tree: Tree)(variable: Symbol, method: TermSymbol)(using Context): Tree = + val isByName = isByNameParam(variable.info) + val strategy = + ReflectEvalStrategy.MethodCapture(variable.asTerm, method.asTerm, isByName) + reflectEval(tree)(nullLiteral, strategy, Nil) + + private def setMethodCapture(tree: Tree)(variable: Symbol, method: Symbol, value: Tree)(using Context) = + val strategy = + ReflectEvalStrategy.MethodCaptureAssign(variable.asTerm, method.asTerm) + reflectEval(tree)(nullLiteral, strategy, List(value)) + + private def getStaticObject(tree: Tree)(obj: Symbol)(using ctx: Context): Tree = + val strategy = ReflectEvalStrategy.StaticObject(obj.asClass) + reflectEval(tree)(nullLiteral, strategy, Nil) + + private def getField(tree: Tree)(qualifier: Tree, field: TermSymbol)(using Context): Tree = + val byName = isByNameParam(field.info) + val strategy = ReflectEvalStrategy.Field(field, byName) + reflectEval(tree)(qualifier, strategy, Nil) + + private def setField(tree: Tree)(qualifier: Tree, field: TermSymbol, rhs: Tree)(using Context): Tree = + val strategy = ReflectEvalStrategy.FieldAssign(field) + reflectEval(tree)(qualifier, strategy, List(rhs)) + + private def callMethod(tree: Tree)(qualifier: Tree, method: TermSymbol, args: List[Tree])(using Context): Tree = + val strategy = ReflectEvalStrategy.MethodCall(method) + reflectEval(tree)(qualifier, strategy, args) + + private def callConstructor(tree: Tree)(qualifier: Tree, ctr: TermSymbol, args: List[Tree])(using Context): Tree = + val strategy = ReflectEvalStrategy.ConstructorCall(ctr, ctr.owner.asClass) + reflectEval(tree)(qualifier, strategy, args) + + private def reflectEval(tree: Tree)( + qualifier: Tree, + strategy: ReflectEvalStrategy, + args: List[Tree] + )(using Context): Tree = + val evalArgs = List( + qualifier, + Literal(Constant(strategy.toString)), // only useful for debugging, when printing trees + JavaSeqLiteral(args, TypeTree(ctx.definitions.ObjectType)) + ) + cpy + .Apply(tree)(Select(This(config.expressionClass), termName("reflectEval")), evalArgs) + .withAttachment(ReflectEvalStrategy, strategy) + + private def isStaticObject(symbol: Symbol)(using Context): Boolean = + symbol.is(Module) && symbol.isStatic && !symbol.is(JavaDefined) && !symbol.isRoot + + private def isNonStaticObject(symbol: Symbol)(using Context): Boolean = + symbol.is(Module) && !symbol.isStatic && !symbol.isRoot + + private def isLocalVariable(symbol: Symbol)(using Context): Boolean = + !symbol.is(Method) && symbol.isLocalToBlock + + /** Check if a term is accessible from the expression class. + * At this phase, there is no need test privateWithin, as it will no longer be checked. + * This eliminates the need to use reflection to evaluate privateWithin members, + * which would otherwise degrade performances. + */ + private def isAccessibleMember(tree: Tree)(using Context): Boolean = + val symbol = tree.symbol + symbol.owner.isType && + !symbol.isPrivate && + !symbol.is(Protected) && + isTypeAccessible(widenDealiasQualifierType(tree)) + + private def widenDealiasQualifierType(tree: Tree)(using Context): Type = + tree match + case Ident(_) => tree.symbol.enclosingClass.thisType.widenDealias + case Select(qualifier, _) => qualifier.tpe.widenDealias + case Apply(fun, _) => widenDealiasQualifierType(fun) + case TypeApply(fun, _) => widenDealiasQualifierType(fun) + + // Check if a type is accessible from the expression class + private def isTypeAccessible(tpe: Type)(using Context): Boolean = + def isPublic(sym: Symbol): Boolean = + !sym.isLocal && (sym.isPublic || sym.privateWithin.is(PackageClass)) + tpe.forallParts { + case tpe: NamedType if tpe.symbol != NoSymbol => + isLocalToExpression(tpe.symbol) || isPublic(tpe.symbol) + case _ => true + } + + private def isLocalToExpression(symbol: Symbol)(using Context): Boolean = + val evaluateMethod = config.evaluateMethod + symbol.ownersIterator.exists(_ == evaluateMethod) + +private object ExtractExpression: + val name: String = "extractExpression" diff --git a/compiler/src/dotty/tools/debug/InsertExpression.scala b/compiler/src/dotty/tools/debug/InsertExpression.scala new file mode 100644 index 000000000000..17d2d7f6ea92 --- /dev/null +++ b/compiler/src/dotty/tools/debug/InsertExpression.scala @@ -0,0 +1,252 @@ +package dotty.tools.debug + +import dotty.tools.dotc.ast.untpd.* +import dotty.tools.dotc.core.Constants.Constant +import dotty.tools.dotc.core.Contexts.* +import dotty.tools.dotc.core.Names.* +import dotty.tools.dotc.core.Phases.Phase +import dotty.tools.dotc.parsing.Parsers +import dotty.tools.dotc.report +import dotty.tools.dotc.transform.MegaPhase.MiniPhase +import dotty.tools.dotc.util.NoSourcePosition +import dotty.tools.dotc.util.SourceFile +import dotty.tools.dotc.util.SourcePosition +import dotty.tools.dotc.util.Spans.Span +import dotty.tools.dotc.util.SrcPos +import dotty.tools.io.VirtualFile + +import java.nio.charset.StandardCharsets + +/** + * This phase inserts the expression being evaluated at the line of the breakpoint + * and inserts the expression class in the same package (so that it can access package private symbols). + * + * Before: + * package example: + * class A: + * def m: T = + * body // breakpoint here + * + * After: + * package example: + * class A: + * def m: T = + * val expression = + * println("") // effect, to prevent constant-folding + * expr // inserted expression + * body // breakpoint here + * + * class Expression(thisObject: Any, names: Array[String], values: Array[Any]): + * def evaluate(): Any = () + * + */ +private class InsertExpression(config: ExpressionCompilerConfig) extends Phase: + private var expressionInserted = false + + override def phaseName: String = InsertExpression.name + override def isCheckable: Boolean = false + + // TODO move reflection methods (callMethod, getField, etc) to scala3-library + // under scala.runtime (or scala.debug?) to avoid recompiling them again and again + private val expressionClassSource = + s"""|class ${config.expressionClassName}(thisObject: Any, names: Array[String], values: Array[Any]) { + | import java.lang.reflect.InvocationTargetException + | val classLoader = getClass.getClassLoader + | + | def evaluate(): Any = + | () + | + | def getThisObject(): Any = thisObject + | + | def getLocalValue(name: String): Any = { + | val idx = names.indexOf(name) + | if idx == -1 then throw new NoSuchElementException(name) + | else values(idx) + | } + | + | def setLocalValue(name: String, value: Any): Any = { + | val idx = names.indexOf(name) + | if idx == -1 then throw new NoSuchElementException(name) + | else values(idx) = value + | } + | + | def callMethod(obj: Any, className: String, methodName: String, paramTypesNames: Array[String], returnTypeName: String, args: Array[Object]): Any = { + | val clazz = classLoader.loadClass(className) + | val method = clazz.getDeclaredMethods + | .find { m => + | m.getName == methodName && + | m.getReturnType.getName == returnTypeName && + | m.getParameterTypes.map(_.getName).toSeq == paramTypesNames.toSeq + | } + | .getOrElse(throw new NoSuchMethodException(methodName)) + | method.setAccessible(true) + | val res = unwrapException(method.invoke(obj, args*)) + | if returnTypeName == "void" then () else res + | } + | + | def callConstructor(className: String, paramTypesNames: Array[String], args: Array[Object]): Any = { + | val clazz = classLoader.loadClass(className) + | val constructor = clazz.getConstructors + | .find { c => c.getParameterTypes.map(_.getName).toSeq == paramTypesNames.toSeq } + | .getOrElse(throw new NoSuchMethodException(s"new $$className")) + | constructor.setAccessible(true) + | unwrapException(constructor.newInstance(args*)) + | } + | + | def getField(obj: Any, className: String, fieldName: String): Any = { + | val clazz = classLoader.loadClass(className) + | val field = clazz.getDeclaredField(fieldName) + | field.setAccessible(true) + | field.get(obj) + | } + | + | def setField(obj: Any, className: String, fieldName: String, value: Any): Any = { + | val clazz = classLoader.loadClass(className) + | val field = clazz.getDeclaredField(fieldName) + | field.setAccessible(true) + | field.set(obj, value) + | } + | + | def getOuter(obj: Any, outerTypeName: String): Any = { + | val clazz = obj.getClass + | val field = getSuperclassIterator(clazz) + | .flatMap(_.getDeclaredFields.toSeq) + | .find { field => field.getName == "$$outer" && field.getType.getName == outerTypeName } + | .getOrElse(throw new NoSuchFieldException("$$outer")) + | field.setAccessible(true) + | field.get(obj) + | } + | + | def getStaticObject(className: String): Any = { + | val clazz = classLoader.loadClass(className) + | val field = clazz.getDeclaredField("MODULE$$") + | field.setAccessible(true) + | field.get(null) + | } + | + | def getSuperclassIterator(clazz: Class[?]): Iterator[Class[?]] = + | Iterator.iterate(clazz: Class[?] | Null)(_.nn.getSuperclass) + | .takeWhile(_ != null) + | .map(_.nn) + | + | // A fake method that is used as a placeholder in the extract-expression phase. + | // The resolve-reflect-eval phase resolves it to a call of one of the other methods in this class. + | def reflectEval(qualifier: Object, term: String, args: Array[Object]): Any = ??? + | + | private def unwrapException(f: => Any): Any = + | try f catch { + | case e: InvocationTargetException => throw e.getCause + | } + |} + |""".stripMargin + + override def run(using Context): Unit = + val inserter = Inserter(parseExpression, parseExpressionClass) + ctx.compilationUnit.untpdTree = inserter.transform(ctx.compilationUnit.untpdTree) + + private class Inserter(expression: Tree, expressionClass: Seq[Tree]) extends UntypedTreeMap: + override def transform(tree: Tree)(using Context): Tree = + tree match + case tree: PackageDef => + val transformed = super.transform(tree).asInstanceOf[PackageDef] + if expressionInserted then + // set to `false` to prevent inserting `Expression` class in other `PackageDef`s + expressionInserted = false + cpy.PackageDef(transformed)( + transformed.pid, + transformed.stats ++ expressionClass.map(_.withSpan(tree.span)) + ) + else transformed + case tree @ DefDef(name, paramss, tpt, rhs) if rhs != EmptyTree && isOnBreakpoint(tree) => + cpy.DefDef(tree)(name, paramss, tpt, mkExprBlock(expression, tree.rhs)) + case tree @ Match(selector, caseDefs) if isOnBreakpoint(tree) || caseDefs.exists(isOnBreakpoint) => + // the expression is on the match or a case of the match + // if it is on the case of the match the program could pause on the pattern, the guard or the body + // we assume it pauses on the pattern because that is the first instruction + // in that case we cannot compile the expression val in the pattern, but we can compile it in the selector + cpy.Match(tree)(mkExprBlock(expression, selector), caseDefs) + case tree @ ValDef(name, tpt, _) if isOnBreakpoint(tree) => + cpy.ValDef(tree)(name, tpt, mkExprBlock(expression, tree.rhs)) + case tree @ PatDef(mods, pat, tpt, rhs) if isOnBreakpoint(tree) => + PatDef(mods, pat, tpt, mkExprBlock(expression, rhs)) + case tree: (Ident | Select | GenericApply | Literal | This | New | InterpolatedString | OpTree | Tuple | + Assign | Block) if isOnBreakpoint(tree) => + mkExprBlock(expression, tree) + + // for loop: we insert the expression on the first enumeration + case tree @ ForYield(enums, rhs) if isOnBreakpoint(tree) => + ForYield(transform(enums.head) :: enums.tail, rhs) + case tree @ ForDo(enums, rhs) if isOnBreakpoint(tree) => + ForDo(transform(enums.head) :: enums.tail, rhs) + + // generator of for loop: we insert the expression on the rhs + case tree @ GenFrom(pat, rhs, checkMode) if isOnBreakpoint(tree) => + GenFrom(pat, mkExprBlock(expression, rhs), checkMode) + case tree @ GenAlias(pat, rhs) if isOnBreakpoint(tree) => + GenAlias(pat, mkExprBlock(expression, rhs)) + + case tree => super.transform(tree) + + private def parseExpression(using Context): Tree = + val prefix = + s"""|object Expression: + | { + | """.stripMargin + // don't use stripMargin on wrappedExpression because expression can contain a line starting with ` |` + val wrappedExpression = prefix + config.expression + "\n }\n" + val expressionFile = SourceFile.virtual("", config.expression) + val contentBytes = wrappedExpression.getBytes(StandardCharsets.UTF_8) + val wrappedExpressionFile = + new VirtualFile("", contentBytes) + val sourceFile = + new SourceFile(wrappedExpressionFile, wrappedExpression.toArray): + override def start: Int = -prefix.size + override def underlying: SourceFile = expressionFile + override def atSpan(span: Span): SourcePosition = + if (span.exists) SourcePosition(this, span) + else NoSourcePosition + + parse(sourceFile) + .asInstanceOf[PackageDef] + .stats + .head + .asInstanceOf[ModuleDef] + .impl + .body + .head + + private def parseExpressionClass(using Context): Seq[Tree] = + val sourceFile = SourceFile.virtual("", expressionClassSource) + parse(sourceFile).asInstanceOf[PackageDef].stats + + private def parse(sourceFile: SourceFile)(using Context): Tree = + val newCtx = ctx.fresh.setSource(sourceFile) + val parser = Parsers.Parser(sourceFile)(using newCtx) + parser.parse() + + private def isOnBreakpoint(tree: Tree)(using Context): Boolean = + val startLine = + if tree.span.exists then tree.sourcePos.startLine + 1 else -1 + startLine == config.breakpointLine + + private def mkExprBlock(expr: Tree, tree: Tree)(using Context): Tree = + if expressionInserted then + warnOrError("expression already inserted", tree.srcPos) + tree + else + expressionInserted = true + val valDef = ValDef(config.expressionTermName, TypeTree(), expr) + // we insert a fake effectful tree to avoid the constant-folding of the block during the firstTransform phase + val effect = Apply( + Select(Select(Ident(termName("scala")), termName("Predef")), termName("print")), + List(Literal(Constant(""))) + ) + Block(List(valDef, effect), tree) + + // only fails in test mode + private def warnOrError(msg: String, srcPos: SrcPos)(using Context): Unit = + if config.testMode then report.error(msg, srcPos) + else report.warning(msg, srcPos) + +private object InsertExpression: + val name: String = "insertExpression" diff --git a/compiler/src/dotty/tools/debug/JavaEncoding.scala b/compiler/src/dotty/tools/debug/JavaEncoding.scala new file mode 100644 index 000000000000..d461c7b0d23c --- /dev/null +++ b/compiler/src/dotty/tools/debug/JavaEncoding.scala @@ -0,0 +1,76 @@ +package dotty.tools.debug + +import dotty.tools.dotc.core.Types.* +import dotty.tools.dotc.core.Contexts.* +import dotty.tools.dotc.core.Symbols.* +import dotty.tools.dotc.core.Flags.* +import dotty.tools.dotc.core.Names.* +import dotty.tools.backend.jvm.DottyBackendInterface.symExtensions +import dotty.tools.dotc.util.NameTransformer + +// Inspired by https://github.com/lampepfl/dotty/blob/main/compiler/src/dotty/tools/backend/sjs/JSEncoding.scala +private object JavaEncoding: + def encode(tpe: Type)(using Context): String = + tpe.widenDealias match + // Array type such as Array[Int] (kept by erasure) + case JavaArrayType(el) => s"[${binaryName(el)}" + case tpe: TypeRef => encode(tpe.symbol.asType) + case AnnotatedType(t, _) => encode(t) + + def encode(sym: TypeSymbol)(using Context): String = + /* When compiling Array.scala, the type parameter T is not erased and shows up in method + * signatures, e.g. `def apply(i: Int): T`. A TypeRef to T is replaced by ObjectReference. + */ + if !sym.isClass then "java.lang.Object" + else if sym.isPrimitiveValueClass then primitiveName(sym) + else className(sym) + + def encode(name: TermName)(using Context): String = + NameTransformer.encode(name.toSimpleName).toString + + private def binaryName(tpe: Type)(using Context): String = + tpe match + case JavaArrayType(el) => s"[${binaryName(el)}" + case tpe: TypeRef => + if tpe.symbol.isPrimitiveValueClass then primitiveBinaryName(tpe.symbol) + else classBinaryName(tpe.symbol) + case AnnotatedType(t, _) => binaryName(t) + + private def primitiveName(sym: Symbol)(using Context): String = + if sym == defn.UnitClass then "void" + else if sym == defn.BooleanClass then "boolean" + else if sym == defn.CharClass then "char" + else if sym == defn.ByteClass then "byte" + else if sym == defn.ShortClass then "short" + else if sym == defn.IntClass then "int" + else if sym == defn.LongClass then "long" + else if sym == defn.FloatClass then "float" + else if sym == defn.DoubleClass then "double" + else throw new Exception(s"Unknown primitive value class $sym") + + private def primitiveBinaryName(sym: Symbol)(using Context): String = + if sym == defn.BooleanClass then "Z" + else if sym == defn.CharClass then "C" + else if sym == defn.ByteClass then "B" + else if sym == defn.ShortClass then "S" + else if sym == defn.IntClass then "I" + else if sym == defn.LongClass then "J" + else if sym == defn.FloatClass then "F" + else if sym == defn.DoubleClass then "D" + else throw new Exception(s"Unknown primitive value class $sym") + + private def className(sym: Symbol)(using Context): String = + val sym1 = + if (sym.isAllOf(ModuleClass | JavaDefined)) sym.linkedClass + else sym + + /* Some re-wirings: + * - scala.Nothing to scala.runtime.Nothing$. + * - scala.Null to scala.runtime.Null$. + */ + if sym1 == defn.NothingClass then "scala.runtime.Nothing$" + else if sym1 == defn.NullClass then "scala.runtime.Null$" + else sym1.javaClassName + + private def classBinaryName(sym: Symbol)(using Context): String = + s"L${className(sym)};" diff --git a/compiler/src/dotty/tools/debug/ReflectEvalStrategy.scala b/compiler/src/dotty/tools/debug/ReflectEvalStrategy.scala new file mode 100644 index 000000000000..36ef2327965e --- /dev/null +++ b/compiler/src/dotty/tools/debug/ReflectEvalStrategy.scala @@ -0,0 +1,29 @@ +package dotty.tools.debug + +import dotty.tools.dotc.core.Symbols.* +import dotty.tools.dotc.util.Property.* + +/** + * The [[ExtractExpression]] phase attaches an [[ReflectEvalStrategy]] to each `reflectEval` node + * capturing information about the term that requires evaluation via reflection (because it is + * inaccessible from the evaluation class). + * Subsequently, the [[ResolveReflectEval]] phase converts each evaluation strategy into a method + * call within the expression class. + */ +private enum ReflectEvalStrategy: + case This(cls: ClassSymbol) + case Outer(outerCls: ClassSymbol) + case LocalOuter(outerCls: ClassSymbol) // the $outer param in a constructor + case LocalValue(variable: TermSymbol, isByName: Boolean) + case LocalValueAssign(variable: TermSymbol) + case MethodCapture(variable: TermSymbol, method: TermSymbol, isByName: Boolean) + case MethodCaptureAssign(variable: TermSymbol, method: TermSymbol) + case ClassCapture(variable: TermSymbol, cls: ClassSymbol, isByName: Boolean) + case ClassCaptureAssign(variable: TermSymbol, cls: ClassSymbol) + case StaticObject(obj: ClassSymbol) + case Field(field: TermSymbol, isByName: Boolean) + case FieldAssign(field: TermSymbol) + case MethodCall(method: TermSymbol) + case ConstructorCall(ctr: TermSymbol, cls: ClassSymbol) + +object ReflectEvalStrategy extends StickyKey[ReflectEvalStrategy] diff --git a/compiler/src/dotty/tools/debug/ResolveReflectEval.scala b/compiler/src/dotty/tools/debug/ResolveReflectEval.scala new file mode 100644 index 000000000000..f79aa462fcb4 --- /dev/null +++ b/compiler/src/dotty/tools/debug/ResolveReflectEval.scala @@ -0,0 +1,374 @@ +package dotty.tools.debug + +import dotty.tools.dotc.core.SymUtils +import dotty.tools.dotc.ast.tpd.* +import dotty.tools.dotc.core.Constants.Constant +import dotty.tools.dotc.core.Contexts.* +import dotty.tools.dotc.core.Flags.* +import dotty.tools.dotc.core.NameKinds.QualifiedInfo +import dotty.tools.dotc.core.Names.* +import dotty.tools.dotc.core.Phases +import dotty.tools.dotc.core.StdNames.* +import dotty.tools.dotc.core.Symbols.* +import dotty.tools.dotc.core.TypeErasure.ErasedValueType +import dotty.tools.dotc.core.Types.* +import dotty.tools.dotc.report +import dotty.tools.dotc.transform.MegaPhase.MiniPhase +import dotty.tools.dotc.transform.ValueClasses + +/** + * This phase transforms every reflectEval call to an actual method call that performs reflection. + * Specifically it does: + * - encode symbols to Java + * - box and unbox value classes where necessary + * - box and unbox captured variables where necessary + * - evaluate by-name params where necessary + * - resolve captured variables and check they are available (they may not be captured at runtime) + * + * Before: + * this.reflectEval(a, "ReflectEvalStrategy.MethodCall(m)", args) + * + * After: + * this.callMethod(a, "example.A", "m", ["ArgType1", "ArgType2"], "ResType", args) + * + */ +private class ResolveReflectEval(config: ExpressionCompilerConfig, expressionStore: ExpressionStore) extends MiniPhase: + private val reflectEvalName = termName("reflectEval") + private val elemName = termName("elem") + override def phaseName: String = ResolveReflectEval.name + + override def transformTypeDef(tree: TypeDef)(using Context): Tree = + ExpressionTransformer.transform(tree) + + object ExpressionTransformer extends TreeMap: + override def transform(tree: Tree)(using Context): Tree = + tree match + case tree: DefDef if tree.symbol == config.evaluateMethod => + // unbox the result of the `evaluate` method if it is a value class + val gen = new Gen( + Apply( + Select(This(config.expressionClass), reflectEvalName), + List(nullLiteral, nullLiteral, nullLiteral) + ) + ) + val rhs = gen.unboxIfValueClass(expressionStore.symbol.nn, transform(tree.rhs)) + cpy.DefDef(tree)(rhs = rhs) + + case reflectEval: Apply if isReflectEval(reflectEval.fun.symbol) => + val qualifier :: _ :: argsTree :: Nil = reflectEval.args.map(transform): @unchecked + val args = argsTree.asInstanceOf[JavaSeqLiteral].elems + val gen = new Gen(reflectEval) + tree.attachment(ReflectEvalStrategy) match + case ReflectEvalStrategy.This(cls) => gen.getThisObject + case ReflectEvalStrategy.LocalOuter(cls) => gen.getLocalValue("$outer") + case ReflectEvalStrategy.Outer(outerCls) => gen.getOuter(qualifier, outerCls) + + case ReflectEvalStrategy.LocalValue(variable, isByName) => + val variableName = JavaEncoding.encode(variable.name) + val rawLocalValue = gen.getLocalValue(variableName) + val localValue = if isByName then gen.evaluateByName(rawLocalValue) else rawLocalValue + val derefLocalValue = gen.derefCapturedVar(localValue, variable) + gen.boxIfValueClass(variable, derefLocalValue) + + case ReflectEvalStrategy.LocalValueAssign(variable) => + val value = gen.unboxIfValueClass(variable, args.head) + val typeSymbol = variable.info.typeSymbol.asType + val variableName = JavaEncoding.encode(variable.name) + JavaEncoding.encode(typeSymbol) match + case s"scala.runtime.${_}Ref" => + val elemField = typeSymbol.info.decl(elemName).symbol + gen.setField(tree)( + gen.getLocalValue(variableName), + elemField.asTerm, + value + ) + case _ => gen.setLocalValue(variableName, value) + + case ReflectEvalStrategy.ClassCapture(variable, cls, isByName) => + val rawCapture = gen + .getClassCapture(tree)(qualifier, variable.name, cls) + .getOrElse { + report.error(s"No capture found for $variable in $cls", tree.srcPos) + ref(defn.Predef_undefined) + } + val capture = if isByName then gen.evaluateByName(rawCapture) else rawCapture + val capturedValue = gen.derefCapturedVar(capture, variable) + gen.boxIfValueClass(variable, capturedValue) + + case ReflectEvalStrategy.ClassCaptureAssign(variable, cls) => + val capture = gen + .getClassCapture(tree)(qualifier, variable.name, cls) + .getOrElse { + report.error(s"No capture found for $variable in $cls", tree.srcPos) + ref(defn.Predef_undefined) + } + val value = gen.unboxIfValueClass(variable, args.head) + val typeSymbol = variable.info.typeSymbol + val elemField = typeSymbol.info.decl(elemName).symbol + gen.setField(tree)(capture, elemField.asTerm, value) + + case ReflectEvalStrategy.MethodCapture(variable, method, isByName) => + val rawCapture = gen + .getMethodCapture(method, variable.name) + .getOrElse { + report.error(s"No capture found for $variable in $method", tree.srcPos) + ref(defn.Predef_undefined) + } + val capture = if isByName then gen.evaluateByName(rawCapture) else rawCapture + val capturedValue = gen.derefCapturedVar(capture, variable) + gen.boxIfValueClass(variable, capturedValue) + + case ReflectEvalStrategy.MethodCaptureAssign(variable, method) => + val capture = gen + .getMethodCapture(method, variable.name) + .getOrElse { + report.error(s"No capture found for $variable in $method", tree.srcPos) + ref(defn.Predef_undefined) + } + val value = gen.unboxIfValueClass(variable, args.head) + val typeSymbol = variable.info.typeSymbol + val elemField = typeSymbol.info.decl(elemName).symbol + gen.setField(tree)(capture, elemField.asTerm, value) + + case ReflectEvalStrategy.StaticObject(obj) => gen.getStaticObject(obj) + + case ReflectEvalStrategy.Field(field, isByName) => + // if the field is lazy, if it is private in a value class or a trait + // then we must call the getter method + val fieldValue = + if field.is(Lazy) || field.owner.isValueClass || field.owner.is(Trait) + then gen.callMethod(tree)(qualifier, field.getter.asTerm, Nil) + else + val rawValue = gen.getField(tree)(qualifier, field) + if isByName then gen.evaluateByName(rawValue) else rawValue + gen.boxIfValueClass(field, fieldValue) + + case ReflectEvalStrategy.FieldAssign(field) => + val arg = gen.unboxIfValueClass(field, args.head) + if field.owner.is(Trait) then + gen.callMethod(tree)(qualifier, field.setter.asTerm, List(arg)) + else gen.setField(tree)(qualifier, field, arg) + + case ReflectEvalStrategy.MethodCall(method) => gen.callMethod(tree)(qualifier, method, args) + case ReflectEvalStrategy.ConstructorCall(ctor, cls) => gen.callConstructor(tree)(qualifier, ctor, args) + case _ => super.transform(tree) + + private def isReflectEval(symbol: Symbol)(using Context): Boolean = + symbol.name == reflectEvalName && symbol.owner == config.expressionClass + + class Gen(reflectEval: Apply)(using Context): + private val expressionThis = reflectEval.fun.asInstanceOf[Select].qualifier + + def derefCapturedVar(tree: Tree, term: TermSymbol): Tree = + val typeSymbol = term.info.typeSymbol.asType + JavaEncoding.encode(typeSymbol) match + case s"scala.runtime.${_}Ref" => + val elemField = typeSymbol.info.decl(elemName).symbol + getField(tree)(tree, elemField.asTerm) + case _ => tree + + def boxIfValueClass(term: TermSymbol, tree: Tree): Tree = + getErasedValueType(atPhase(Phases.elimErasedValueTypePhase)(term.info)) match + case Some(erasedValueType) => + boxValueClass(erasedValueType.tycon.typeSymbol.asClass, tree) + case None => tree + + def boxValueClass(valueClass: ClassSymbol, tree: Tree): Tree = + // qualifier is null: a value class cannot be nested into a class + val ctor = valueClass.primaryConstructor.asTerm + callConstructor(tree)(nullLiteral, ctor, List(tree)) + + def unboxIfValueClass(term: TermSymbol, tree: Tree): Tree = + getErasedValueType(atPhase(Phases.elimErasedValueTypePhase)(term.info)) match + case Some(erasedValueType) => unboxValueClass(tree, erasedValueType) + case None => tree + + private def getErasedValueType(tpe: Type): Option[ErasedValueType] = tpe match + case tpe: ErasedValueType => Some(tpe) + case tpe: MethodOrPoly => getErasedValueType(tpe.resultType) + case tpe => None + + private def unboxValueClass(tree: Tree, tpe: ErasedValueType): Tree = + val cls = tpe.tycon.typeSymbol.asClass + val unboxMethod = ValueClasses.valueClassUnbox(cls).asTerm + callMethod(tree)(tree, unboxMethod, Nil) + + def getThisObject: Tree = + Apply(Select(expressionThis, termName("getThisObject")), Nil) + + def getLocalValue(name: String): Tree = + Apply( + Select(expressionThis, termName("getLocalValue")), + List(Literal(Constant(name))) + ) + + def setLocalValue(name: String, value: Tree): Tree = + Apply( + Select(expressionThis, termName("setLocalValue")), + List(Literal(Constant(name)), value) + ) + + def getOuter(qualifier: Tree, outerCls: ClassSymbol): Tree = + Apply( + Select(expressionThis, termName("getOuter")), + List(qualifier, Literal(Constant(JavaEncoding.encode(outerCls)))) + ) + + def getClassCapture(tree: Tree)(qualifier: Tree, originalName: Name, cls: ClassSymbol): Option[Tree] = + cls.info.decls.iterator + .filter(term => term.isField) + .find { field => + field.name match + case DerivedName(underlying, _) if field.isPrivate => + underlying == originalName + case DerivedName(DerivedName(_, info: QualifiedInfo), _) => + info.name == originalName + case _ => false + } + .map(field => getField(tree: Tree)(qualifier, field.asTerm)) + + def getMethodCapture(method: TermSymbol, originalName: TermName): Option[Tree] = + val methodType = method.info.asInstanceOf[MethodType] + methodType.paramNames + .collectFirst { case name @ DerivedName(n, _) if n == originalName => name } + .map(param => getLocalValue(JavaEncoding.encode(param))) + + def getStaticObject(obj: ClassSymbol): Tree = + Apply( + Select(expressionThis, termName("getStaticObject")), + List(Literal(Constant(JavaEncoding.encode(obj)))) + ) + + def getField(tree: Tree)(qualifier: Tree, field: TermSymbol): Tree = + if field.owner.isTerm then + report.error(s"Cannot access local val ${field.name} in ${field.owner} as field", tree.srcPos) + ref(defn.Predef_undefined) + else + Apply( + Select(expressionThis, termName("getField")), + List( + qualifier, + Literal(Constant(JavaEncoding.encode(field.owner.asType))), + Literal(Constant(JavaEncoding.encode(field.name))) + ) + ) + + def setField(tree: Tree)(qualifier: Tree, field: TermSymbol, value: Tree): Tree = + if field.owner.isTerm then + report.error(s"Cannot access local var ${field.name} in ${field.owner} as field", tree.srcPos) + ref(defn.Predef_undefined) + else + Apply( + Select(expressionThis, termName("setField")), + List( + qualifier, + Literal(Constant(JavaEncoding.encode(field.owner.asType))), + Literal(Constant(JavaEncoding.encode(field.name))), + value + ) + ) + + def evaluateByName(function: Tree): Tree = + val castFunction = function.cast(defn.Function0.typeRef.appliedTo(defn.AnyType)) + Apply(Select(castFunction, termName("apply")), List()) + + def callMethod(tree: Tree)(qualifier: Tree, method: TermSymbol, args: List[Tree]): Tree = + val methodType = method.info.asInstanceOf[MethodType] + val paramTypesNames = methodType.paramInfos.map(JavaEncoding.encode) + val paramTypesArray = JavaSeqLiteral( + paramTypesNames.map(t => Literal(Constant(t))), + TypeTree(ctx.definitions.StringType) + ) + + def unknownCapture(name: Name): Tree = + report.error(s"Unknown captured variable $name in $method", reflectEval.srcPos) + ref(defn.Predef_undefined) + val capturedArgs = methodType.paramNames.dropRight(args.size).map { + case name @ DerivedName(underlying, _) => capturedValue(tree)(method, underlying).getOrElse(unknownCapture(name)) + case name => unknownCapture(name) + } + + val erasedMethodInfo = atPhase(Phases.elimErasedValueTypePhase)(method.info).asInstanceOf[MethodType] + val unboxedArgs = erasedMethodInfo.paramInfos.takeRight(args.size).zip(args).map { + case (tpe: ErasedValueType, arg) => unboxValueClass(arg, tpe) + case (_, arg) => arg + } + + val returnTypeName = JavaEncoding.encode(methodType.resType) + val methodName = JavaEncoding.encode(method.name) + val result = Apply( + Select(expressionThis, termName("callMethod")), + List( + qualifier, + Literal(Constant(JavaEncoding.encode(method.owner.asType))), + Literal(Constant(methodName)), + paramTypesArray, + Literal(Constant(returnTypeName)), + JavaSeqLiteral(capturedArgs ++ unboxedArgs, TypeTree(ctx.definitions.ObjectType)) + ) + ) + erasedMethodInfo.resType match + case tpe: ErasedValueType => boxValueClass(tpe.tycon.typeSymbol.asClass, result) + case _ => result + end callMethod + + def callConstructor(tree: Tree)(qualifier: Tree, ctor: TermSymbol, args: List[Tree]): Tree = + val methodType = ctor.info.asInstanceOf[MethodType] + val paramTypesNames = methodType.paramInfos.map(JavaEncoding.encode) + val clsName = JavaEncoding.encode(methodType.resType) + + val capturedArgs = + methodType.paramNames.dropRight(args.size).map { + case outer if outer == nme.OUTER => qualifier + case name @ DerivedName(underlying, _) => + // if derived then probably a capture + capturedValue(tree: Tree)(ctor.owner, underlying) + .getOrElse { + report.error(s"Unknown captured variable $name in $ctor of ${ctor.owner}", reflectEval.srcPos) + ref(defn.Predef_undefined) + } + case name => + val paramName = JavaEncoding.encode(name) + getLocalValue(paramName) + } + + val erasedCtrInfo = atPhase(Phases.elimErasedValueTypePhase)(ctor.info) + .asInstanceOf[MethodType] + val unboxedArgs = erasedCtrInfo.paramInfos.takeRight(args.size).zip(args).map { + case (tpe: ErasedValueType, arg) => unboxValueClass(arg, tpe) + case (_, arg) => arg + } + + val paramTypesArray = JavaSeqLiteral( + paramTypesNames.map(t => Literal(Constant(t))), + TypeTree(ctx.definitions.StringType) + ) + Apply( + Select(expressionThis, termName("callConstructor")), + List( + Literal(Constant(clsName)), + paramTypesArray, + JavaSeqLiteral(capturedArgs ++ unboxedArgs, TypeTree(ctx.definitions.ObjectType)) + ) + ) + end callConstructor + + private def capturedValue(tree: Tree)(sym: Symbol, originalName: TermName): Option[Tree] = + val encodedName = JavaEncoding.encode(originalName) + if expressionStore.classOwners.contains(sym) then capturedByClass(tree: Tree)(sym.asClass, originalName) + else if config.localVariables.contains(encodedName) then Some(getLocalValue(encodedName)) + else + // if the captured value is not a local variables + // then it must have been captured by the outer method + expressionStore.capturingMethod.flatMap(getMethodCapture(_, originalName)) + + private def capturedByClass(tree: Tree)(cls: ClassSymbol, originalName: TermName): Option[Tree] = + val target = expressionStore.classOwners.indexOf(cls) + val qualifier = expressionStore.classOwners + .drop(1) + .take(target) + .foldLeft(getThisObject)((q, cls) => getOuter(q, cls)) + getClassCapture(tree: Tree)(qualifier, originalName, cls) + +private object ResolveReflectEval: + val name = "resolvReflectEval" diff --git a/compiler/test/debug/Gen b/compiler/test/debug/Gen deleted file mode 100755 index 7212b9cbfb62..000000000000 --- a/compiler/test/debug/Gen +++ /dev/null @@ -1,13 +0,0 @@ -#! /usr/bin/env bash - -DIR="$( cd "$( dirname "$0" )" && pwd )" - -SOURCE=$DIR/Gen.scala -CLASS=./Gen.class - -if [ ! -e $CLASS ] || [ $SOURCE -nt $CLASS ]; then - ./bin/scalac $DIR/Gen.scala -fi - -./bin/scala Gen $@ - diff --git a/compiler/test/debug/Gen.scala b/compiler/test/debug/Gen.scala deleted file mode 100755 index 646dec189f10..000000000000 --- a/compiler/test/debug/Gen.scala +++ /dev/null @@ -1,174 +0,0 @@ -import scala.language.unsafeNulls - -import scala.io.Source -import scala.collection.mutable.ListBuffer -import scala.util.Using - -/** Automate testing debuggability of generated code using JDB and expect - * - * The debugging information is annotated as comments to the code in brackets: - * - * val x = f(3) // [break] [next: line=5] - * val y = 5 - * - * 1. A jdb command must be wrapped in brackets, like `[step]`. All jdb commands can be used. - * 2. To check output of jdb for a command, use `[cmd: expect]`. - * 3. If `expect` is wrapped in double quotes, regex is supported. - * 4. Break commands are collected and set globally. - * 5. Other commands will be send to jdb in the order they appear in the source file - * - * Note: jdb uses line number starts from 1 - */ - -object Gen { - val MainObject = "Test" - val CommandWait = 1 - - sealed trait Tree - - case class Break(line: Int) extends Tree - - case class Command(val name: String, val expect: Expect = EmptyExpect) extends Tree - - sealed trait Expect - - case object EmptyExpect extends Expect - - case class LitExpect(lit: String) extends Expect - - case class PatExpect(pat: String) extends Expect - - case class Program(breaks: Seq[Break], commands: Seq[Command]) - - def error(msg: String): Nothing = { - throw new Exception(msg) - } - - def parseCommand(command: String, lineNo: Int): Tree = { - val index = command.indexOf(':') - if (index == -1) { - // simple command - if (command == "break") Break(lineNo) - else Command(command) - } else { - val Seq(cmd, rhs) = command.split(":", 2).toSeq.map(_.trim) - if (rhs.startsWith("\"")) { - // regex match - val content = "\"(.+)\"".r - rhs match { - case content(expect) => Command(cmd, PatExpect(expect)) - case _ => error(s"""incorrect specification: `$rhs` for `$cmd` at line $lineNo. Ending " expected.""") - } - } else { - // literal match - Command(cmd, LitExpect(rhs)) - } - } - } - - def parse(file: String): Program = { - val lines = Using(Source.fromFile(file))(_.getLines().toBuffer).get - - val breaks = new ListBuffer[Break]() - val cmds = new ListBuffer[Command]() - lines.zipWithIndex.map { case (code, line) => - val comment = if (code.indexOf("//") != -1) code.split("//").last else "" - val regex = """(?<=\[).*?(?=\])""".r - for (p <- regex findAllIn comment) parseCommand(p.trim, line + 1) match { // jdb index from 0 - case b: Break => breaks += b - case c: Command => cmds += c - } - } - - Program(breaks.toList, cmds.toList) - } - - def generate(program: Program, source: String = "tests/debug/"): String = { - val Program(breaks, cmds) = program - val breakpoints = (breaks.map { - case Break(point) => - s"""|send "stop at $MainObject$$:$point\\r" - |sleep $CommandWait - |expect "breakpoint $MainObject$$:$point" - |expect -re $$ - """.stripMargin - }).mkString("\n\n") - - val commands = (cmds.map { - case Command(cmd, EmptyExpect) => - s"""|# send_user "send command `$cmd`\\n" - |send "$cmd\\r" - |sleep $CommandWait - |expect -re $$ - """.stripMargin - case Command(cmd, LitExpect(lit)) => - s"""|# send_user "send command `$cmd`\\n" - |send "$cmd\\r" - |sleep $CommandWait - |expect { - | "*$lit*" { send_user "success - $cmd : $lit \\n" } - | timeout { - | send_user "timeout while waiting for response: $cmd : $lit\\n" - | exit 1 - | } - |} - |expect -re $$ - |""".stripMargin - case Command(cmd, PatExpect(pat)) => - s"""|# send_user "send command `$cmd`\\n" - |send "$cmd\\r" - |sleep $CommandWait - |expect { - | -re {$pat} { send_user "success - $cmd : $pat \\n" } - | timeout { - | send_user "timeout while waiting for response: $cmd : $pat\\n" - | exit 1 - | } - |} - |expect -re $$ - |""".stripMargin - }).mkString("\n\n") - - s"""|#!/usr/bin/expect - | - |# log_user 1 - |# exp_internal 1 - |# set timeout 5 - | - |send_user "spawning job...\\n" - | - |spawn jdb -attach 5005 -sourcepath $source - | - |send_user "interacting...\\n" - | - |expect { - | "*VM Started*" { send_user "success - connected to server \\n" } - | timeout { - | send_user "timeout while waiting for: *VM Started*\\n" - | exit 1 - | } - |} - | - |send_user "setting breakpoints...\\n" - | - |# breakpoints - |$breakpoints - | - |# run - |send_user "run program...\\n" - |send "run\\r" - |expect "Breakpoint hit" - | - |# interactions - |$commands""".stripMargin - } - - def main(args: Array[String]): Unit = { - val prog = Gen.parse(args(0)) - // println("--------------------------------") - // println("prog:" + prog) - // println("\n\n\n scrip:") - // println("--------------------------------") - println(Gen.generate(prog)) - } -} diff --git a/compiler/test/debug/test b/compiler/test/debug/test deleted file mode 100755 index 6081862448e3..000000000000 --- a/compiler/test/debug/test +++ /dev/null @@ -1,21 +0,0 @@ -#!/usr/bin/env bash - -DIR="$( cd "$( dirname "$0" )" && pwd )" - -echo "start debug test..." -for file in tests/debug/*.scala; do - ./bin/scalac $file || exit 1 - ./bin/scala -d Test > /dev/null & - $DIR/Gen $file > robot - expect robot 2>&1 > /dev/null - - if [[ $? != 0 ]]; then - echo "debug test failed for file $file" - exit 1 - fi - - echo "$file -- success" -done - -echo "debug test success!" - diff --git a/compiler/test/dotty/tools/debug/DebugStepAssert.scala b/compiler/test/dotty/tools/debug/DebugStepAssert.scala new file mode 100644 index 000000000000..f3c3d8af405f --- /dev/null +++ b/compiler/test/dotty/tools/debug/DebugStepAssert.scala @@ -0,0 +1,139 @@ +package dotty.tools.debug + +import com.sun.jdi.Location +import dotty.tools.io.JPath +import dotty.tools.readLines + +import scala.annotation.tailrec + +/** + * A debug step and an associated assertion to validate the step. + * A sequence of DebugStepAssert is parsed from the check file in tests/debug + */ +private[debug] case class DebugStepAssert[T](step: DebugStep[T], assertion: T => Unit)( + using val location: CheckFileLocation +) + +/** A location in the check file */ +private[debug] case class CheckFileLocation(checkFile: JPath, line: Int): + override def toString: String = s"$checkFile:$line" + +/** When a DebugStepAssert fails it throws a DebugStepException */ +private[debug] case class DebugStepException(message: String, location: CheckFileLocation) extends Exception + +private[debug] enum DebugStep[T]: + case Break(className: String, line: Int) extends DebugStep[Location] + case Step extends DebugStep[Location] + case Next extends DebugStep[Location] + case Eval(expression: String) extends DebugStep[Either[String, String]] + +private[debug] object DebugStepAssert: + private val sym = "[a-zA-Z0-9$.]+" + private val line = raw"\d+" + private val trailing = raw" *(?://.*)?".r // empty or comment + private val break = s"break ($sym) ($line)$trailing".r + private val step = s"step ($sym|$line)$trailing".r + private val next = s"next ($sym|$line)$trailing".r + private val multiLineEval = s"eval$trailing".r + private val eval = s"eval (.*)".r + private val result = "result (.*)".r + private val error = "error (.*)".r + private val multiLineError = s"error$trailing".r + + import DebugStep.* + def parseCheckFile(checkFile: JPath): Seq[DebugStepAssert[?]] = + val allLines = readLines(checkFile.toFile) + + @tailrec + def loop(lines: List[String], acc: List[DebugStepAssert[?]]): List[DebugStepAssert[?]] = + given location: CheckFileLocation = CheckFileLocation(checkFile, allLines.size - lines.size + 1) + lines match + case Nil => acc.reverse + case break(className , lineStr) :: tail => + val breakpointLine = lineStr.toInt + val step = DebugStepAssert(Break(className, breakpointLine), checkClassAndLine(className, breakpointLine)) + loop(tail, step :: acc) + case step(pattern) :: tail => + val step = DebugStepAssert(Step, checkLineOrMethod(pattern)) + loop(tail, step :: acc) + case next(pattern) :: tail => + val step = DebugStepAssert(Next, checkLineOrMethod(pattern)) + loop(tail, step :: acc) + case eval(expr) :: tail0 => + val (assertion, tail1) = parseEvalAssertion(tail0) + val step = DebugStepAssert(Eval(expr), assertion) + loop(tail1, step :: acc) + case multiLineEval() :: tail0 => + val (exprLines, tail1) = tail0.span(_.startsWith(" ")) + val expr = exprLines.map(s => s.stripPrefix(" ")).mkString("\n") + val (assertion, tail2) = parseEvalAssertion(tail1) + val step = DebugStepAssert(Eval(expr), assertion) + loop(tail2, step :: acc) + case trailing() :: tail => loop(tail, acc) + case invalid :: tail => + throw new Exception(s"Cannot parse debug step: $invalid ($location)") + + def parseEvalAssertion(lines: List[String]): (Either[String, String] => Unit, List[String]) = + given location: CheckFileLocation = CheckFileLocation(checkFile, allLines.size - lines.size + 1) + lines match + case Nil => throw new Exception(s"Missing result or error") + case trailing() :: tail => parseEvalAssertion(tail) + case result(expected) :: tail => (checkResult(expected), tail) + case error(expected) :: tail => (checkError(Seq(expected)), tail) + case multiLineError() :: tail0 => + val (expected, tail1) = tail0.span(_.startsWith(" ")) + (checkError(expected.map(_.stripPrefix(" "))), tail1) + case invalid :: _ => + throw new Exception(s"Cannot parse as result or error: $invalid ($location)") + + loop(allLines, Nil) + end parseCheckFile + + private def checkClassAndLine(className: String, breakpointLine: Int)(using CheckFileLocation)(location: Location): Unit = + debugStepAssertEquals(location.declaringType.name, className) + checkLine(breakpointLine)(location) + + private def checkLineOrMethod(pattern: String)(using CheckFileLocation): Location => Unit = + pattern.toIntOption.map(checkLine).getOrElse(checkMethod(pattern)) + + private def checkLine(expected: Int)(using CheckFileLocation)(location: Location): Unit = + debugStepAssertEquals(location.lineNumber, expected) + + private def checkMethod(expected: String)(using CheckFileLocation)(location: Location): Unit = + debugStepAssertEquals(location.method.name, expected) + + private def checkResult(expected: String)(using CheckFileLocation)(obtained: Either[String, String]): Unit = + obtained match + case Left(obtained) => + debugStepFailed( + s"""|Evaluation failed: + |${obtained.replace("\n", "\n|")}""".stripMargin + ) + case Right(obtained) => debugStepAssertEquals(obtained, expected) + + private def checkError(expected: Seq[String])(using CheckFileLocation)(obtained: Either[String, String]): Unit = + obtained match + case Left(obtained) => + debugStepAssert( + expected.forall(e => e.r.findFirstMatchIn(obtained).isDefined), + s"""|Expected: + |${expected.mkString("\n|")} + |Obtained: + |${obtained.replace("\n", "\n|")}""".stripMargin + ) + case Right(obtained) => + debugStepFailed( + s"""|Evaluation succeeded but failure expected. + |Obtained: $obtained + |""".stripMargin + ) + + private def debugStepAssertEquals[T](obtained: T, expected: T)(using CheckFileLocation): Unit = + debugStepAssert(obtained == expected, s"Obtained $obtained, Expected: $expected") + + private def debugStepAssert(assertion: Boolean, message: String)(using CheckFileLocation): Unit = + if !assertion then debugStepFailed(message) + + private def debugStepFailed(message: String)(using location: CheckFileLocation): Unit = + throw DebugStepException(message, location) +end DebugStepAssert diff --git a/compiler/test/dotty/tools/debug/DebugTests.scala b/compiler/test/dotty/tools/debug/DebugTests.scala new file mode 100644 index 000000000000..95bf5a2e52a6 --- /dev/null +++ b/compiler/test/dotty/tools/debug/DebugTests.scala @@ -0,0 +1,137 @@ +package dotty.tools.debug + +import com.sun.jdi.* +import dotty.Properties +import dotty.tools.dotc.reporting.TestReporter +import dotty.tools.io.JFile +import dotty.tools.vulpix.* +import org.junit.AfterClass +import org.junit.Test + +import java.util.concurrent.TimeoutException +import scala.concurrent.duration.* +import scala.util.control.NonFatal + +class DebugTests: + import DebugTests.* + @Test def debug: Unit = + implicit val testGroup: TestGroup = TestGroup("debug") + CompilationTest.aggregateTests( + compileFile("tests/debug-custom-args/eval-explicit-nulls.scala", TestConfiguration.explicitNullsOptions), + compileFilesInDir("tests/debug", TestConfiguration.defaultOptions) + ).checkDebug() + +object DebugTests extends ParallelTesting: + def maxDuration = + // Increase the timeout when the user is debugging the tests + if isUserDebugging then 3.hours else 45.seconds + def numberOfSlaves = Runtime.getRuntime().availableProcessors() + def safeMode = Properties.testsSafeMode + def isInteractive = SummaryReport.isInteractive + def testFilter = Properties.testsFilter + def updateCheckFiles: Boolean = Properties.testsUpdateCheckfile + def failedTests = TestReporter.lastRunFailedTests + override def debugMode = true + + implicit val summaryReport: SummaryReporting = new SummaryReport + @AfterClass def tearDown(): Unit = + super.cleanup() + summaryReport.echoSummary() + + extension (test: CompilationTest) + private def checkDebug()(implicit summaryReport: SummaryReporting): test.type = + import test.* + checkPass(new DebugTest(targets, times, threadLimit, shouldFail || shouldSuppressOutput), "Debug") + + private final class DebugTest(testSources: List[TestSource], times: Int, threadLimit: Option[Int], suppressAllOutput: Boolean)(implicit summaryReport: SummaryReporting) + extends RunTest(testSources, times, threadLimit, suppressAllOutput): + + override def onSuccess(testSource: TestSource, reporters: Seq[TestReporter], logger: LoggedRunnable) = + verifyDebug(testSource.outDir, testSource, countWarnings(reporters), reporters, logger) + + private def verifyDebug(dir: JFile, testSource: TestSource, warnings: Int, reporters: Seq[TestReporter], logger: LoggedRunnable) = + if Properties.testsNoRun then addNoRunWarning() + else + val checkFile = testSource.checkFile.getOrElse(throw new Exception("Missing check file")).toPath + val debugSteps = DebugStepAssert.parseCheckFile(checkFile) + val expressionEvaluator = + ExpressionEvaluator(testSource.sourceFiles, testSource.flags, testSource.runClassPath, testSource.outDir) + try debugMain(testSource.runClassPath): debuggee => + val jdiPort = debuggee.readJdiPort() + val debugger = Debugger(jdiPort, expressionEvaluator, maxDuration/* , verbose = true */) + // configure the breakpoints before starting the debuggee + val breakpoints = debugSteps.map(_.step).collect { case b: DebugStep.Break => b }.distinct + for b <- breakpoints do debugger.configureBreakpoint(b.className, b.line) + try + debuggee.launch() + playDebugSteps(debugger, debugSteps/* , verbose = true */) + val status = debuggee.exit() + reportDebuggeeStatus(testSource, status) + finally + // closing the debugger must be done at the very end so that the + // 'Listening for transport dt_socket at address: ' message is ready to be read + // by the next DebugTest + debugger.dispose() + catch case DebugStepException(message, location) => + echo(s"\n[error] Debug step failed: $location\n" + message) + failTestSource(testSource) + end verifyDebug + + private def playDebugSteps(debugger: Debugger, steps: Seq[DebugStepAssert[?]], verbose: Boolean = false): Unit = + /** The DebugTests can only debug one thread at a time. It cannot handle breakpoints in concurrent threads. + * When thread is None, it means the JVM is running and no thread is waiting to be resumed. + * If thread is Some, it is waiting to be resumed by calling continue, step or next. + * While the thread is paused, it can be used for evaluation. + */ + var thread: Option[ThreadReference] = None + def location = thread.get.frame(0).location + def continueIfPaused(): Unit = + thread.foreach(debugger.continue) + thread = None + + for case step <- steps do + import DebugStep.* + try step match + case DebugStepAssert(Break(className, line), assertion) => + continueIfPaused() + thread = Some(debugger.break()) + if verbose then + println(s"break $location ${location.method.name}") + assertion(location) + case DebugStepAssert(Next, assertion) => + thread = Some(debugger.next(thread.get)) + if verbose then println(s"next $location ${location.method.name}") + assertion(location) + case DebugStepAssert(Step, assertion) => + thread = Some(debugger.step(thread.get)) + if verbose then println(s"step $location ${location.method.name}") + assertion(location) + case DebugStepAssert(Eval(expr), assertion) => + if verbose then println(s"eval $expr") + val result = debugger.evaluate(expr, thread.get) + if verbose then println(result.fold("error " + _, "result " + _)) + assertion(result) + catch + case _: TimeoutException => throw new DebugStepException("Timeout", step.location) + case e: DebugStepException => throw e + case NonFatal(e) => + throw new Exception(s"Debug step failed unexpectedly: ${step.location}", e) + end for + // let the debuggee finish its execution + continueIfPaused() + end playDebugSteps + + private def reportDebuggeeStatus(testSource: TestSource, status: Status): Unit = + status match + case Success(output) => () + case Failure(output) => + if output == "" then + echo(s"Test '${testSource.title}' failed with no output") + else + echo(s"Test '${testSource.title}' failed with output:") + echo(output) + failTestSource(testSource) + case Timeout => + echo("failed because test " + testSource.title + " timed out") + failTestSource(testSource, TimeoutFailure(testSource.title)) + end DebugTest diff --git a/compiler/test/dotty/tools/debug/Debugger.scala b/compiler/test/dotty/tools/debug/Debugger.scala new file mode 100644 index 000000000000..5826db133915 --- /dev/null +++ b/compiler/test/dotty/tools/debug/Debugger.scala @@ -0,0 +1,139 @@ +package dotty.tools.debug + +import com.sun.jdi.* +import com.sun.jdi.event.* +import com.sun.jdi.request.* + +import java.lang.ref.Reference +import java.util.concurrent.LinkedBlockingQueue +import java.util.concurrent.TimeUnit +import java.util.concurrent.atomic.AtomicReference +import scala.concurrent.duration.Duration +import scala.jdk.CollectionConverters.* +import java.util.concurrent.TimeoutException + +class Debugger(vm: VirtualMachine, evaluator: ExpressionEvaluator, maxDuration: Duration, verbose: Boolean): + // For some JDI events that we receive, we wait for client actions. + // Example: On a BreakpointEvent, the client may want to inspect frames and variables, before it + // decides to step in or continue. + private val pendingEvents = new LinkedBlockingQueue[Event]() + + // When the debuggee is evaluating an expression coming from the debugger + // we should resume the thread after each BreakpointEvent + private var isEvaluating = false + + // Internal event subscriptions, to react to JDI events + // Example: add a Breakpoint on a ClassPrepareEvent + private val eventSubs = new AtomicReference(List.empty[PartialFunction[Event, Unit]]) + private val eventListener = startListeningVM() + + def configureBreakpoint(className: String, line: Int): Unit = + vm.classesByName(className).asScala.foreach(addBreakpoint(_, line)) + // watch class preparation and add breakpoint when the class is prepared + val request = vm.eventRequestManager.createClassPrepareRequest + request.addClassFilter(className) + subscribe: + case e: ClassPrepareEvent if e.request == request => addBreakpoint(e.referenceType, line) + request.enable() + + def break(): ThreadReference = receiveEvent { case e: BreakpointEvent => e.thread } + + def continue(thread: ThreadReference): Unit = thread.resume() + + def next(thread: ThreadReference): ThreadReference = + stepAndWait(thread, StepRequest.STEP_LINE, StepRequest.STEP_OVER) + + def step(thread: ThreadReference): ThreadReference = + stepAndWait(thread, StepRequest.STEP_LINE, StepRequest.STEP_INTO) + + def evaluate(expression: String, thread: ThreadReference): Either[String, String] = + try + isEvaluating = true + evaluator.evaluate(expression, thread) + finally + isEvaluating = false + + + /** stop listening and disconnect debugger */ + def dispose(): Unit = + eventListener.interrupt() + vm.dispose() + + private def addBreakpoint(refType: ReferenceType, line: Int): Unit = + try + for location <- refType.locationsOfLine(line).asScala do + if verbose then println(s"Adding breakpoint in $location") + val breakpoint = vm.eventRequestManager.createBreakpointRequest(location) + // suspend only the thread which triggered the event + breakpoint.setSuspendPolicy(EventRequest.SUSPEND_EVENT_THREAD) + // let's enable the breakpoint and forget about it + // we don't need to store it because we never remove any breakpoint + breakpoint.enable() + catch + case e: AbsentInformationException => + if verbose then println(s"AbsentInformationException on ${refType}") + + private def stepAndWait(thread: ThreadReference, size: Int, depth: Int): ThreadReference = + val request = vm.eventRequestManager.createStepRequest(thread, size, depth) + request.enable() + thread.resume() + // Because our debuggee is mono-threaded, we don't check that `e.request` is our step request. + // Indeed there can be only one step request per thread at a time. + val newThreadRef = receiveEvent { case e: StepEvent => e.thread } + request.disable() + newThreadRef + + private def subscribe(f: PartialFunction[Event, Unit]): Unit = + eventSubs.updateAndGet(f :: _) + + private def startListeningVM(): Thread = + val thread = Thread: () => + var isAlive = true + try while isAlive do + val eventSet = vm.eventQueue.remove() + val subscriptions = eventSubs.get + var shouldResume = true + for event <- eventSet.iterator.asScala.toSeq do + if verbose then println(formatEvent(event)) + for f <- subscriptions if f.isDefinedAt(event) do f(event) + event match + case e: (BreakpointEvent | StepEvent) => + if !isEvaluating then + shouldResume = false + pendingEvents.put(e) + case _: VMDisconnectEvent => isAlive = false + case _ => () + if shouldResume then eventSet.resume() + catch case _: InterruptedException => () + thread.start() + thread + end startListeningVM + + private def receiveEvent[T](f: PartialFunction[Event, T]): T = + // poll repeatedly until we get an event that matches the partial function or a timeout + Iterator.continually(pendingEvents.poll(maxDuration.toMillis, TimeUnit.MILLISECONDS)) + .map(e => if (e == null) throw new TimeoutException() else e) + .collect(f) + .next() + + private def formatEvent(event: Event): String = + event match + case e: ClassPrepareEvent => s"$e ${e.referenceType}" + case e => e.toString + +object Debugger: + // The socket JDI connector + private val connector = Bootstrap.virtualMachineManager + .attachingConnectors + .asScala + .find(_.getClass.getName == "com.sun.tools.jdi.SocketAttachingConnector") + .get + + def apply(jdiPort: Int, expressionEvaluator: ExpressionEvaluator, maxDuration: Duration, verbose: Boolean = false): Debugger = + val arguments = connector.defaultArguments() + arguments.get("hostname").setValue("localhost") + arguments.get("port").setValue(jdiPort.toString) + arguments.get("timeout").setValue(maxDuration.toMillis.toString) + val vm = connector.attach(arguments) + new Debugger(vm, expressionEvaluator, maxDuration, verbose) + diff --git a/compiler/test/dotty/tools/debug/ExpressionEvaluator.scala b/compiler/test/dotty/tools/debug/ExpressionEvaluator.scala new file mode 100644 index 000000000000..6cb8300fc508 --- /dev/null +++ b/compiler/test/dotty/tools/debug/ExpressionEvaluator.scala @@ -0,0 +1,233 @@ +package dotty.tools.debug + +import com.sun.jdi.* +import dotty.tools.io.* +import dotty.tools.vulpix.TestFlags + +import scala.jdk.CollectionConverters.* + +class ExpressionEvaluator( + sources: Map[String, JPath], + options: Array[String], + classPath: String, + outputDir: JPath +): + private val compiler = ExpressionCompilerBridge() + private var uniqueID: Int = 1 + + private class EvaluationException(message: String, cause: InvocationException) + extends Exception(message, cause) + + /** returns the value of the evaluated expression or compiler errors */ + def evaluate(expression: String, thread: ThreadReference): Either[String, String] = + // We evaluate the expression at the top frame of the stack + val frame = thread.frame(0) + + // Extract everything from the frame now because, as soon as we start using the thread + // for remote execution, the frame becomes invalid + val localVariables = frame.visibleVariables.asScala.toSeq + val values = localVariables.map(frame.getValue) + val location = frame.location + val thisRef = frame.thisObject // null in a static context + + for expressionClassName <- compile(expression, location, localVariables) yield + // we don't need to create a new classloader because we compiled the expression class + // in the same outputDir as main classes + val classLoader = location.declaringType.classLoader + val expressionClass = thread.loadClass(classLoader, expressionClassName) + + val nameArray = thread.createArray( + "java.lang.String", + localVariables.map(v => thread.virtualMachine.mirrorOf(v.name)) + ) + val valueArray = thread.createArray("java.lang.Object", values.map(thread.boxIfPrimitive)) + val args = Seq(thisRef, nameArray, valueArray) + + val exprRef = thread.newInstance(expressionClass, args) + try + val output = + thread.invoke[ObjectReference](exprRef, "evaluate", "()Ljava/lang/Object;", Seq.empty) + updateVariables(thread, valueArray) + thread.invoke[StringReference](output, "toString", "()Ljava/lang/String;", Seq.empty).value + catch case e: EvaluationException => + // if expr.evaluate() throws an exception, we return exception.toString as the value + // to distinguish it from an evaluation error + // throwing an exception is a valid result of evaluation + e.getMessage + end evaluate + + /** compiles the expression and returns the new expression class name to load, or compiler errors */ + private def compile( + expression: String, + location: Location, + localVariables: Seq[LocalVariable] + ): Either[String, String] = + // We assume there is no 2 files with the same name + val sourceFile = sources(location.sourceName) + val packageName = getPackageName(location.declaringType) + val outputClassName = getUniqueClassName() + val errorBuilder = StringBuilder() + val config = ExpressionCompilerConfig( + packageName = packageName, + outputClassName = outputClassName, + breakpointLine = location.lineNumber, + expression = expression, + localVariables = localVariables.toSet.map(_.name).asJava, + errorReporter = errorMsg => errorBuilder.append(errorMsg), + testMode = true + ) + val success = compiler.run(outputDir, classPath, options, sourceFile, config) + val fullyQualifiedClassName = + if packageName.isEmpty then outputClassName else s"$packageName.$outputClassName" + if success then Right(fullyQualifiedClassName) else Left(errorBuilder.toString) + end compile + + private def updateVariables(thread: ThreadReference, valueArray: ArrayReference): Unit = + // the frame reference change after each remote execution + def frame = thread.frame(0) + frame + .visibleVariables + .asScala + .toSeq + .zip(valueArray.getValues.asScala) + .map: (variable, value) => + val preparedValue = + if variable.`type`.isInstanceOf[PrimitiveType] then thread.unboxIfPrimitive(value) + else value + frame.setValue(variable, preparedValue) + + private def getPackageName(tpe: ReferenceType): String = + tpe.name.split('.').dropRight(1).mkString(".") + + private def getUniqueClassName(): String = + val id = uniqueID + uniqueID += 1 + "Expression" + id + + extension (thread: ThreadReference) + private def boxIfPrimitive(value: Value): ObjectReference = + value match + case value: PrimitiveValue => box(value) + case ref: ObjectReference => ref + + private def unboxIfPrimitive(value: Value): Value = + import ExpressionEvaluator.unboxMethods + value match + case ref: ObjectReference if unboxMethods.contains(ref.referenceType.name) => + val (methodName, sig) = unboxMethods(ref.referenceType.name) + invoke(ref, methodName, sig, Seq.empty) + case _ => value + + private def box(value: PrimitiveValue): ObjectReference = + val (className, sig) = value match + case _: BooleanValue => ("java.lang.Boolean", "(Ljava/lang/String;)Ljava/lang/Boolean;") + case _: ByteValue => ("java.lang.Byte", "(Ljava/lang/String;)Ljava/lang/Byte;") + case _: CharValue => ("java.lang.Character", "(C)Ljava/lang/Character;") + case _: DoubleValue => ("java.lang.Double", "(Ljava/lang/String;)Ljava/lang/Double;") + case _: FloatValue => ("java.lang.Float", "(Ljava/lang/String;)Ljava/lang/Float;") + case _: IntegerValue => ("java.lang.Integer", "(Ljava/lang/String;)Ljava/lang/Integer;") + case _: LongValue => ("java.lang.Long", "(Ljava/lang/String;)Ljava/lang/Long;") + case _: ShortValue => ("java.lang.Short", "(Ljava/lang/String;)Ljava/lang/Short;") + val cls = getClass(className) + val args = value match + case c: CharValue => Seq(c) + case value => Seq(mirrorOf(value.toString)) + invokeStatic(cls, "valueOf", sig, args) + + private def createArray(arrayType: String, values: Seq[Value]): ArrayReference = + val arrayClassObject = getClass(arrayType).classObject + val reflectArrayClass = getClass("java.lang.reflect.Array") + val args = Seq(arrayClassObject, mirrorOf(values.size)) + val sig = "(Ljava/lang/Class;I)Ljava/lang/Object;" + val arrayRef = invokeStatic[ArrayReference](reflectArrayClass, "newInstance", sig, args) + arrayRef.setValues(values.asJava) + arrayRef + + /** Get the remote class if it is already loaded. Otherwise you should use loadClass. */ + private def getClass(className: String): ClassType = + thread.virtualMachine.classesByName(className).get(0).asInstanceOf[ClassType] + + private def loadClass(classLoader: ClassLoaderReference, className: String): ClassType = + // Calling classLoader.loadClass would create useless class object which throws + // ClassNotPreparedException. We use java.lang.Class.forName instead. + val classClass = getClass("java.lang.Class") + val args = Seq(mirrorOf(className), mirrorOf(true), classLoader) + val sig = "(Ljava/lang/String;ZLjava/lang/ClassLoader;)Ljava/lang/Class;" + invokeStatic[ClassObjectReference](classClass, "forName", sig, args) + .reflectedType + .asInstanceOf[ClassType] + + private def invokeStatic[T <: Value]( + cls: ClassType, + methodName: String, + sig: String, + args: Seq[Value] + ): T = + val method = cls.methodsByName(methodName, sig).get(0) + remotely: + cls.invokeMethod(thread, method, args.asJava, ObjectReference.INVOKE_SINGLE_THREADED) + + // we assume there is a single constructor, otherwise we need to add sig as parameter + private def newInstance(cls: ClassType, args: Seq[Value]): ObjectReference = + val constructor = cls.methodsByName("").get(0) + remotely: + cls.newInstance(thread, constructor, args.asJava, ObjectReference.INVOKE_SINGLE_THREADED) + + private def invoke[T <: Value]( + ref: ObjectReference, + methodName: String, + sig: String, + args: Seq[Value] + ): T = + val method = ref.referenceType.methodsByName(methodName, sig).get(0) + remotely: + ref.invokeMethod(thread, method, args.asJava, ObjectReference.INVOKE_SINGLE_THREADED) + + /** wrapper for safe remote execution: + * - it catches InvocationException to extract the message of the remote exception + * - it disables GC on the returned value + */ + private def remotely[T <: Value](value: => Value): T = + val res = + try value + catch case invocationException: InvocationException => + val sig = "()Ljava/lang/String;" + val message = + invoke[StringReference](invocationException.exception, "toString", sig, List()) + throw new EvaluationException(message.value, invocationException) + // Prevent object created by the debugger to be garbage collected + // In theory we should re-enable collection later to avoid memory leak + // But maybe it is okay to have a few leaks in the tested debuggee + res match + case ref: ObjectReference => ref.disableCollection() + case _ => + res.asInstanceOf[T] + + private def mirrorOf(value: String): StringReference = thread.virtualMachine.mirrorOf(value) + private def mirrorOf(value: Int): IntegerValue = thread.virtualMachine.mirrorOf(value) + private def mirrorOf(value: Boolean): BooleanValue = thread.virtualMachine.mirrorOf(value) + end extension +end ExpressionEvaluator + +object ExpressionEvaluator: + private val unboxMethods = Map( + "java.lang.Boolean" -> ("booleanValue", "()Z"), + "java.lang.Byte" -> ("byteValue", "()B"), + "java.lang.Character" -> ("charValue", "()C"), + "java.lang.Double" -> ("doubleValue", "()D"), + "java.lang.Float" -> ("floatValue", "()F"), + "java.lang.Integer" -> ("intValue", "()I"), + "java.lang.Long" -> ("longValue", "()J"), + "java.lang.Short" -> ("shortValue", "(S)") + ) + + + def apply( + sources: Array[JFile], + flags: TestFlags, + classPath: String, + outputDir: JFile + ): ExpressionEvaluator = + val sourceMap = sources.map(s => s.getName -> s.toPath).toMap + val filteredOptions = flags.options.filterNot(_ == "-Ycheck:all") + new ExpressionEvaluator(sourceMap, filteredOptions, classPath, outputDir.toPath) diff --git a/compiler/test/dotty/tools/dotc/coverage/CoverageTests.scala b/compiler/test/dotty/tools/dotc/coverage/CoverageTests.scala index f6460180cab9..2aa811abd75c 100644 --- a/compiler/test/dotty/tools/dotc/coverage/CoverageTests.scala +++ b/compiler/test/dotty/tools/dotc/coverage/CoverageTests.scala @@ -110,9 +110,8 @@ class CoverageTests: if run then val path = if isDirectory then inputFile.toString else inputFile.getParent.toString val test = compileDir(path, options) - test.checkFilePaths.foreach { checkFilePath => - assert(checkFilePath.exists, s"Expected checkfile for $path $checkFilePath does not exist.") - } + test.checkFiles.foreach: checkFile => + assert(checkFile.exists, s"Expected checkfile for $path $checkFile does not exist.") test.checkRuns() else val test = diff --git a/compiler/test/dotty/tools/vulpix/ParallelTesting.scala b/compiler/test/dotty/tools/vulpix/ParallelTesting.scala index 3508e38bb30c..12a53a19931d 100644 --- a/compiler/test/dotty/tools/vulpix/ParallelTesting.scala +++ b/compiler/test/dotty/tools/vulpix/ParallelTesting.scala @@ -6,6 +6,7 @@ import scala.language.unsafeNulls import java.io.{File => JFile, IOException, PrintStream, ByteArrayOutputStream} import java.lang.System.{lineSeparator => EOL} +import java.lang.management.ManagementFactory import java.net.URL import java.nio.file.StandardCopyOption.REPLACE_EXISTING import java.nio.file.{Files, NoSuchFileException, Path, Paths} @@ -70,6 +71,7 @@ trait ParallelTesting extends RunnerOrchestration { self => def outDir: JFile def flags: TestFlags def sourceFiles: Array[JFile] + def checkFile: Option[JFile] def runClassPath: String = outDir.getPath + JFile.pathSeparator + flags.runClassPath @@ -183,6 +185,10 @@ trait ParallelTesting extends RunnerOrchestration { self => decompilation: Boolean = false ) extends TestSource { def sourceFiles: Array[JFile] = files.filter(isSourceFile) + + def checkFile: Option[JFile] = + sourceFiles.map(f => new JFile(f.getPath.replaceFirst("\\.(scala|java)$", ".check"))) + .find(_.exists()) } /** A test source whose files will be compiled separately according to their @@ -214,6 +220,12 @@ trait ParallelTesting extends RunnerOrchestration { self => .map { (g, f) => (g, f.sorted) } def sourceFiles = compilationGroups.map(_._2).flatten.toArray + + def checkFile: Option[JFile] = + val platform = + if allToolArgs.getOrElse(ToolName.Target, Nil).nonEmpty then s".$testPlatform" + else "" + Some(new JFile(dir.getPath + platform + ".check")).filter(_.exists) } protected def shouldSkipTestSource(testSource: TestSource): Boolean = false @@ -226,7 +238,7 @@ trait ParallelTesting extends RunnerOrchestration { self => rerun.exists(dir.getPath.contains) }) - private trait CompilationLogic { this: Test => + protected trait CompilationLogic { this: Test => def suppressErrors = false /** @@ -259,12 +271,6 @@ trait ParallelTesting extends RunnerOrchestration { self => final def countWarnings(reporters: Seq[TestReporter]) = countErrorsAndWarnings(reporters)._2 final def reporterFailed(r: TestReporter) = r.errorCount > 0 - /** - * For a given test source, returns a check file against which the result of the test run - * should be compared. Is used by implementations of this trait. - */ - final def checkFile(testSource: TestSource): Option[JFile] = (CompilationLogic.checkFilePath(testSource)).filter(_.exists) - /** * Checks if the given actual lines are the same as the ones in the check file. * If not, fails the test. @@ -340,26 +346,10 @@ trait ParallelTesting extends RunnerOrchestration { self => } } - object CompilationLogic { - private[ParallelTesting] def checkFilePath(testSource: TestSource) = testSource match { - case ts: JointCompilationSource => - ts.files.collectFirst { - case f if !f.isDirectory => - new JFile(f.getPath.replaceFirst("\\.(scala|java)$", ".check")) - } - case ts: SeparateCompilationSource => - val platform = - if testSource.allToolArgs.getOrElse(ToolName.Target, Nil).nonEmpty then - s".$testPlatform" - else "" - Option(new JFile(ts.dir.getPath + platform + ".check")) - } - } - /** Each `Test` takes the `testSources` and performs the compilation and assertions * according to the implementing class "neg", "run" or "pos". */ - private class Test(testSources: List[TestSource], times: Int, threadLimit: Option[Int], suppressAllOutput: Boolean)(implicit val summaryReport: SummaryReporting) extends CompilationLogic { test => + protected class Test(testSources: List[TestSource], times: Int, threadLimit: Option[Int], suppressAllOutput: Boolean)(implicit val summaryReport: SummaryReporting) extends CompilationLogic { test => import summaryReport._ @@ -470,7 +460,7 @@ trait ParallelTesting extends RunnerOrchestration { self => /** Print a progress bar for the current `Test` */ private def updateProgressMonitor(start: Long): Unit = - if testSourcesCompleted < sourceCount then + if testSourcesCompleted < sourceCount && !isUserDebugging then realStdout.print(s"\r${makeProgressBar(start)}") private def finishProgressMonitor(start: Long): Unit = @@ -735,7 +725,7 @@ trait ParallelTesting extends RunnerOrchestration { self => private def mkReporter = TestReporter.reporter(realStdout, logLevel = mkLogLevel) protected def diffCheckfile(testSource: TestSource, reporters: Seq[TestReporter], logger: LoggedRunnable) = - checkFile(testSource).foreach(diffTest(testSource, _, reporterOutputLines(reporters), reporters, logger)) + testSource.checkFile.foreach(diffTest(testSource, _, reporterOutputLines(reporters), reporters, logger)) private def reporterOutputLines(reporters: Seq[TestReporter]): List[String] = reporters.flatMap(_.consoleOutput.split("\n")).toList @@ -903,15 +893,15 @@ trait ParallelTesting extends RunnerOrchestration { self => verifyOutput(testSource, reporters, logger) } - private final class RunTest(testSources: List[TestSource], times: Int, threadLimit: Option[Int], suppressAllOutput: Boolean)(implicit summaryReport: SummaryReporting) + protected class RunTest(testSources: List[TestSource], times: Int, threadLimit: Option[Int], suppressAllOutput: Boolean)(implicit summaryReport: SummaryReporting) extends Test(testSources, times, threadLimit, suppressAllOutput) { private var didAddNoRunWarning = false - private def addNoRunWarning() = if (!didAddNoRunWarning) { + protected def addNoRunWarning() = if (!didAddNoRunWarning) { didAddNoRunWarning = true summaryReport.addStartingMessage { """|WARNING |------- - |Run tests were only compiled, not run - this is due to the `dotty.tests.norun` + |Run and debug tests were only compiled, not run - this is due to the `dotty.tests.norun` |property being set |""".stripMargin } @@ -938,7 +928,7 @@ trait ParallelTesting extends RunnerOrchestration { self => } override def onSuccess(testSource: TestSource, reporters: Seq[TestReporter], logger: LoggedRunnable) = - verifyOutput(checkFile(testSource), testSource.outDir, testSource, countWarnings(reporters), reporters, logger) + verifyOutput(testSource.checkFile, testSource.outDir, testSource, countWarnings(reporters), reporters, logger) } private final class NegTest(testSources: List[TestSource], times: Int, threadLimit: Option[Int], suppressAllOutput: Boolean)(implicit summaryReport: SummaryReporting) @@ -1162,12 +1152,12 @@ trait ParallelTesting extends RunnerOrchestration { self => * `aggregateTests` in the companion, which will ensure that aggregation is allowed. */ final class CompilationTest private ( - private[ParallelTesting] val targets: List[TestSource], - private[ParallelTesting] val times: Int, - private[ParallelTesting] val shouldDelete: Boolean, - private[ParallelTesting] val threadLimit: Option[Int], - private[ParallelTesting] val shouldFail: Boolean, - private[ParallelTesting] val shouldSuppressOutput: Boolean + val targets: List[TestSource], + val times: Int, + val shouldDelete: Boolean, + val threadLimit: Option[Int], + val shouldFail: Boolean, + val shouldSuppressOutput: Boolean ) { import org.junit.Assert.fail @@ -1177,7 +1167,7 @@ trait ParallelTesting extends RunnerOrchestration { self => def this(targets: List[TestSource]) = this(targets, 1, true, None, false, false) - def checkFilePaths: List[JFile] = targets.map(CompilationLogic.checkFilePath).flatten + def checkFiles: List[JFile] = targets.flatMap(_.checkFile) def copy(targets: List[TestSource], times: Int = times, @@ -1270,7 +1260,7 @@ trait ParallelTesting extends RunnerOrchestration { self => checkFail(test, "Rewrite") } - private def checkPass(test: Test, desc: String): this.type = + def checkPass(test: Test, desc: String): this.type = test.executeTestSuite() cleanup() @@ -1843,6 +1833,11 @@ trait ParallelTesting extends RunnerOrchestration { self => flags.options.sliding(2).collectFirst { case Array("-encoding", encoding) => Charset.forName(encoding) }.getOrElse(StandardCharsets.UTF_8) + + /** checks if the current process is being debugged */ + def isUserDebugging: Boolean = + val mxBean = ManagementFactory.getRuntimeMXBean + mxBean.getInputArguments.asScala.exists(_.contains("jdwp")) } object ParallelTesting { diff --git a/compiler/test/dotty/tools/vulpix/RunnerOrchestration.scala b/compiler/test/dotty/tools/vulpix/RunnerOrchestration.scala index 9047bb6737dc..a91b0de3e238 100644 --- a/compiler/test/dotty/tools/vulpix/RunnerOrchestration.scala +++ b/compiler/test/dotty/tools/vulpix/RunnerOrchestration.scala @@ -4,7 +4,7 @@ package vulpix import scala.language.unsafeNulls -import java.io.{ File => JFile, InputStreamReader, BufferedReader, PrintStream } +import java.io.{ File => JFile, InputStreamReader, IOException, BufferedReader, PrintStream } import java.nio.file.Paths import java.nio.charset.StandardCharsets import java.util.concurrent.atomic.AtomicBoolean @@ -48,10 +48,29 @@ trait RunnerOrchestration { /** Destroy and respawn process after each test */ def safeMode: Boolean - /** Running a `Test` class's main method from the specified `dir` */ + /** Open JDI connection for testing the debugger */ + def debugMode: Boolean = false + + /** Running a `Test` class's main method from the specified `classpath` */ def runMain(classPath: String, toolArgs: ToolArgs)(implicit summaryReport: SummaryReporting): Status = monitor.runMain(classPath) + /** Each method of Debuggee can be called only once, in the order of definition.*/ + trait Debuggee: + /** read the jdi port to connect the debugger */ + def readJdiPort(): Int + /** start the main method in the background */ + def launch(): Unit + /** wait until the end of the main method */ + def exit(): Status + + /** Provide a Debuggee for debugging the Test class's main method + * @param f the debugging flow: set breakpoints, launch main class, pause, step, evaluate, exit etc + */ + def debugMain(classPath: String)(f: Debuggee => Unit)(implicit summaryReport: SummaryReporting): Unit = + assert(debugMode, "debugMode is disabled") + monitor.debugMain(classPath)(f) + /** Kill all processes */ def cleanup() = monitor.killAll() @@ -70,10 +89,33 @@ trait RunnerOrchestration { def runMain(classPath: String)(implicit summaryReport: SummaryReporting): Status = withRunner(_.runMain(classPath)) - private class Runner(private var process: Process) { - private var childStdout: BufferedReader = uninitialized - private var childStdin: PrintStream = uninitialized + def debugMain(classPath: String)(f: Debuggee => Unit)(implicit summaryReport: SummaryReporting): Unit = + withRunner(_.debugMain(classPath)(f)) + + private class RunnerProcess(p: Process): + private val stdout = new BufferedReader(new InputStreamReader(p.getInputStream(), StandardCharsets.UTF_8)) + private val stdin = new PrintStream(p.getOutputStream(), /* autoFlush = */ true) + + def readLine(): String = + stdout.readLine() match + case s"Listening for transport dt_socket at address: $port" => + throw new IOException( + s"Unexpected transport dt_socket message." + + " The port is going to be lost and no debugger will be able to connect." + ) + case line => line + + def printLine(line: String): Unit = stdin.println(line) + + def getJdiPort(): Int = + stdout.readLine() match + case s"Listening for transport dt_socket at address: $port" => port.toInt + case line => throw new IOException(s"Failed getting JDI port of child JVM: got $line") + + export p.{exitValue, isAlive, destroy} + end RunnerProcess + private class Runner(private var process: RunnerProcess): /** Checks if `process` is still alive * * When `process.exitValue()` is called on an active process the caught @@ -82,96 +124,92 @@ trait RunnerOrchestration { */ def isAlive: Boolean = try { process.exitValue(); false } - catch { case _: IllegalThreadStateException => true } + catch case _: IllegalThreadStateException => true /** Destroys the underlying process and kills IO streams */ - def kill(): Unit = { + def kill(): Unit = if (process ne null) process.destroy() process = null - childStdout = null - childStdin = null - } - - /** Did add hook to kill the child VMs? */ - private val didAddCleanupCallback = new AtomicBoolean(false) /** Blocks less than `maxDuration` while running `Test.main` from `dir` */ - def runMain(classPath: String)(implicit summaryReport: SummaryReporting): Status = { - if (didAddCleanupCallback.compareAndSet(false, true)) { - // If for some reason the test runner (i.e. sbt) doesn't kill the VM, we - // need to clean up ourselves. - summaryReport.addCleanup(() => killAll()) - } - assert(process ne null, - "Runner was killed and then reused without setting a new process") - - // Makes the encapsulating RunnerMonitor spawn a new runner - def respawn(): Unit = { - process.destroy() - process = createProcess - childStdout = null - childStdin = null - } - - if (childStdin eq null) - childStdin = new PrintStream(process.getOutputStream, /* autoFlush = */ true) - - // pass file to running process - childStdin.println(classPath) + def runMain(classPath: String): Status = + assert(process ne null, "Runner was killed and then reused without setting a new process") + awaitStatusOrRespawn(startMain(classPath)) + + def debugMain(classPath: String)(f: Debuggee => Unit): Unit = + assert(process ne null, "Runner was killed and then reused without setting a new process") + + val debuggee = new Debuggee: + private var mainFuture: Future[Status] = null + def readJdiPort(): Int = process.getJdiPort() + def launch(): Unit = mainFuture = startMain(classPath) + def exit(): Status = + awaitStatusOrRespawn(mainFuture) + + try f(debuggee) + catch case e: Throwable => + // if debugging failed it is safer to respawn a new process + respawn() + throw e + end debugMain + + private def startMain(classPath: String): Future[Status] = + // pass classpath to running process + process.printLine(classPath) // Create a future reading the object: - val readOutput = Future { + Future: val sb = new StringBuilder - if (childStdout eq null) - childStdout = new BufferedReader(new InputStreamReader(process.getInputStream, StandardCharsets.UTF_8)) - - var childOutput: String = childStdout.readLine() + var childOutput: String = process.readLine() // Discard all messages until the test starts while (childOutput != ChildJVMMain.MessageStart && childOutput != null) - childOutput = childStdout.readLine() - childOutput = childStdout.readLine() + childOutput = process.readLine() + childOutput = process.readLine() - while (childOutput != ChildJVMMain.MessageEnd && childOutput != null) { + while childOutput != ChildJVMMain.MessageEnd && childOutput != null do sb.append(childOutput).append(System.lineSeparator) - childOutput = childStdout.readLine() - } + childOutput = process.readLine() - if (process.isAlive && childOutput != null) Success(sb.toString) + if process.isAlive() && childOutput != null then Success(sb.toString) else Failure(sb.toString) - } + end startMain - // Await result for `maxDuration` and then timout and destroy the - // process: + // wait status of the main class execution, respawn if failure or timeout + private def awaitStatusOrRespawn(future: Future[Status]): Status = val status = - try Await.result(readOutput, maxDuration) - catch { case _: TimeoutException => Timeout } - - // Handle failure of the VM: - status match { - case _: Success if safeMode => respawn() - case _: Success => // no need to respawn sub process - case _: Failure => respawn() - case Timeout => respawn() - } + try Await.result(future, maxDuration) + catch case _: TimeoutException => Timeout + // handle failures + status match + case _: Success if !safeMode => () // no need to respawn + case _ => respawn() // safeMode, failure or timeout status - } - } + + // Makes the encapsulating RunnerMonitor spawn a new runner + private def respawn(): Unit = + process.destroy() + process = null + process = createProcess() + end Runner /** Create a process which has the classpath of the `ChildJVMMain` and the * scala library. */ - private def createProcess: Process = { + private def createProcess(): RunnerProcess = val url = classOf[ChildJVMMain].getProtectionDomain.getCodeSource.getLocation val cp = Paths.get(url.toURI).toString + JFile.pathSeparator + Properties.scalaLibrary val javaBin = Paths.get(sys.props("java.home"), "bin", "java").toString - new ProcessBuilder(javaBin, "-Dfile.encoding=UTF-8", "-Duser.language=en", "-Duser.country=US", "-Xmx1g", "-cp", cp, "dotty.tools.vulpix.ChildJVMMain") + val args = Seq("-Dfile.encoding=UTF-8", "-Duser.language=en", "-Duser.country=US", "-Xmx1g", "-cp", cp) ++ + (if debugMode then Seq("-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,quiet=n") else Seq.empty) + val command = (javaBin +: args) :+ "dotty.tools.vulpix.ChildJVMMain" + val process = new ProcessBuilder(command*) .redirectErrorStream(true) .redirectInput(ProcessBuilder.Redirect.PIPE) .redirectOutput(ProcessBuilder.Redirect.PIPE) .start() - } + RunnerProcess(process) private val freeRunners = mutable.Queue.empty[Runner] private val busyRunners = mutable.Set.empty[Runner] @@ -180,7 +218,7 @@ trait RunnerOrchestration { while (freeRunners.isEmpty && busyRunners.size >= numberOfSlaves) wait() val runner = - if (freeRunners.isEmpty) new Runner(createProcess) + if (freeRunners.isEmpty) new Runner(createProcess()) else freeRunners.dequeue() busyRunners += runner @@ -194,12 +232,11 @@ trait RunnerOrchestration { notify() } - private def withRunner[T](op: Runner => T): T = { + private def withRunner[T](op: Runner => T)(using summaryReport: SummaryReporting): T = val runner = getRunner() val result = op(runner) freeRunner(runner) result - } def killAll(): Unit = { freeRunners.foreach(_.kill()) diff --git a/compiler/test/dotty/tools/vulpix/SummaryReport.scala b/compiler/test/dotty/tools/vulpix/SummaryReport.scala index 74612387015f..77bfdc063d08 100644 --- a/compiler/test/dotty/tools/vulpix/SummaryReport.scala +++ b/compiler/test/dotty/tools/vulpix/SummaryReport.scala @@ -30,9 +30,6 @@ trait SummaryReporting { /** Add a message that will be issued in the beginning of the summary */ def addStartingMessage(msg: String): Unit - /** Add a cleanup hook to be run upon completion */ - def addCleanup(f: () => Unit): Unit - /** Echo the summary report to the appropriate locations */ def echoSummary(): Unit @@ -51,7 +48,6 @@ final class NoSummaryReport extends SummaryReporting { def addFailedTest(msg: FailedTestInfo): Unit = () def addReproduceInstruction(instr: String): Unit = () def addStartingMessage(msg: String): Unit = () - def addCleanup(f: () => Unit): Unit = () def echoSummary(): Unit = () def echoToLog(msg: String): Unit = () def echoToLog(it: Iterator[String]): Unit = () @@ -67,7 +63,6 @@ final class SummaryReport extends SummaryReporting { private val startingMessages = new java.util.concurrent.ConcurrentLinkedDeque[String] private val failedTests = new java.util.concurrent.ConcurrentLinkedDeque[FailedTestInfo] private val reproduceInstructions = new java.util.concurrent.ConcurrentLinkedDeque[String] - private val cleanUps = new java.util.concurrent.ConcurrentLinkedDeque[() => Unit] private var passed = 0 private var failed = 0 @@ -87,9 +82,6 @@ final class SummaryReport extends SummaryReporting { def addStartingMessage(msg: String): Unit = startingMessages.add(msg) - def addCleanup(f: () => Unit): Unit = - cleanUps.add(f) - /** Both echoes the summary to stdout and prints to file */ def echoSummary(): Unit = { import SummaryReport._ @@ -131,9 +123,6 @@ final class SummaryReport extends SummaryReporting { if (!isInteractive) println(rep.toString) TestReporter.logPrintln(rep.toString) - - // Perform cleanup callback: - if (!cleanUps.isEmpty()) cleanUps.asScala.foreach(_.apply()) } private def removeColors(msg: String): String = diff --git a/docs/_docs/contributing/debugging/other-debugging.md b/docs/_docs/contributing/debugging/other-debugging.md index e8b72bcca656..db32a25dabd7 100644 --- a/docs/_docs/contributing/debugging/other-debugging.md +++ b/docs/_docs/contributing/debugging/other-debugging.md @@ -26,71 +26,6 @@ $ jdb -attach 5005 -sourcepath tests/debug/ You can run `help` for commands that supported by JDB. -## Debug Automatically with Expect - -### 1. Annotate the source code with debug information. - -Following file (`tests/debug/while.scala`) is an example of annotated source code: - -```scala -object Test { - - def main(args: Array[String]): Unit = { - var a = 1 + 2 - a = a + 3 - a = 4 + 5 // [break] [step: while] - - while (a * 8 < 100) { // [step: a += 1] - a += 1 // [step: while] [cont: print] - } - - print(a) // [break] [cont] - } -} -``` - -The debugging information is annotated as comments to the code in brackets: - -```scala -val x = f(3) // [break] [next: line=5] -val y = 5 -``` - -1. A JDB command must be wrapped in brackets, like `[step]`. All JDB commands can be used. -2. To check output of JDB for a command, use `[cmd: expect]`. -3. If `expect` is wrapped in double quotes, regex is supported. -4. Break commands are collected and set globally. -5. Other commands will be send to jdb in the order they appear in the source file - -Note that JDB uses line number starts from 1. - -### 2. Generate Expect File - -Now we can run the following command to generate an expect file: - -```shell -compiler/test/debug/Gen tests/debug/while.scala > robot -``` - -### 3. Run the Test - -First, compile the file `tests/debug/while.scala`: - -```shell -$ scalac tests/debug/while.scala -``` - -Second, run the compiled class with debugging enabled: - -```shell -$ scala -d Test -``` - -Finally, run the expect script: - -```shell -expect robot -``` ## Other tips ### Show for human readable output diff --git a/docs/_docs/contributing/testing.md b/docs/_docs/contributing/testing.md index 9ea02f071cb6..8b0ecfc217e2 100644 --- a/docs/_docs/contributing/testing.md +++ b/docs/_docs/contributing/testing.md @@ -41,6 +41,8 @@ of the `tests/` directory. A small selection of test categories include: - `tests/pos` – tests that should compile: pass if compiles successfully. - `tests/neg` – should not compile: pass if fails compilation. Useful, e.g., to test an expected compiler error. - `tests/run` – these tests not only compile but are also run. Must include at least a `@main def Test = ...`. +- `tests/debug` – these tests are compiled but also debugged. As for `tests/run` they must include at least a `@main def Test = ...` + See [Debug Tests](#debug-tests). ### Naming and Running a Test Case @@ -205,6 +207,136 @@ $ sbt > testCompilation --from-tasty ``` +## Debug Tests + +Debug tests are a variant of compilation tests located in `compiler/tests/debug`. +Similar to `tests/run`, each test case is executed. +However, instead of verifying the program's output, a debugger is attached to the running program to validate a predefined debug scenario. + +The debug scenario is specified in the `.check` file associated with each test case. +It consists of a sequence of debug steps that describe the debugger interactions and outcomes. + +**Example debug scenario**: +``` +// Pause on a breakpoint in class Test$ on line 5 +break Test$ 5 + +// Stepping in should go to line 10 +step 10 + +// Next should go to line 11 +next 11 + +// Evaluating the expression x should return 42 +eval x +result 42 +``` + +To run all the debug tests: +``` +sbt 'scala3-compiler/testOnly dotty.tools.debug.DebugTests' +``` + +### Debug Steps + +#### Breakpoint + +Syntax: + +``` +break ${runtime class} ${line number} +``` + +Examples: + +``` +break Test$ 5 +break example.A 10 +break example.A$B$1 12 +``` + +A breakpoint is defined by a fully-qualified class name and a source line. + +All breakpoints of a debug scenario are configured before the program starts. + +When the program pauses on a breakpoint, we check the class name and source line of the current frame. + +### Step in + +Syntax: +``` +step ${expected line number or method name} +``` + +Examples: +``` +step 10 +step println +``` + +A `step` request expects the program to enter into the called method or go to the next instruction. +After a step request, we check that the source line (or method name) of the current frame matches the expected one. + +Typically we use a source line when we stay in the same source file and a method name when we step in a library or JDK class. + +### Next + +A `next` request behaves similarly to `step` but jumps over a method call and stops on the next instruction. + +Syntax: +``` +next ${expected line number or method name} +``` + +Examples: +``` +next 10 +next println +``` + +### Evaluation + +Syntax: +``` +eval ${expression} +result ${expected output} + +// or in case an error is expected +eval ${expression} +error ${expected message} +``` + +It also supports multi-line expressions and multi-line error messages. + +Examples: +``` +eval fibonacci(2) +result 55 + +eval + def square(x: Int): Int = + x * x + square(2) +result 4 + +eval foo +error + :1:0 + 1 |foo + |^^^ + | Not found: foo +``` + +An `eval` request verifies that an expression can be evaluated by the `ExpressionCompiler` during a debugging session. +A `result` assertion checks the evaluation produced the expected output, while an `error` assertion checks the compilation failed with the expected error message. + +When the evaluation throws an exception, the exception is returned as a result, not an error. + +``` +eval throw new Exception("foo") +result java.lang.Exception: foo +``` + ## Unit Tests Unit tests cover the other areas of the compiler, such as interactions with the REPL, scripting tools and more. diff --git a/project/plugins.sbt b/project/plugins.sbt index a17f2253784f..9b9b90913228 100644 --- a/project/plugins.sbt +++ b/project/plugins.sbt @@ -22,9 +22,7 @@ addSbtPlugin("ch.epfl.scala" % "sbt-tasty-mima" % "1.0.0") addSbtPlugin("com.github.sbt" % "sbt-native-packager" % "1.10.0") -addSbtPlugin("com.gradle" % "sbt-develocity" % "1.1.2") - -resolvers += - "Develocity Artifactory" at "https://repo.grdev.net/artifactory/public/" - +resolvers += "Develocity Artifactory" at "https://repo.grdev.net/artifactory/public/" addSbtPlugin("com.gradle" % "sbt-develocity" % "1.2-rc-2") + +addSbtPlugin("com.github.sbt" % "sbt-jdi-tools" % "1.2.0") diff --git a/sjs-compiler-tests/test/scala/dotty/tools/dotc/ScalaJSCompilationTests.scala b/sjs-compiler-tests/test/scala/dotty/tools/dotc/ScalaJSCompilationTests.scala index 0e5bd20d5c06..88de410d05db 100644 --- a/sjs-compiler-tests/test/scala/dotty/tools/dotc/ScalaJSCompilationTests.scala +++ b/sjs-compiler-tests/test/scala/dotty/tools/dotc/ScalaJSCompilationTests.scala @@ -10,14 +10,33 @@ import reporting.TestReporter import vulpix._ @Category(Array(classOf[ScalaJSCompilationTests])) -class ScalaJSCompilationTests extends ParallelTesting { +class ScalaJSCompilationTests { import ParallelTesting._ import TestConfiguration._ import ScalaJSCompilationTests._ import CompilationTest.aggregateTests - // Test suite configuration -------------------------------------------------- + // Negative tests ------------------------------------------------------------ + + @Test def negScalaJS: Unit = { + implicit val testGroup: TestGroup = TestGroup("negScalaJS") + aggregateTests( + compileFilesInDir("tests/neg-scalajs", scalaJSOptions), + ).checkExpectedErrors() + } + + @Test def runScalaJS: Unit = { + implicit val testGroup: TestGroup = TestGroup("runScalaJS") + aggregateTests( + compileFilesInDir("tests/run", scalaJSOptions), + ).checkRuns() + } +} + +object ScalaJSCompilationTests extends ParallelTesting { + implicit val summaryReport: SummaryReporting = new SummaryReport + // Test suite configuration -------------------------------------------------- def maxDuration = 60.seconds def numberOfSlaves = 5 def safeMode = Properties.testsSafeMode @@ -26,14 +45,9 @@ class ScalaJSCompilationTests extends ParallelTesting { def updateCheckFiles: Boolean = Properties.testsUpdateCheckfile def failedTests = TestReporter.lastRunFailedTests - // Negative tests ------------------------------------------------------------ - - @Test def negScalaJS: Unit = { - implicit val testGroup: TestGroup = TestGroup("negScalaJS") - aggregateTests( - compileFilesInDir("tests/neg-scalajs", scalaJSOptions), - ).checkExpectedErrors() - } + @AfterClass def tearDown(): Unit = + cleanup() + summaryReport.echoSummary() // Run tests ----------------------------------------------------------------- @@ -57,16 +71,4 @@ class ScalaJSCompilationTests extends ParallelTesting { t.printStackTrace(new java.io.PrintWriter(writer)) Failure(writer.toString()) end runMain - - @Test def runScalaJS: Unit = { - implicit val testGroup: TestGroup = TestGroup("runScalaJS") - aggregateTests( - compileFilesInDir("tests/run", scalaJSOptions), - ).checkRuns() - } -} - -object ScalaJSCompilationTests { - implicit val summaryReport: SummaryReporting = new SummaryReport - @AfterClass def cleanup(): Unit = summaryReport.echoSummary() } diff --git a/tasty/test/dotty/tools/tasty/BuildTastyVersionTest.scala b/tasty/test/dotty/tools/tasty/BuildTastyVersionTest.scala index d2e62e1f9eb0..548399b233d6 100644 --- a/tasty/test/dotty/tools/tasty/BuildTastyVersionTest.scala +++ b/tasty/test/dotty/tools/tasty/BuildTastyVersionTest.scala @@ -9,10 +9,10 @@ import TastyBuffer._ class BuildTastyVersionTest { val CurrentTastyVersion = TastyVersion(TastyFormat.MajorVersion, TastyFormat.MinorVersion, TastyFormat.ExperimentalVersion) - + // Needs to be defined in build Test/envVars val ExpectedTastyVersionEnvVar = "EXPECTED_TASTY_VERSION" - + @Test def testBuildTastyVersion(): Unit = { val expectedVersion = sys.env.get(ExpectedTastyVersionEnvVar) .getOrElse(fail(s"Env variable $ExpectedTastyVersionEnvVar not defined")) diff --git a/tests/debug-custom-args/eval-explicit-nulls.check b/tests/debug-custom-args/eval-explicit-nulls.check new file mode 100644 index 000000000000..1c83191f645c --- /dev/null +++ b/tests/debug-custom-args/eval-explicit-nulls.check @@ -0,0 +1,13 @@ +break Test$ 4 +eval msg.size +result 13 +eval msg = null +error Type Mismatch Error +eval + val x: String = null + println(x) +error Type Mismatch Error +eval + val x: String | Null = null + x.nn +result java.lang.NullPointerException: tried to cast away nullability, but value is null diff --git a/tests/debug-custom-args/eval-explicit-nulls.scala b/tests/debug-custom-args/eval-explicit-nulls.scala new file mode 100644 index 000000000000..921bdde7eb15 --- /dev/null +++ b/tests/debug-custom-args/eval-explicit-nulls.scala @@ -0,0 +1,4 @@ +object Test: + def main(args: Array[String]): Unit = + var msg = "Hello, world!" + println(msg) diff --git a/tests/debug/eval-at-default-arg.check b/tests/debug/eval-at-default-arg.check new file mode 100644 index 000000000000..79885c035ab3 --- /dev/null +++ b/tests/debug/eval-at-default-arg.check @@ -0,0 +1,3 @@ +break Test$ 6 +eval x + 1 +result 4 diff --git a/tests/debug/eval-at-default-arg.scala b/tests/debug/eval-at-default-arg.scala new file mode 100644 index 000000000000..6ca064308bb4 --- /dev/null +++ b/tests/debug/eval-at-default-arg.scala @@ -0,0 +1,8 @@ +object Test: + def main(args: Array[String]): Unit = + foo(3)() + + def foo(x: Int)( + y: Int = x + 1 + ): Unit = + println("foo") diff --git a/tests/debug/eval-by-name-capture.check b/tests/debug/eval-by-name-capture.check new file mode 100644 index 000000000000..2a8c7940c9d4 --- /dev/null +++ b/tests/debug/eval-by-name-capture.check @@ -0,0 +1,4 @@ +break Test$ 5 // main +break Test$ 5 // main$$anonfun$1 +eval x +result hello diff --git a/tests/debug/eval-by-name-capture.scala b/tests/debug/eval-by-name-capture.scala new file mode 100644 index 000000000000..c74d97eddc54 --- /dev/null +++ b/tests/debug/eval-by-name-capture.scala @@ -0,0 +1,7 @@ +object Test: + def main(args: Array[String]): Unit = + val x = "hello" + m: + x + ", world!" + + def m(y: => String): Unit = println(y) diff --git a/tests/debug/eval-by-name.check b/tests/debug/eval-by-name.check new file mode 100644 index 000000000000..87e29543f230 --- /dev/null +++ b/tests/debug/eval-by-name.check @@ -0,0 +1,33 @@ +break Test$ 11 +eval x +result foo +eval m +result foofoo +eval A().m +result fo +eval this.m("bar") +result barbarba + +break Test$ 7 +eval x +result foo +eval m +result foofoo +eval A().m +result fo + +break Test$A$1 10 +eval x +result foo +eval m +result fo +eval A().m +result fo + +break A 14 +eval x +result bar +eval m +result bar +eval A("foo").m +result foo diff --git a/tests/debug/eval-by-name.scala b/tests/debug/eval-by-name.scala new file mode 100644 index 000000000000..dc97c07670ed --- /dev/null +++ b/tests/debug/eval-by-name.scala @@ -0,0 +1,14 @@ +object Test: + def main(args: Array[String]): Unit = + println(m("foo") + A("bar").m) + + def m(x: => String): String = + def m: String = + x + x + class A: + def m: String = + x.take(2) + m + A().m + +class A(x: => String): + def m: String = x diff --git a/tests/debug/eval-captured-value-class.check b/tests/debug/eval-captured-value-class.check new file mode 100644 index 000000000000..d8dcee0cdcdb --- /dev/null +++ b/tests/debug/eval-captured-value-class.check @@ -0,0 +1,23 @@ +break Test$ 14 +eval new A("foo") +result fo +eval m("bar") +result ba + +break Test$A$1 9 +eval size +result 2 +eval size.value +result 2 +eval new A("foo") +result fo + +break Test$ 12 +eval size +result 2 +eval size.value +result 2 +eval new A("foo") +result fo +eval m("bar") +result ba diff --git a/tests/debug/eval-captured-value-class.scala b/tests/debug/eval-captured-value-class.scala new file mode 100644 index 000000000000..842906469ba4 --- /dev/null +++ b/tests/debug/eval-captured-value-class.scala @@ -0,0 +1,15 @@ +class Size(val value: Int) extends AnyVal + +object Test: + def main(args: Array[String]): Unit = + val size = new Size(2) + + class A(msg: String): + override def toString: String = + msg.take(size.value) + + def m(msg: String): String = + msg.take(size.value) + + println(new A("foo")) + println(m("bar")) diff --git a/tests/debug/eval-captures.check b/tests/debug/eval-captures.check new file mode 100644 index 000000000000..a971b20fbefe --- /dev/null +++ b/tests/debug/eval-captures.check @@ -0,0 +1,57 @@ +break A 26 +eval (new B).m +result x1x2x3x4 + +break A$B$1 22 +eval x1 +result x1 +eval m // local def m +result x1x2x3x4 +eval (new B).m +result x1x2x3x4 +eval A.this.m // compiles but throws NoSuchFieldException +result java.lang.NoSuchFieldException: $outer + +break A$B$1 21 +eval x1 +result x1 +eval x2 +result x2 +eval m +result x1x2x3x4 +eval (new C).m +result x1x2x3x4 +eval (new B).m +result x1x2x3x4 + +break A$B$1$C$1 19 +eval x1 +result x1 +eval x2 +result x2 +eval x3 +result x3 +eval x4 +result x4 +eval m +result x1x2x3x4 +eval (new C).m +result x1x2x3x4 +eval (new B).m +result x1x2x3x4 + +break A$B$1$C$1 18 +eval x1 +result x1 +eval x2 +result x2 +eval x3 +result x3 +eval x4 +result x4 +eval m +result x1x2x3x4 +eval (new C).m +result x1x2x3x4 +eval (new B).m +result x1x2x3x4 diff --git a/tests/debug/eval-captures.scala b/tests/debug/eval-captures.scala new file mode 100644 index 000000000000..b417788404ef --- /dev/null +++ b/tests/debug/eval-captures.scala @@ -0,0 +1,26 @@ +object Test: + def main(args: Array[String]): Unit = + val a = new A + println(a.m) + +class A: + def m: String = + val x1 = "x1" + class B: + def m: String = + val x2 = "x2" + def m: String = + val x3 = "x3" + class C: + def m: String = + val x4 = "x4" + def m: String = + x1 + x2 + x3 + x4 + m + val c = new C + c.m + m + end m + end B + val b = new B + b.m diff --git a/tests/debug/eval-encoding.check b/tests/debug/eval-encoding.check new file mode 100644 index 000000000000..a37d965f3bf6 --- /dev/null +++ b/tests/debug/eval-encoding.check @@ -0,0 +1,3 @@ +break Test$ 4 +eval | + new <> + &(":") + ! +result |<>&(:)! diff --git a/tests/debug/eval-encoding.scala b/tests/debug/eval-encoding.scala new file mode 100644 index 000000000000..79e1286e1f70 --- /dev/null +++ b/tests/debug/eval-encoding.scala @@ -0,0 +1,8 @@ +object Test: + def main(args: Array[String]): Unit = + val ! = "!" + println(| + new <> + &(":") + !) + private val | = "|" + private class <> : + override def toString: String = "<>" + private def &(`:`: String): String = s"&(${`:`})" \ No newline at end of file diff --git a/tests/debug/eval-enum.check b/tests/debug/eval-enum.check new file mode 100644 index 000000000000..02faca9f5a9e --- /dev/null +++ b/tests/debug/eval-enum.check @@ -0,0 +1,18 @@ +break B$C 10 // B$C. +break Test$ 18 +eval A.A1.a +result 1 +eval A.A2.a +result 2 + +break B 13 +eval C.C1.m +result bb +eval C.C2("bb").m +result bbb + +break B$C 10 +eval C1.m +result bb +eval C2("bb").m +result bbb diff --git a/tests/debug/eval-enum.scala b/tests/debug/eval-enum.scala new file mode 100644 index 000000000000..3ce4747c9fbe --- /dev/null +++ b/tests/debug/eval-enum.scala @@ -0,0 +1,18 @@ +enum A(val a: Int) extends java.lang.Enum[A]: + case A1 extends A(1) + case A2 extends A(2) + +class B(b: String): + private enum C(c: String): + case C1 extends C(b) + case C2(x: String) extends C(x) + + def m: String = b + c + end C + + def bar: String = C.C1.m + +object Test: + def main(args: Array[String]): Unit = + val b = new B("b") + println(b.bar) diff --git a/tests/debug/eval-error-pos.check b/tests/debug/eval-error-pos.check new file mode 100644 index 000000000000..22937505e99e --- /dev/null +++ b/tests/debug/eval-error-pos.check @@ -0,0 +1,7 @@ +break Test$ 3 +eval foo +error + :1:0 + 1 |foo + |^^^ + | Not found: foo diff --git a/tests/debug/eval-error-pos.scala b/tests/debug/eval-error-pos.scala new file mode 100644 index 000000000000..04adae25f1c9 --- /dev/null +++ b/tests/debug/eval-error-pos.scala @@ -0,0 +1,3 @@ +object Test: + def main(args: Array[String]): Unit = + println("Hello, World!") diff --git a/tests/debug/eval-exception.check b/tests/debug/eval-exception.check new file mode 100644 index 000000000000..aaa776c41bca --- /dev/null +++ b/tests/debug/eval-exception.check @@ -0,0 +1,11 @@ +break Test$ 7 +eval throwException() +result java.lang.Exception: foo +eval throw new Exception("bar") +result java.lang.Exception: bar +eval + try throwException() + catch case e: Exception => "caught" +result caught +eval assert(false, "fizz") +result java.lang.AssertionError: assertion failed: fizz diff --git a/tests/debug/eval-exception.scala b/tests/debug/eval-exception.scala new file mode 100644 index 000000000000..bcd97b143c67 --- /dev/null +++ b/tests/debug/eval-exception.scala @@ -0,0 +1,7 @@ +object Test: + def main(args: Array[String]): Unit = + try throwException() + catch case e: Exception => () + + def throwException(): Unit = + throw new Exception("foo") diff --git a/tests/debug/eval-fields.check b/tests/debug/eval-fields.check new file mode 100644 index 000000000000..bbe5c124896c --- /dev/null +++ b/tests/debug/eval-fields.check @@ -0,0 +1,25 @@ +break Test$ 4 +eval a.a1 +result a.a1 +eval a.B.b1 +result a.B.b1 +eval new A("aa", 2).a1 +result aa.a1 +eval new A("aa", 2).B.b1 +result aa.B.b1 + +break A 17 +eval name +result a +eval this.n +result 1 +eval a2 +result a.a2 +eval new A("aa", 2).a2 +result aa.a2 +eval B.b1 +result a.B.b1 +eval C.c1 +result a.C.c1 +eval new A("aa", 2).C.c1 +result aa.C.c1 diff --git a/tests/debug/eval-fields.scala b/tests/debug/eval-fields.scala new file mode 100644 index 000000000000..8363b98d6b80 --- /dev/null +++ b/tests/debug/eval-fields.scala @@ -0,0 +1,17 @@ +object Test: + def main(args: Array[String]): Unit = + val a = new A("a", 1) + println(a) + +class A(name: String, val n: Int): + val a1 = s"$name.a1" + private val a2 = s"$name.a2" + + object B: + val b1 = s"$name.B.b1" + + private object C: + val c1 = s"$name.C.c1" + + override def toString: String = + name + a2 diff --git a/tests/debug/eval-i425.check b/tests/debug/eval-i425.check new file mode 100644 index 000000000000..d8e13c75c9bf --- /dev/null +++ b/tests/debug/eval-i425.check @@ -0,0 +1,7 @@ +break Test$ 7 +eval patch.span +result 0 +eval patch.span = Span(1) +result () +eval patch.span +result 1 diff --git a/tests/debug/eval-i425.scala b/tests/debug/eval-i425.scala new file mode 100644 index 000000000000..9dfe2b87ae26 --- /dev/null +++ b/tests/debug/eval-i425.scala @@ -0,0 +1,10 @@ +// https://github.com/scalacenter/scala-debug-adapter/issues/425 +object Test: + private class Patch(var span: Span) + + def main(args: Array[String]): Unit = + val patch = new Patch(new Span(0)) + println("ok") + +class Span(val start: Int) extends AnyVal: + def end: Int = start + 1 diff --git a/tests/debug/eval-i485.check b/tests/debug/eval-i485.check new file mode 100644 index 000000000000..a7a31fc230e0 --- /dev/null +++ b/tests/debug/eval-i485.check @@ -0,0 +1,11 @@ +break Test$ 16 +eval b.a1 +result a1 +eval b.a2 = 2; b.a2 +result 2 +eval b.m +result m +eval new b.D +result D +eval b.D +result D$ diff --git a/tests/debug/eval-i485.scala b/tests/debug/eval-i485.scala new file mode 100644 index 000000000000..5f7d7f9d09c5 --- /dev/null +++ b/tests/debug/eval-i485.scala @@ -0,0 +1,16 @@ +// https://github.com/scalacenter/scala-debug-adapter/issues/425 +class A: + val a1 = "a1" + var a2 = 1 + def m = "m" + class D: + override def toString: String = "D" + object D: + override def toString: String = "D$" + +object Test: + private class B extends A + + def main(args: Array[String]): Unit = + val b = new B + println("foo") diff --git a/tests/debug/eval-in-case-def.check b/tests/debug/eval-in-case-def.check new file mode 100644 index 000000000000..a91439c17b35 --- /dev/null +++ b/tests/debug/eval-in-case-def.check @@ -0,0 +1,3 @@ +break Test$ 6 +eval n + m +result 2 diff --git a/tests/debug/eval-in-case-def.scala b/tests/debug/eval-in-case-def.scala new file mode 100644 index 000000000000..ab502e36fc79 --- /dev/null +++ b/tests/debug/eval-in-case-def.scala @@ -0,0 +1,6 @@ +object Test: + def main(args: Array[String]): Unit = + val n = 1 + n match + case m => + println(n + m) diff --git a/tests/debug/eval-in-for-comprehension.check b/tests/debug/eval-in-for-comprehension.check new file mode 100644 index 000000000000..fb0d62135efb --- /dev/null +++ b/tests/debug/eval-in-for-comprehension.check @@ -0,0 +1,34 @@ +break Test$ 5 // in main +eval list(0) +result 1 +// TODO can we remove debug line in adapted methods? +break Test$ 5 // in main$$anonfun$adapted$1 +break Test$ 6 // in main$$anonfun$1 +eval list(0) +result 1 +eval x +result 1 +break Test$ 6 // in main$$anonfun$1$$anonfun$adapted$1 +break Test$ 7 // in main$$anonfun$1$$anonfun$1 +eval x + y +result 2 +// TODO this line position does not make any sense +break Test$ 6 // in main$$anonfun$1$$anonfun$1 +break Test$ 7 // in main$$anonfun$1$$anonfun$1 +break Test$ 6 // in main$$anonfun$1$$anonfun$2 +break Test$ 6 // in main$$anonfun$1$$anonfun$2 +break Test$ 7 // in main$$anonfun$1$$anonfun$2 + +break Test$ 11 // in main$$anonfun$2 +eval x +result 1 + +break Test$ 13 // in main +eval list(0) +result 1 +break Test$ 13 // in main$$anonfun$4 + +break Test$ 14 // in main +eval list(0) +result 1 +break Test$ 14 // in main$$anonfun$5 diff --git a/tests/debug/eval-in-for-comprehension.scala b/tests/debug/eval-in-for-comprehension.scala new file mode 100644 index 000000000000..0ea86fbb0302 --- /dev/null +++ b/tests/debug/eval-in-for-comprehension.scala @@ -0,0 +1,14 @@ +object Test: + def main(args: Array[String]): Unit = + val list = List(1) + for + x <- list + y <- list + z = x + y + yield x + for + x <- list + if x == 1 + yield x + for x <- list yield x + for x <- list do println(x) \ No newline at end of file diff --git a/tests/debug/eval-inline.check b/tests/debug/eval-inline.check new file mode 100644 index 000000000000..7c56eff72a67 --- /dev/null +++ b/tests/debug/eval-inline.check @@ -0,0 +1,17 @@ +break Test$ 4 +eval m1 +result 42 +eval m2(y) +result 2 +eval x + 1 +result 2 +eval test1(42) +result Test(42) +eval test2 +result 42 +eval m2(test2) +result 42 +eval + inline val x = 3 + x +result 3 diff --git a/tests/debug/eval-inline.scala b/tests/debug/eval-inline.scala new file mode 100644 index 000000000000..9a77d09956bb --- /dev/null +++ b/tests/debug/eval-inline.scala @@ -0,0 +1,12 @@ +object Test: + def main(args: Array[String]): Unit = + inline val y = 2 + println("Hello, World!") + + inline def m1: Int = 42 + inline def m2(inline x: Int): Int = x + private inline val x = 1 + + inline def test1(inline x: Int): Test = Test(x) + inline def test2: Int = test1(42).x + case class Test(x: Int) diff --git a/tests/debug/eval-inner-class.check b/tests/debug/eval-inner-class.check new file mode 100644 index 000000000000..305c3963046e --- /dev/null +++ b/tests/debug/eval-inner-class.check @@ -0,0 +1,27 @@ +break Test$ 4 +eval a1 +result A +eval a2(new A) +result a2 +eval a1.a3 +result a3 +eval (new A).a3("foo") +result a3(foo) +eval new test.B +result B +eval test.b2(test.b1) +result b2 +eval test.b1.b3 +result b3 +eval (new test.B).b3("foo") +result b3(foo) + +break Test 17 +eval new B +result B +eval b2(new this.B) +result b2 +eval (new B).b3 +result b3 +eval b1.b3("foo") +result b3(foo) diff --git a/tests/debug/eval-inner-class.scala b/tests/debug/eval-inner-class.scala new file mode 100644 index 000000000000..f17118434b59 --- /dev/null +++ b/tests/debug/eval-inner-class.scala @@ -0,0 +1,25 @@ +object Test: + def main(args: Array[String]): Unit = + val test = new Test + test.m() + + private def a1: A = new A + private def a2(a: A): String = "a2" + + class A: + val a3: String = "a3" + def a3(x: String): String = s"a3($x)" + override def toString: String = "A" +end Test + +class Test: + def m(): Unit = + println("test.m()") + + private def b1: B = new B + private def b2(b: B) = "b2" + + private class B: + val b3: String = "b3" + def b3(x: String): String = s"b3($x)" + override def toString: String = "B" diff --git a/tests/debug/eval-intersection-type.check b/tests/debug/eval-intersection-type.check new file mode 100644 index 000000000000..1091a87f5154 --- /dev/null +++ b/tests/debug/eval-intersection-type.check @@ -0,0 +1,3 @@ +break Test$ 8 +eval c.m +result m diff --git a/tests/debug/eval-intersection-type.scala b/tests/debug/eval-intersection-type.scala new file mode 100644 index 000000000000..901162cf40da --- /dev/null +++ b/tests/debug/eval-intersection-type.scala @@ -0,0 +1,8 @@ +class A +trait B: + def m: String = "m" + +object Test: + def main(args: Array[String]): Unit = + val c: A & B = new A with B {} + println(c.m) diff --git a/tests/debug/eval-java-protected-members.check b/tests/debug/eval-java-protected-members.check new file mode 100644 index 000000000000..96cddde25c8a --- /dev/null +++ b/tests/debug/eval-java-protected-members.check @@ -0,0 +1,13 @@ +break Test$ 3 +eval example.A.x1 +result x1 +eval example.A.x1 = "y1"; example.A.x1 +result y1 +eval example.A.m1() +result m1 +eval x2 +result x2 +eval x2 = "y2"; x2 +result y2 +eval m2() +result m2 diff --git a/tests/debug/eval-java-protected-members/A.java b/tests/debug/eval-java-protected-members/A.java new file mode 100644 index 000000000000..16ba0dd15fbf --- /dev/null +++ b/tests/debug/eval-java-protected-members/A.java @@ -0,0 +1,13 @@ +package example; + +public class A { + protected static String x1 = "x1"; + protected static String m1() { + return "m1"; + } + + protected String x2 = "x2"; + protected String m2() { + return "m2"; + } +} diff --git a/tests/debug/eval-java-protected-members/Test.scala b/tests/debug/eval-java-protected-members/Test.scala new file mode 100644 index 000000000000..e7cd0f4e1e42 --- /dev/null +++ b/tests/debug/eval-java-protected-members/Test.scala @@ -0,0 +1,3 @@ +object Test extends example.A: + def main(args: Array[String]): Unit = + println("Hello, World!") diff --git a/tests/debug/eval-lambdas.check b/tests/debug/eval-lambdas.check new file mode 100644 index 000000000000..b4f955687684 --- /dev/null +++ b/tests/debug/eval-lambdas.check @@ -0,0 +1,7 @@ +break Test$ 6 +eval List(1, 2, 3).map(_ * a * b * c).sum +result 36 + +break A 14 +eval List(1, 2, 3).map(_ * a * b * c).sum +result 36 diff --git a/tests/debug/eval-lambdas.scala b/tests/debug/eval-lambdas.scala new file mode 100644 index 000000000000..24a4c8a5a5f9 --- /dev/null +++ b/tests/debug/eval-lambdas.scala @@ -0,0 +1,14 @@ +object Test: + val a = 1 + private val b = 2 + def main(args: Array[String]): Unit = + val c = 3 + println(a + b + c) + (new A).m() + +class A: + val a = 1 + private val b = 2 + def m() = + val c = 3 + println(a + b + c) diff --git a/tests/debug/eval-lazy-val.check b/tests/debug/eval-lazy-val.check new file mode 100644 index 000000000000..90e8d69a823e --- /dev/null +++ b/tests/debug/eval-lazy-val.check @@ -0,0 +1,12 @@ +break A 9 +eval x +result 1 +eval y +// it is not supported because the compiler does not declare y as a local variable in the class file +error local lazy val not supported +eval A.z +result 3 +eval + lazy val z = 4 + z +result 4 diff --git a/tests/debug/eval-lazy-val.scala b/tests/debug/eval-lazy-val.scala new file mode 100644 index 000000000000..2acee77d7f20 --- /dev/null +++ b/tests/debug/eval-lazy-val.scala @@ -0,0 +1,12 @@ +object Test: + def main(args: Array[String]): Unit = + (new A).m + +class A: + private lazy val x = 1 + def m: Int = + lazy val y = 2 + x + y + A.z + +object A: + private lazy val z = 3 diff --git a/tests/debug/eval-local-class-in-value-class.check b/tests/debug/eval-local-class-in-value-class.check new file mode 100644 index 000000000000..a5c354bd7421 --- /dev/null +++ b/tests/debug/eval-local-class-in-value-class.check @@ -0,0 +1,15 @@ +break A$ 6 +eval this.m(1) +result f +eval (new B).m +result fo + +break A$B$1 5 +eval value +result foo +eval size +result 2 +eval m +result fo +eval A.this.m(1) +result f diff --git a/tests/debug/eval-local-class-in-value-class.scala b/tests/debug/eval-local-class-in-value-class.scala new file mode 100644 index 000000000000..0d415378a26d --- /dev/null +++ b/tests/debug/eval-local-class-in-value-class.scala @@ -0,0 +1,11 @@ +class A(val value: String) extends AnyVal: + def m(size: Int): String = + class B: + def m: String = + value.take(size) + (new B).m + +object Test: + def main(args: Array[String]): Unit = + val a = new A("foo") + println(a.m(2)) diff --git a/tests/debug/eval-local-class.check b/tests/debug/eval-local-class.check new file mode 100644 index 000000000000..7c7ce67cf08d --- /dev/null +++ b/tests/debug/eval-local-class.check @@ -0,0 +1,9 @@ +break A$B$1 9 +eval new B +result B +eval x1 + x2 +result x1x2 +eval A.this.x1 +result Ax1 +eval this.x2 +result Bx2 diff --git a/tests/debug/eval-local-class.scala b/tests/debug/eval-local-class.scala new file mode 100644 index 000000000000..05644c7f8bd9 --- /dev/null +++ b/tests/debug/eval-local-class.scala @@ -0,0 +1,17 @@ +class A: + val x1 = "Ax1" + def m(): Unit = + val x1 = "x1" + class B: + val x2 = "Bx2" + def m(): Unit = + val x2 = "x2" + println(x1 + A.this.x1) + override def toString: String = "B" + val b = new B + b.m() + +object Test: + def main(args: Array[String]): Unit = + val a = new A + a.m() diff --git a/tests/debug/eval-local-method-in-value-class.check b/tests/debug/eval-local-method-in-value-class.check new file mode 100644 index 000000000000..f503891dc916 --- /dev/null +++ b/tests/debug/eval-local-method-in-value-class.check @@ -0,0 +1,15 @@ +break A$ 5 +eval this.m(1) +result ff +eval m(3) +result fofofo + +break A$ 4 +eval value +result foo +eval size +result 2 +eval m(1) +result fo +eval this.m(3) +result foofoo diff --git a/tests/debug/eval-local-method-in-value-class.scala b/tests/debug/eval-local-method-in-value-class.scala new file mode 100644 index 000000000000..612616acdd83 --- /dev/null +++ b/tests/debug/eval-local-method-in-value-class.scala @@ -0,0 +1,10 @@ +class A(val value: String) extends AnyVal: + def m(size: Int): String = + def m(mul: Int): String = + value.take(size) * mul + m(2) + +object Test: + def main(args: Array[String]): Unit = + val a = new A("foo") + println(a.m(2)) diff --git a/tests/debug/eval-local-methods.check b/tests/debug/eval-local-methods.check new file mode 100644 index 000000000000..bd00091faa65 --- /dev/null +++ b/tests/debug/eval-local-methods.check @@ -0,0 +1,15 @@ +break Test$ 12 +eval m1("foo") +result m1(foo) +eval m3 +result A +eval m2(new A) +result m2(A) + +break B 23 +eval m1("foo") +result m1(foo) +eval m3 +result C +eval m2(new C) +result m2(C) diff --git a/tests/debug/eval-local-methods.scala b/tests/debug/eval-local-methods.scala new file mode 100644 index 000000000000..1bb5df5b84b8 --- /dev/null +++ b/tests/debug/eval-local-methods.scala @@ -0,0 +1,23 @@ +object Test: + private class A: + override def toString: String = "A" + + def main(args: Array[String]): Unit = + val x1 = 1 + def m1(x: String) = s"m$x1($x)" + def m2(a: A): String = s"m2($a)" + def m3: A = new A + println(m1("x") + m2(m3)) + val b = new B + b.m() + +class B: + val x1 = 1 + private class C: + override def toString: String = "C" + + def m(): Unit = + def m1(x: String) = s"m$x1($x)" + def m2(c: C): String = s"m2($c)" + def m3: C = new C + println(m1("x") + m2(m3)) diff --git a/tests/debug/eval-macro.check b/tests/debug/eval-macro.check new file mode 100644 index 000000000000..ae759090757d --- /dev/null +++ b/tests/debug/eval-macro.check @@ -0,0 +1,11 @@ +break Test$ 6 +eval showType(msg) +result java.lang.String +eval + type Foo = Int + showType(1: Foo) +result Foo +eval + class Bar + showType(new Bar) +result Bar diff --git a/tests/debug/eval-macro/Macro.scala b/tests/debug/eval-macro/Macro.scala new file mode 100644 index 000000000000..a4c65e218419 --- /dev/null +++ b/tests/debug/eval-macro/Macro.scala @@ -0,0 +1,8 @@ +import scala.quoted.* + +object Macro: + inline def showType(inline expr: Any): String = ${showType('expr)} + + private def showType(expr: Expr[Any])(using Quotes): Expr[String] = + import quotes.reflect.* + Expr(expr.asTerm.tpe.widen.show) diff --git a/tests/debug/eval-macro/Test.scala b/tests/debug/eval-macro/Test.scala new file mode 100644 index 000000000000..4b38a60024a7 --- /dev/null +++ b/tests/debug/eval-macro/Test.scala @@ -0,0 +1,6 @@ +import Macro.showType + +object Test: + def main(args: Array[String]): Unit = + val msg = "Hello, World!" + println(showType(msg)) diff --git a/tests/debug/eval-multi-line-expr.check b/tests/debug/eval-multi-line-expr.check new file mode 100644 index 000000000000..7f4df1b31a9e --- /dev/null +++ b/tests/debug/eval-multi-line-expr.check @@ -0,0 +1,7 @@ +break Test$ 2 // breaks in Test$. +break Test$ 2 +eval + val a = new A + val b = "b" + a.toString + b +result Ab diff --git a/tests/debug/eval-multi-line-expr.scala b/tests/debug/eval-multi-line-expr.scala new file mode 100644 index 000000000000..ee23f75b2ab6 --- /dev/null +++ b/tests/debug/eval-multi-line-expr.scala @@ -0,0 +1,5 @@ +object Test: + def main(args: Array[String]): Unit = println("Hello, World!") + +class A: + override def toString = "A" diff --git a/tests/debug/eval-mutable-value-class.check b/tests/debug/eval-mutable-value-class.check new file mode 100644 index 000000000000..a14af0b761fa --- /dev/null +++ b/tests/debug/eval-mutable-value-class.check @@ -0,0 +1,23 @@ +break Test$ 9 +eval x = A(2) +result () +eval y = x + A(1); y +result 3 +eval z += A(2); z +result 3 +eval xx() +result 3 +eval (new B).yy() +result 4 + +break Test$ 12 +eval x += A(1); x +result 5 +eval xx() +result 6 + +break Test$B$1 16 +eval y += A(1); y +result 6 +eval (new B).yy() +result 7 diff --git a/tests/debug/eval-mutable-value-class.scala b/tests/debug/eval-mutable-value-class.scala new file mode 100644 index 000000000000..2982c3fa336c --- /dev/null +++ b/tests/debug/eval-mutable-value-class.scala @@ -0,0 +1,19 @@ +class A(val value: Int) extends AnyVal: + def +(x: A) = new A(value + x.value) + +object Test: + def main(args: Array[String]): Unit = + var x: A = new A(1) + var y: A = new A(1) + var z: A = new A(1) + z += new A(1) + def xx(): A = + x += new A(1) + x + class B: + def yy(): A = + y += new A(1) + y + val b = new B + val res = xx() + b.yy() + z + println(res) diff --git a/tests/debug/eval-mutable-variables.check b/tests/debug/eval-mutable-variables.check new file mode 100644 index 000000000000..7a2e7c9a9e61 --- /dev/null +++ b/tests/debug/eval-mutable-variables.check @@ -0,0 +1,33 @@ +break A 12 +eval x = 2 +result () +eval x +result 2 +eval + u = 2 + u +result 2 +eval u +result 2 +eval y += 1 +result () +eval yy() +result 3 +eval (new B).zz() +result 2 + +break A 14 +eval y+=1; y +result 4 + +break A 15 +eval y +result 5 + +break A$B$1 18 +eval z += 1; z +result 3 + +break A$B$1 19 +eval z +result 4 diff --git a/tests/debug/eval-mutable-variables.scala b/tests/debug/eval-mutable-variables.scala new file mode 100644 index 000000000000..4ed669b7a614 --- /dev/null +++ b/tests/debug/eval-mutable-variables.scala @@ -0,0 +1,21 @@ +object Test: + def main(args: Array[String]): Unit = + val a = new A + println(a.m) + +class A: + private var x = 1 + def m: Int = + var y = 1 + var z = 1 + var u = 1 // not captured + x += 1 + def yy(): Int = + y += 1 + y + class B: + def zz(): Int = + z += 1 + z + val b = new B + x + yy() + b.zz() + u diff --git a/tests/debug/eval-outer-from-init.check b/tests/debug/eval-outer-from-init.check new file mode 100644 index 000000000000..a217912ff626 --- /dev/null +++ b/tests/debug/eval-outer-from-init.check @@ -0,0 +1,13 @@ +// TODO those debug step don't make any sense +break A$B 4 +next 3 +break A$B 4 +eval x +result x + +// TODO same here +break A$B$C 6 +next 5 +break A$B$C 6 +eval x +result x diff --git a/tests/debug/eval-outer-from-init.scala b/tests/debug/eval-outer-from-init.scala new file mode 100644 index 000000000000..a1c032c271a9 --- /dev/null +++ b/tests/debug/eval-outer-from-init.scala @@ -0,0 +1,12 @@ +class A: + val x = "x" + class B: + println(x) + class C: + println(x) + new C + new B + +object Test: + def main(args: Array[String]): Unit = + new A diff --git a/tests/debug/eval-outer.check b/tests/debug/eval-outer.check new file mode 100644 index 000000000000..6500043a8237 --- /dev/null +++ b/tests/debug/eval-outer.check @@ -0,0 +1,8 @@ +break A$B$C 6 +eval a + a +result aa + +break A$D 10 +// the compilation succeeds but the evaluation throws a NoSuchFieldException +eval a + a +result java.lang.NoSuchFieldException: $outer diff --git a/tests/debug/eval-outer.scala b/tests/debug/eval-outer.scala new file mode 100644 index 000000000000..1b5bbdc8bc49 --- /dev/null +++ b/tests/debug/eval-outer.scala @@ -0,0 +1,18 @@ +class A: + private val a = "a" + class B: + class C: + def m: String = + a + a + def d: String = (new D).m + private final class D: + def m: String = + "d" + +object Test: + def main(args: Array[String]): Unit = + val a = new A + val b = new a.B + val c = new b.C + println(c.m) + println(a.d) diff --git a/tests/debug/eval-overloads.check b/tests/debug/eval-overloads.check new file mode 100644 index 000000000000..288b1797d8e1 --- /dev/null +++ b/tests/debug/eval-overloads.check @@ -0,0 +1,25 @@ +break Test$ 3 +eval m() +result m +eval m(5) +result m(5: Int) +eval m(true) +result m(true: Boolean) +eval m("foo") +result m(foo: String) +eval m(new B) +result m(b: B) +eval m(new B: A) +result m(a: A) +eval m(Array(1, 2)) +result m(xs: Array[Int]) +eval m(Array[A](new B)) +result m(xs: Array[A]) +eval m(Array(Array(1), Array(2))) +result m(xs: Array[Array[Int]]) +eval m1(Seq(1, 2, 3)) +result List(1, 2, 3) +eval m1(Vector(1, 2, 3)) +result Vector(1, 2, 3) +eval m1(Seq(true, false, true)) +result 2 diff --git a/tests/debug/eval-overloads.scala b/tests/debug/eval-overloads.scala new file mode 100644 index 000000000000..26ed5c19b22d --- /dev/null +++ b/tests/debug/eval-overloads.scala @@ -0,0 +1,18 @@ +object Test: + def main(args: Array[String]): Unit = + println("Hello, World!") + + trait A + class B extends A + + private def m(): String = "m" + private def m(n: Int): String = s"m($n: Int)" + private def m(b: Boolean): String = s"m($b: Boolean)" + private def m(str: String): String = s"m($str: String)" + private def m(a: A): String = s"m(a: A)" + private def m(b: B): String = s"m(b: B)" + private def m(xs: Array[Int]): String = s"m(xs: Array[Int])" + private def m(xs: Array[A]): String = s"m(xs: Array[A])" + private def m(xs: Array[Array[Int]]): String = s"m(xs: Array[Array[Int]])" + private def m1(xs: Seq[Int]): String = xs.toString + private def m1(xs: Seq[Boolean]): Int = xs.count(identity) diff --git a/tests/debug/eval-private-members-in-parent.check b/tests/debug/eval-private-members-in-parent.check new file mode 100644 index 000000000000..b718066c440d --- /dev/null +++ b/tests/debug/eval-private-members-in-parent.check @@ -0,0 +1,22 @@ +break ParentA$ParentB 20 // in ParentA$ParentB. +break ParentA 17 +eval x + y + z +result xyz +eval y = "yy" +result () +eval m2 +result yyz +eval (new B).m +result x + +break ParentA$ParentB 20 +eval x + y + z +result xyyz +eval y = "yyy" +result () +eval m2 +result yyyz + +break TraitA 9 +eval u = "uu"; u +result uu diff --git a/tests/debug/eval-private-members-in-parent.scala b/tests/debug/eval-private-members-in-parent.scala new file mode 100644 index 000000000000..89720bf31eaa --- /dev/null +++ b/tests/debug/eval-private-members-in-parent.scala @@ -0,0 +1,23 @@ +object Test: + def main(args: Array[String]): Unit = + val a = new A + println(a.m1) + println(a.m2) + +trait TraitA: + private var u: String = "u" + def m2: String = u + +abstract class ParentA: + private val x: String = "x" + private var y: String = "y" + private lazy val z: String = "z" + def m1: String = + val b = new B + b.m + m2 + private def m2: String = y + z + private abstract class ParentB: + def m: String = x + private class B extends ParentB + +class A extends ParentA with TraitA diff --git a/tests/debug/eval-shaded-fields-and-values.check b/tests/debug/eval-shaded-fields-and-values.check new file mode 100644 index 000000000000..3a3056a74018 --- /dev/null +++ b/tests/debug/eval-shaded-fields-and-values.check @@ -0,0 +1,5 @@ +break A$B 9 +eval x1 + x2 + x3 +result Ax1Bx2x3 +eval x1 + A.this.x2 + this.x3 +result Ax1Ax2Bx3 diff --git a/tests/debug/eval-shaded-fields-and-values.scala b/tests/debug/eval-shaded-fields-and-values.scala new file mode 100644 index 000000000000..ff97ceda7f9e --- /dev/null +++ b/tests/debug/eval-shaded-fields-and-values.scala @@ -0,0 +1,15 @@ +class A: + val x1 = "Ax1" + val x2 = "Ax2" + class B: + val x2 = "Bx2" + val x3 = "Bx3" + def m(): Unit = + val x3 = "x3" + println(x1 + x2 + x3) + +object Test: + def main(args: Array[String]): Unit = + val a = new A() + val b = new a.B() + b.m() diff --git a/tests/debug/eval-static-fields.check b/tests/debug/eval-static-fields.check new file mode 100644 index 000000000000..42174f0092ed --- /dev/null +++ b/tests/debug/eval-static-fields.check @@ -0,0 +1,36 @@ +break example.A$ 8 +eval a1 +result a1 +eval this.a2 +result a2 +eval A.a3 +result a3 +eval B +result example.A.B +eval this.B.b1 +result b1 +eval A.B.b2 +error value b2 cannot be accessed +eval B.b3 +result b3 +eval C.c1 +result c1 +eval D.d1 +result d1 +eval E +result example.E +eval E.e1 +result e1 + +// eval static fields from private object +break example.A$B$ 16 +eval b1 +result b1 +eval b2 +error Cannot access local val b2 in method as field +eval a2 +result a2 +eval C.c1 +result c1 +eval E.e1 +result e1 diff --git a/tests/debug/eval-static-fields.scala b/tests/debug/eval-static-fields.scala new file mode 100644 index 000000000000..21297a76c747 --- /dev/null +++ b/tests/debug/eval-static-fields.scala @@ -0,0 +1,30 @@ +object Test: + def main(args: Array[String]): Unit = + example.A.m() + +package example: + object A: + def m(): Unit = + println("A.m()" + a2) + B.m() + + val a1 = "a1" + private val a2 = "a2" + private[example] val a3 = "a3" + + private object B: + def m(): Unit = println("B.m()") + val b1 = "b1" + private val b2 = "b2" + private[A] val b3 = "b3" + override def toString: String = "example.A.B" + + private[A] object C: + val c1 = "c1" + + object D: + val d1 = "d1" + + private object E: + val e1 = "e1" + override def toString: String = "example.E" diff --git a/tests/debug/eval-static-java-method.check b/tests/debug/eval-static-java-method.check new file mode 100644 index 000000000000..f27ba8cb7e19 --- /dev/null +++ b/tests/debug/eval-static-java-method.check @@ -0,0 +1,5 @@ +break Test$ 3 +eval + import java.nio.file.Paths + Paths.get(".") +result . diff --git a/tests/debug/eval-static-java-method.scala b/tests/debug/eval-static-java-method.scala new file mode 100644 index 000000000000..04adae25f1c9 --- /dev/null +++ b/tests/debug/eval-static-java-method.scala @@ -0,0 +1,3 @@ +object Test: + def main(args: Array[String]): Unit = + println("Hello, World!") diff --git a/tests/debug/eval-static-methods.check b/tests/debug/eval-static-methods.check new file mode 100644 index 000000000000..7d6c782a8ca1 --- /dev/null +++ b/tests/debug/eval-static-methods.check @@ -0,0 +1,36 @@ +break example.A$ 8 +eval a1("foo") +result a1: foo +eval this.a2("foo") +result a2: foo +eval B.b1 +result b1 +eval B.b1("foo") +result b1: foo +eval B.b2("foo") +result b2: foo +eval B.b3("foo") +error method b3 cannot be accessed +eval C.c1("foo") +result c1: foo +eval C.c2("foo") +result c2: foo + +// access static methods from private object +break example.A$B$ 16 +eval a1("foo") +result a1: foo +eval A.this.a2("foo") +result a2: foo +eval B.b1 +result b1 +eval B.b1("foo") +result b1: foo +eval B.b2("foo") +result b2: foo +eval B.b3("foo") +result b3: foo +eval C.c1("foo") +result c1: foo +eval C.c2("foo") +result c2: foo diff --git a/tests/debug/eval-static-methods.scala b/tests/debug/eval-static-methods.scala new file mode 100644 index 000000000000..33987fad60c4 --- /dev/null +++ b/tests/debug/eval-static-methods.scala @@ -0,0 +1,25 @@ +object Test: + def main(args: Array[String]): Unit = + example.A.m() + +package example: + object A: + def m(): Unit = + println("A.m()") + B.m() + + def a1(str: String) = s"a1: $str" + private def a2(str: String) = s"a2: $str" + + private object B: + def m(): Unit = + println("B.m()") + + val b1 = "b1" + def b1(str: String) = s"b1: $str" + private[A] def b2(str: String) = s"b2: $str" + private def b3(str: String) = s"b3: $str" + + object C: + def c1(str: String) = s"c1: $str" + private[example] def c2(str: String) = s"c2: $str" diff --git a/tests/debug/eval-tail-rec.check b/tests/debug/eval-tail-rec.check new file mode 100644 index 000000000000..d37f4475368a --- /dev/null +++ b/tests/debug/eval-tail-rec.check @@ -0,0 +1,14 @@ +break Test$ 3 +eval f(100) +result 25 + +break Test$ 8 +eval x +result 80 +eval f(x) +result 40 +eval + @scala.annotation.tailrec + def g(x: Int): Int = if x <= 42 then g(x * 2) else x + g(21) +result 84 diff --git a/tests/debug/eval-tail-rec.scala b/tests/debug/eval-tail-rec.scala new file mode 100644 index 000000000000..28a2b93cc11f --- /dev/null +++ b/tests/debug/eval-tail-rec.scala @@ -0,0 +1,9 @@ +object Test: + def main(args: Array[String]): Unit = + println(f(80)) + + @scala.annotation.tailrec + def f(x: Int): Int = + if x <= 42 then x + else f(x/2) + diff --git a/tests/debug/eval-tuple-extractor.check b/tests/debug/eval-tuple-extractor.check new file mode 100644 index 000000000000..5f3ad9e00727 --- /dev/null +++ b/tests/debug/eval-tuple-extractor.check @@ -0,0 +1,9 @@ +break Test$ 4 +eval t(0) +result 1 +eval t._2 +result 2 +eval + val (x, y) = t + y +result 2 diff --git a/tests/debug/eval-tuple-extractor.scala b/tests/debug/eval-tuple-extractor.scala new file mode 100644 index 000000000000..c49599c394a0 --- /dev/null +++ b/tests/debug/eval-tuple-extractor.scala @@ -0,0 +1,5 @@ +object Test: + def main(args: Array[String]): Unit = + val t = (1, 2) + val (x, y) = t + println(x + y) diff --git a/tests/debug/eval-value-class.check b/tests/debug/eval-value-class.check new file mode 100644 index 000000000000..4b622ba37841 --- /dev/null +++ b/tests/debug/eval-value-class.check @@ -0,0 +1,37 @@ +break Test$ 24 +eval b1 +result foo +eval size.value +result 2 +eval b2.take(size) +result ba +eval m(bar) +result B(bar) +eval new B("fizz") +result fizz +eval b1 + new B("buzz") +result foobuzz +eval new Msg(new Size(3)) +result Hel + +break B$ 6 +eval x +result foo +eval take(size) +result fo + +break Test$ 25 +eval b1 = new B("fizz") +result () +eval size = new Size(3) +result () + +break Test$ 29 +eval a +result B(fizzbar) +eval a.take(this.size) +result B(fiz) +eval a.asInstanceOf[B] + B("buzz") +result fizzbarbuzz + +break B$ 6 diff --git a/tests/debug/eval-value-class.scala b/tests/debug/eval-value-class.scala new file mode 100644 index 000000000000..73a2f33bdceb --- /dev/null +++ b/tests/debug/eval-value-class.scala @@ -0,0 +1,31 @@ +trait A extends Any: + def take(size: Size): A + +class B(val x: String) extends AnyVal with A: + def take(size: Size): B = + new B(x.take(size.value)) + + def +(b: B): B = + new B(x + b.x) + + override def toString: String = s"B($x)" + +class Size(val value: Int) + +class Msg(size: Size): + override def toString = "Hello, World!".take(size.value) + +object Test: + var b1 = new B("foo") + private var size = new Size(2) + + def main(args: Array[String]): Unit = + val b2 = bar + println(b1.take(size)) + println(m(b1 + b2)) + + def m(a: A): A = + val size = new Size(5) + a.take(size) + + def bar: B = new B("bar") diff --git a/tests/debug/for.check b/tests/debug/for.check new file mode 100644 index 000000000000..0591d66a4030 --- /dev/null +++ b/tests/debug/for.check @@ -0,0 +1,9 @@ +break Test$ 3 +step 4 +eval b +result 72 +eval + val c = b + 1 + c + 1 +result 74 +step 10 diff --git a/tests/debug/for.scala b/tests/debug/for.scala index b2287a988a23..e5592a281d59 100644 --- a/tests/debug/for.scala +++ b/tests/debug/for.scala @@ -1,16 +1,16 @@ object Test { def main(args: Array[String]): Unit = { - val b = 8 * 9 // [break] [step: f()] - f() // [step: val a] + val b = 8 * 9 + f() 20 + b - print(b) + println(b) } def f(): Unit = { - val a = for (i <- 1 to 5; j <- 10 to 20) // [cont] - yield (i, j) // Error: incorrect reaching this line + val a = for (i <- 1 to 5; j <- 10 to 20) + yield (i, j) for (i <- 1 to 5; j <- 10 to 20) - println(i + j) // TODO: i is renamed to i$2 --> reduce debuggability + println(i + j) } } \ No newline at end of file diff --git a/tests/debug/function.check b/tests/debug/function.check new file mode 100644 index 000000000000..af6aa0437806 --- /dev/null +++ b/tests/debug/function.check @@ -0,0 +1,9 @@ +break Test$ 4 +step 5 +step 10 +break Test$ 6 +step 7 +step 8 +next apply$mcIII$sp // specialized Lambda.apply +next 10 +next 11 diff --git a/tests/debug/function.scala b/tests/debug/function.scala index 644344414464..7e099dce7a3c 100644 --- a/tests/debug/function.scala +++ b/tests/debug/function.scala @@ -1,14 +1,14 @@ object Test { def main(args: Array[String]): Unit = { val a = 1 + 2 - val b = a * 9 // [break] [step: plus] [step: c = plus] - val plus = (x: Int, y: Int) => { // [cont: x * x] - val a = x * x // [break] [step: y * y] - val b = y * y // [step: a + b] - a + b // [next] [next] + val b = a * 9 + val plus = (x: Int, y: Int) => { + val a = x * x + val b = y * y + a + b } - val c = plus(a, b) // [next: print] - print(c) // [cont] + val c = plus(a, b) + println(c) } } diff --git a/tests/debug/if.check b/tests/debug/if.check new file mode 100644 index 000000000000..de253554d239 --- /dev/null +++ b/tests/debug/if.check @@ -0,0 +1,8 @@ +break Test$ 4 +step 5 +step 6 +step 8 +step 9 +step 13 +step 16 +step 18 diff --git a/tests/debug/if.scala b/tests/debug/if.scala index af598c1cd40d..8cedcfa5bed0 100644 --- a/tests/debug/if.scala +++ b/tests/debug/if.scala @@ -1,20 +1,20 @@ object Test { def main(args: Array[String]): Unit = { - var a = 1 + 2 // [break] [step: a + 3] - a = a + 3 // [step: 4 + 5] - a = 4 + 5 // [step: if] + var a = 1 + 2 + a = a + 3 + a = 4 + 5 - if (a * 8 > 20) // [step: 9 * 9] - a = 9 * 9 // [step: if] + if (a * 8 > 20) + a = 9 * 9 else a = 34 * 23 - if (a * 8 < 20) // [step: 34 * 23] + if (a * 8 < 20) a = 9 * 9 else - a = 34 * 23 // [step: print] + a = 34 * 23 - print(a) + println(a) } } diff --git a/tests/debug/method.check b/tests/debug/method.check new file mode 100644 index 000000000000..d715b4be63fd --- /dev/null +++ b/tests/debug/method.check @@ -0,0 +1,8 @@ +break Test$ 3 +step 4 +step 5 +step 10 +step 11 +step 12 +step 5 +step 6 diff --git a/tests/debug/method.scala b/tests/debug/method.scala index 9489b0088f3e..96d234a52d28 100644 --- a/tests/debug/method.scala +++ b/tests/debug/method.scala @@ -1,14 +1,14 @@ object Test { def main(args: Array[String]): Unit = { - val a = 1 + 2 // [break] [step: a * 9] - val b = a * 9 // [step: plus] - val c = plus(a, b) // [step: x * x] - print(c) + val a = 1 + 2 + val b = a * 9 + val c = plus(a, b) + println(c) } def plus(x: Int, y: Int) = { - val a = x * x // [step: y * y] - val b = y * y // [step: a + b] - a + b // [step: plus] [step: print] [cont] + val a = x * x + val b = y * y + a + b } } diff --git a/tests/debug/nested-method.check b/tests/debug/nested-method.check new file mode 100644 index 000000000000..60c2de53bc3a --- /dev/null +++ b/tests/debug/nested-method.check @@ -0,0 +1,8 @@ +break Test$ 3 +step 4 +step 12 +step 7 +step 8 +step 9 +step 12 +step 13 diff --git a/tests/debug/nested-method.scala b/tests/debug/nested-method.scala index fcc326ccba25..a27aae833a34 100644 --- a/tests/debug/nested-method.scala +++ b/tests/debug/nested-method.scala @@ -1,15 +1,15 @@ object Test { def main(args: Array[String]): Unit = { - val a = 1 + 2 // [break] [step: a * 9] - val b = a * 9 // [step: plus] [step: x * x] + val a = 1 + 2 + val b = a * 9 def plus(x: Int, y: Int) = { - val a = x * x // [step: y * y] - val b = y * y // [step: a + b] - a + b // [step: plus] + val a = x * x + val b = y * y + a + b } - val c = plus(a, b) // [step: print] [cont] - print(c) + val c = plus(a, b) + println(c) } } \ No newline at end of file diff --git a/tests/debug/sequence.check b/tests/debug/sequence.check new file mode 100644 index 000000000000..4816863d6798 --- /dev/null +++ b/tests/debug/sequence.check @@ -0,0 +1,7 @@ +break Test$ 3 +step 4 +step 5 +step 6 +step 7 +step 8 +step 9 diff --git a/tests/debug/sequence.scala b/tests/debug/sequence.scala index a6c1e90185b9..7e948c85c692 100644 --- a/tests/debug/sequence.scala +++ b/tests/debug/sequence.scala @@ -1,11 +1,11 @@ object Test { def main(args: Array[String]): Unit = { - var a = 1 + 2 // [break] [step: a + 3] - a = a + 3 // [step: 4 + 5] - a = 4 + 5 // [step: a * 8] - a = a * 8 // [step: 9 * 9] - a = 9 * 9 // [step: 34 * 23] - a = 34 * 23 // [step: print] - print(a) // [cont] + var a = 1 + 2 + a = a + 3 + a = 4 + 5 + a = a * 8 + a = 9 * 9 + a = 34 * 23 + println(a) } } \ No newline at end of file diff --git a/tests/debug/tailrec.check b/tests/debug/tailrec.check new file mode 100644 index 000000000000..713880f8d234 --- /dev/null +++ b/tests/debug/tailrec.check @@ -0,0 +1,10 @@ +break Test$ 12 +step 13 +step 3 +step 6 +step 3 +break Test$ 14 +step 3 +step 4 +step 14 +step 15 diff --git a/tests/debug/tailrec.scala b/tests/debug/tailrec.scala index f79514fa3a99..07e63728628e 100644 --- a/tests/debug/tailrec.scala +++ b/tests/debug/tailrec.scala @@ -3,15 +3,15 @@ object Test { if (x == 0) 1 else - x * fact(x - 1) // TODO: incorrect this line when x = 0 + x * fact(x - 1) } def main(args: Array[String]): Unit = { val a = 1 + 2 - val b = a * 9 // [break] [step: fact] - val c = fact(a) // [step: x == 0] [step: fact(x - 1)] [step: x == 0] [cont] - fact(0) // [break] [step: x == 0] [step: 1] [step: fact(x - 1)] [step: print] - print(c) // [cont] + val b = a * 9 + val c = fact(a) + fact(0) + println(c) } } \ No newline at end of file diff --git a/tests/debug/while.check b/tests/debug/while.check new file mode 100644 index 000000000000..2e051cee0dfe --- /dev/null +++ b/tests/debug/while.check @@ -0,0 +1,5 @@ +break Test$ 6 +step 8 +step 9 +step 8 +break Test$ 12 diff --git a/tests/debug/while.scala b/tests/debug/while.scala index 0e5f8f8b0b9c..16f4675824e6 100644 --- a/tests/debug/while.scala +++ b/tests/debug/while.scala @@ -3,12 +3,12 @@ object Test { def main(args: Array[String]): Unit = { var a = 1 + 2 a = a + 3 - a = 4 + 5 // [break] [step: while] + a = 4 + 5 - while (a * 8 < 100) { // [step: a += 1] - a += 1 // [step: while] [cont: print] + while (a * 8 < 100) { + a += 1 } - print(a) // [break] [cont] + println(a) } }