From 15ccd28e2e56e4694444459b345979b742bac192 Mon Sep 17 00:00:00 2001 From: Ilya Chernikov Date: Sat, 25 Mar 2017 14:37:30 +0100 Subject: [PATCH] Switch old IDE/CLI repls to the new infrastructure should also fix #KT-5822 --- .../kotlin/cli/jvm/repl/ReplFromTerminal.kt | 24 +-- .../kotlin/cli/jvm/repl/ReplInterpreter.kt | 204 ++++-------------- compiler/testData/repl/analyzeErrors.repl | 2 +- compiler/testData/repl/evaluationErrors.repl | 2 +- .../repl/exceptionInValueToString.repl | 8 +- .../repl/AbstractReplInterpreterTest.kt | 12 +- 6 files changed, 68 insertions(+), 184 deletions(-) diff --git a/compiler/cli/src/org/jetbrains/kotlin/cli/jvm/repl/ReplFromTerminal.kt b/compiler/cli/src/org/jetbrains/kotlin/cli/jvm/repl/ReplFromTerminal.kt index 60e4decc51f..f2c89272943 100644 --- a/compiler/cli/src/org/jetbrains/kotlin/cli/jvm/repl/ReplFromTerminal.kt +++ b/compiler/cli/src/org/jetbrains/kotlin/cli/jvm/repl/ReplFromTerminal.kt @@ -18,7 +18,7 @@ package org.jetbrains.kotlin.cli.jvm.repl import com.intellij.openapi.Disposable import com.intellij.openapi.util.io.FileUtil -import org.jetbrains.kotlin.cli.jvm.repl.LineResult.* +import org.jetbrains.kotlin.cli.common.repl.ReplEvalResult import org.jetbrains.kotlin.cli.jvm.repl.messages.unescapeLineBreaks import org.jetbrains.kotlin.config.CompilerConfiguration import org.jetbrains.kotlin.config.KotlinCompilerVersion @@ -86,7 +86,7 @@ class ReplFromTerminal( } val lineResult = eval(line) - if (lineResult is LineResult.Incomplete) { + if (lineResult is ReplEvalResult.Incomplete) { return WhatNextAfterOneLine.INCOMPLETE } else { @@ -94,20 +94,20 @@ class ReplFromTerminal( } } - private fun eval(line: String): LineResult { - val lineResult = replInterpreter.eval(line) - when (lineResult) { - is ValueResult, UnitResult -> { + private fun eval(line: String): ReplEvalResult { + val evalResult = replInterpreter.eval(line) + when (evalResult) { + is ReplEvalResult.ValueResult, is ReplEvalResult.UnitResult -> { writer.notifyCommandSuccess() - if (lineResult is ValueResult) { - writer.outputCommandResult(lineResult.valueAsString) + if (evalResult is ReplEvalResult.ValueResult) { + writer.outputCommandResult(evalResult.value.toString()) } } - Incomplete -> writer.notifyIncomplete() - is Error.CompileTime -> writer.outputCompileError(lineResult.errorText) - is Error.Runtime -> writer.outputRuntimeError(lineResult.errorText) + is ReplEvalResult.Error.Runtime -> writer.outputRuntimeError(evalResult.message) + is ReplEvalResult.Error.CompileTime -> writer.outputRuntimeError(evalResult.message) + is ReplEvalResult.Incomplete -> writer.notifyIncomplete() } - return lineResult + return evalResult } @Throws(Exception::class) diff --git a/compiler/cli/src/org/jetbrains/kotlin/cli/jvm/repl/ReplInterpreter.kt b/compiler/cli/src/org/jetbrains/kotlin/cli/jvm/repl/ReplInterpreter.kt index 4d9e21af05b..71a1d695552 100644 --- a/compiler/cli/src/org/jetbrains/kotlin/cli/jvm/repl/ReplInterpreter.kt +++ b/compiler/cli/src/org/jetbrains/kotlin/cli/jvm/repl/ReplInterpreter.kt @@ -16,143 +16,80 @@ package org.jetbrains.kotlin.cli.jvm.repl -import com.google.common.base.Throwables import com.intellij.openapi.Disposable -import com.intellij.openapi.vfs.CharsetToolkit -import com.intellij.psi.PsiFileFactory -import com.intellij.psi.impl.PsiFileFactoryImpl -import com.intellij.testFramework.LightVirtualFile -import org.jetbrains.kotlin.cli.common.messages.AnalyzerWithCompilerReport -import org.jetbrains.kotlin.cli.common.repl.ReplClassLoader -import org.jetbrains.kotlin.cli.common.repl.ReplCodeLine -import org.jetbrains.kotlin.cli.jvm.compiler.EnvironmentConfigFiles -import org.jetbrains.kotlin.cli.jvm.compiler.KotlinCoreEnvironment +import org.jetbrains.kotlin.cli.common.messages.CompilerMessageLocation +import org.jetbrains.kotlin.cli.common.messages.CompilerMessageSeverity +import org.jetbrains.kotlin.cli.common.messages.MessageCollector +import org.jetbrains.kotlin.cli.common.messages.MessageRenderer +import org.jetbrains.kotlin.cli.common.repl.* import org.jetbrains.kotlin.cli.jvm.config.jvmClasspathRoots -import org.jetbrains.kotlin.codegen.ClassBuilderFactories -import org.jetbrains.kotlin.codegen.CompilationErrorHandler -import org.jetbrains.kotlin.codegen.KotlinCodegenFacade -import org.jetbrains.kotlin.codegen.state.GenerationState import org.jetbrains.kotlin.config.CompilerConfiguration -import org.jetbrains.kotlin.config.JVMConfigurationKeys -import org.jetbrains.kotlin.descriptors.ScriptDescriptor -import org.jetbrains.kotlin.idea.KotlinLanguage -import org.jetbrains.kotlin.parsing.KotlinParserDefinition -import org.jetbrains.kotlin.psi.KtFile -import org.jetbrains.kotlin.psi.KtScript -import org.jetbrains.kotlin.resolve.jvm.JvmClassName import org.jetbrains.kotlin.script.KotlinScriptDefinition import java.io.PrintWriter import java.net.URLClassLoader +import java.util.concurrent.atomic.AtomicInteger class ReplInterpreter( disposable: Disposable, private val configuration: CompilerConfiguration, private val replConfiguration: ReplConfiguration ): ReplConfiguration by replConfiguration { - private var lineNumber = 0 - private val earlierLines = arrayListOf() + private val lineNumber = AtomicInteger() + private val previousIncompleteLines = arrayListOf() private val classLoader: ReplClassLoader = run { val classpath = configuration.jvmClasspathRoots.map { it.toURI().toURL() } ReplClassLoader(URLClassLoader(classpath.toTypedArray(), null)) } - private val environment = run { - configuration.add(JVMConfigurationKeys.SCRIPT_DEFINITIONS, REPL_LINE_AS_SCRIPT_DEFINITION) - KotlinCoreEnvironment.createForProduction(disposable, configuration, EnvironmentConfigFiles.JVM_CONFIG_FILES) + private val messageCollector = object : MessageCollector { + private var hasErrors = false + private val messageRenderer = MessageRenderer.WITHOUT_PATHS + + override fun clear() { + hasErrors = false + } + + override fun report(severity: CompilerMessageSeverity, message: String, location: CompilerMessageLocation) { + val msg = messageRenderer.render(severity, message, location) + with (replConfiguration.writer) { + when (severity) { + CompilerMessageSeverity.EXCEPTION -> sendInternalErrorReport(msg) + CompilerMessageSeverity.ERROR -> outputCompileError(msg) + CompilerMessageSeverity.STRONG_WARNING -> {} // TODO consider reporting this and two below + CompilerMessageSeverity.WARNING -> {} + CompilerMessageSeverity.INFO -> {} + else -> {} + } + } + } + + override fun hasErrors(): Boolean = hasErrors } - private val psiFileFactory: PsiFileFactoryImpl = PsiFileFactory.getInstance(environment.project) as PsiFileFactoryImpl - private val analyzerEngine = ReplCodeAnalyzer(environment) + // TODO: add script definition with project-based resolving for IDEA repl + private val scriptCompiler: ReplCompiler by lazy { GenericReplCompiler(disposable, REPL_LINE_AS_SCRIPT_DEFINITION, configuration, messageCollector) } + private val scriptEvaluator: ReplFullEvaluator by lazy { GenericReplCompilingEvaluator(scriptCompiler, configuration.jvmClasspathRoots, classLoader, null, ReplRepeatingMode.REPEAT_ANY_PREVIOUS) } - fun eval(line: String): LineResult { - ++lineNumber + private val evalState by lazy { scriptEvaluator.createState() } + + fun eval(line: String): ReplEvalResult { val fullText = (previousIncompleteLines + line).joinToString(separator = "\n") - val virtualFile = - LightVirtualFile("line$lineNumber${KotlinParserDefinition.STD_SCRIPT_EXT}", KotlinLanguage.INSTANCE, fullText).apply { - charset = CharsetToolkit.UTF8_CHARSET - } - val psiFile = psiFileFactory.trySetupPsiForFile(virtualFile, KotlinLanguage.INSTANCE, true, false) as KtFile? - ?: error("Script file not analyzed at line $lineNumber: $fullText") - - val errorHolder = createDiagnosticHolder() - - val syntaxErrorReport = AnalyzerWithCompilerReport.reportSyntaxErrors(psiFile, errorHolder) - - if (syntaxErrorReport.isHasErrors && syntaxErrorReport.isAllErrorsAtEof) { - return if (allowIncompleteLines) { - previousIncompleteLines.add(line) - LineResult.Incomplete - } - else { - LineResult.Error.CompileTime(errorHolder.renderedDiagnostics) - } - } - - previousIncompleteLines.clear() - - if (syntaxErrorReport.isHasErrors) { - return LineResult.Error.CompileTime(errorHolder.renderedDiagnostics) - } - - val analysisResult = analyzerEngine.analyzeReplLine(psiFile, ReplCodeLine(lineNumber, 0, "fake line")) - AnalyzerWithCompilerReport.reportDiagnostics(analysisResult.diagnostics, errorHolder) - val scriptDescriptor = when (analysisResult) { - is ReplCodeAnalyzer.ReplLineAnalysisResult.WithErrors -> return LineResult.Error.CompileTime(errorHolder.renderedDiagnostics) - is ReplCodeAnalyzer.ReplLineAnalysisResult.Successful -> analysisResult.scriptDescriptor - else -> error("Unexpected result ${analysisResult::class.java}") - } - - val state = GenerationState( - psiFile.project, - ClassBuilderFactories.binaries(false), - analyzerEngine.module, - analyzerEngine.trace.bindingContext, - listOf(psiFile), - configuration) - - compileScript(psiFile.script!!, earlierLines.map(EarlierLine::getScriptDescriptor), state, CompilationErrorHandler.THROW_EXCEPTION) - - for (outputFile in state.factory.asList()) { - if (outputFile.relativePath.endsWith(".class")) { - classLoader.addClass(JvmClassName.byInternalName(outputFile.relativePath.replaceFirst("\\.class$".toRegex(), "")), - outputFile.asByteArray()) - } - } - try { - val scriptClass = classLoader.loadClass("Line$lineNumber") - val constructorParams = earlierLines.map(EarlierLine::getScriptClass).toTypedArray() - val constructorArgs = earlierLines.map(EarlierLine::getScriptInstance).toTypedArray() + val evalRes = scriptEvaluator.compileAndEval(evalState, ReplCodeLine(lineNumber.getAndIncrement(), 0, fullText), null, object : InvokeWrapper { + override fun invoke(body: () -> T): T = executeUserCode { body() } + }) - val scriptInstanceConstructor = scriptClass.getConstructor(*constructorParams) - val scriptInstance = try { - executeUserCode { scriptInstanceConstructor.newInstance(*constructorArgs) } + when { + evalRes !is ReplEvalResult.Incomplete -> previousIncompleteLines.clear() + allowIncompleteLines -> previousIncompleteLines.add(line) + else -> return ReplEvalResult.Error.CompileTime("incomplete code") } - catch (e: Throwable) { - // ignore everything in the stack trace until this constructor call - return LineResult.Error.Runtime(renderStackTrace(e.cause!!, startFromMethodName = "${scriptClass.name}.")) - } - - val rvField = scriptClass.getDeclaredField(SCRIPT_RESULT_FIELD_NAME).apply { isAccessible = true } - val rv: Any? = rvField.get(scriptInstance) - - earlierLines.add(EarlierLine(line, scriptDescriptor, scriptClass, scriptInstance)) - - if (!state.replSpecific.hasResult) { - return LineResult.UnitResult - } - val valueAsString: String = try { - executeUserCode { rv.toString() } - } - catch (e: Throwable) { - return LineResult.Error.Runtime(renderStackTrace(e, startFromMethodName = "java.lang.String.valueOf")) - } - return LineResult.ValueResult(valueAsString) + return evalRes } catch (e: Throwable) { val writer = PrintWriter(System.err) @@ -177,59 +114,8 @@ class ReplInterpreter( } companion object { - private val SCRIPT_RESULT_FIELD_NAME = "\$\$result" private val REPL_LINE_AS_SCRIPT_DEFINITION = object : KotlinScriptDefinition(Any::class) { override val name = "Kotlin REPL" } - - private fun renderStackTrace(cause: Throwable, startFromMethodName: String): String { - val newTrace = arrayListOf() - var skip = true - for (element in cause.stackTrace.reversed()) { - if ("${element.className}.${element.methodName}" == startFromMethodName) { - skip = false - } - if (!skip) { - newTrace.add(element) - } - } - - val resultingTrace = newTrace.reversed().dropLast(1) - - @Suppress("PLATFORM_CLASS_MAPPED_TO_KOTLIN", "UsePropertyAccessSyntax") - (cause as java.lang.Throwable).setStackTrace(resultingTrace.toTypedArray()) - - return Throwables.getStackTraceAsString(cause) - } - - fun compileScript( - script: KtScript, - earlierScripts: List, - state: GenerationState, - errorHandler: CompilationErrorHandler - ) { - state.replSpecific.scriptResultFieldName = SCRIPT_RESULT_FIELD_NAME - state.replSpecific.earlierScriptsForReplInterpreter = earlierScripts.toList() - - state.beforeCompile() - KotlinCodegenFacade.generatePackage( - state, - script.containingKtFile.packageFqName, - setOf(script.containingKtFile), - errorHandler - ) - } - } -} - -sealed class LineResult { - class ValueResult(val valueAsString: String): LineResult() - - object UnitResult: LineResult() - object Incomplete : LineResult() - - sealed class Error(val errorText: String): LineResult() { - class Runtime(errorText: String): Error(errorText) - class CompileTime(errorText: String): Error(errorText) } } diff --git a/compiler/testData/repl/analyzeErrors.repl b/compiler/testData/repl/analyzeErrors.repl index 3867a02111c..e0ff23282ef 100644 --- a/compiler/testData/repl/analyzeErrors.repl +++ b/compiler/testData/repl/analyzeErrors.repl @@ -1,6 +1,6 @@ >>> fun foo() = 765 >>> foo(1) -error: too many arguments for public final fun foo(): Int defined in Line1 +error: too many arguments for public final fun foo(): Int defined in Line_0 foo(1) ^ >>> foo() diff --git a/compiler/testData/repl/evaluationErrors.repl b/compiler/testData/repl/evaluationErrors.repl index ab79a2237fc..5eaa42b1ff8 100644 --- a/compiler/testData/repl/evaluationErrors.repl +++ b/compiler/testData/repl/evaluationErrors.repl @@ -6,4 +6,4 @@ java.lang.Exception: hi there >>> fun bar(): Nothing = throw AssertionError() >>> bar() java.lang.AssertionError - at Line4.bar(Unknown Source) + at Line_3.bar(Unknown Source) diff --git a/compiler/testData/repl/exceptionInValueToString.repl b/compiler/testData/repl/exceptionInValueToString.repl index e63ee6d12d1..6b58a56a844 100644 --- a/compiler/testData/repl/exceptionInValueToString.repl +++ b/compiler/testData/repl/exceptionInValueToString.repl @@ -1,9 +1,5 @@ >>> class B { override fun toString(): String { return foo() } ; fun foo(): String { return error("message") } } >>> B().toString() java.lang.IllegalStateException: message - at Line1$B.foo(line1.kts:1) - at Line1$B.toString(line1.kts:1) ->>> B() -java.lang.IllegalStateException: message - at Line1$B.foo(line1.kts:1) - at Line1$B.toString(line1.kts:1) \ No newline at end of file + at Line_0$B.foo(Line_0.kts:1) + at Line_0$B.toString(Line_0.kts:1) diff --git a/compiler/tests/org/jetbrains/kotlin/repl/AbstractReplInterpreterTest.kt b/compiler/tests/org/jetbrains/kotlin/repl/AbstractReplInterpreterTest.kt index 4c84245f78f..4d84eacdda6 100644 --- a/compiler/tests/org/jetbrains/kotlin/repl/AbstractReplInterpreterTest.kt +++ b/compiler/tests/org/jetbrains/kotlin/repl/AbstractReplInterpreterTest.kt @@ -17,8 +17,8 @@ package org.jetbrains.kotlin.repl import com.intellij.openapi.util.text.StringUtil +import org.jetbrains.kotlin.cli.common.repl.ReplEvalResult import org.jetbrains.kotlin.cli.jvm.repl.ConsoleReplConfiguration -import org.jetbrains.kotlin.cli.jvm.repl.LineResult import org.jetbrains.kotlin.cli.jvm.repl.ReplInterpreter import org.jetbrains.kotlin.test.ConfigurationKind import org.jetbrains.kotlin.test.KotlinTestUtils @@ -38,6 +38,7 @@ private val INCOMPLETE_PATTERN = Pattern.compile("\\.\\.\\.( *)(.*)$") private val TRAILING_NEWLINE_REGEX = Regex("\n$") private val INCOMPLETE_LINE_MESSAGE = "incomplete line" +private val HISTORY_MISMATCH_LINE_MESSAGE = "history mismatch" abstract class AbstractReplInterpreterTest : KtUsefulTestCase() { init { @@ -93,10 +94,11 @@ abstract class AbstractReplInterpreterTest : KtUsefulTestCase() { } val actual = when (lineResult) { - is LineResult.ValueResult -> lineResult.valueAsString - is LineResult.Error -> lineResult.errorText - LineResult.Incomplete -> INCOMPLETE_LINE_MESSAGE - LineResult.UnitResult -> "" + is ReplEvalResult.ValueResult -> lineResult.value.toString() + is ReplEvalResult.Error -> lineResult.message + is ReplEvalResult.Incomplete -> INCOMPLETE_LINE_MESSAGE + is ReplEvalResult.UnitResult -> "" + is ReplEvalResult.HistoryMismatch -> HISTORY_MISMATCH_LINE_MESSAGE } Assert.assertEquals(