Switch old IDE/CLI repls to the new infrastructure

should also fix #KT-5822
This commit is contained in:
Ilya Chernikov
2017-03-25 14:37:30 +01:00
parent e384268c8b
commit 15ccd28e2e
6 changed files with 68 additions and 184 deletions
@@ -18,7 +18,7 @@ package org.jetbrains.kotlin.cli.jvm.repl
import com.intellij.openapi.Disposable import com.intellij.openapi.Disposable
import com.intellij.openapi.util.io.FileUtil 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.cli.jvm.repl.messages.unescapeLineBreaks
import org.jetbrains.kotlin.config.CompilerConfiguration import org.jetbrains.kotlin.config.CompilerConfiguration
import org.jetbrains.kotlin.config.KotlinCompilerVersion import org.jetbrains.kotlin.config.KotlinCompilerVersion
@@ -86,7 +86,7 @@ class ReplFromTerminal(
} }
val lineResult = eval(line) val lineResult = eval(line)
if (lineResult is LineResult.Incomplete) { if (lineResult is ReplEvalResult.Incomplete) {
return WhatNextAfterOneLine.INCOMPLETE return WhatNextAfterOneLine.INCOMPLETE
} }
else { else {
@@ -94,20 +94,20 @@ class ReplFromTerminal(
} }
} }
private fun eval(line: String): LineResult { private fun eval(line: String): ReplEvalResult {
val lineResult = replInterpreter.eval(line) val evalResult = replInterpreter.eval(line)
when (lineResult) { when (evalResult) {
is ValueResult, UnitResult -> { is ReplEvalResult.ValueResult, is ReplEvalResult.UnitResult -> {
writer.notifyCommandSuccess() writer.notifyCommandSuccess()
if (lineResult is ValueResult) { if (evalResult is ReplEvalResult.ValueResult) {
writer.outputCommandResult(lineResult.valueAsString) writer.outputCommandResult(evalResult.value.toString())
} }
} }
Incomplete -> writer.notifyIncomplete() is ReplEvalResult.Error.Runtime -> writer.outputRuntimeError(evalResult.message)
is Error.CompileTime -> writer.outputCompileError(lineResult.errorText) is ReplEvalResult.Error.CompileTime -> writer.outputRuntimeError(evalResult.message)
is Error.Runtime -> writer.outputRuntimeError(lineResult.errorText) is ReplEvalResult.Incomplete -> writer.notifyIncomplete()
} }
return lineResult return evalResult
} }
@Throws(Exception::class) @Throws(Exception::class)
@@ -16,143 +16,80 @@
package org.jetbrains.kotlin.cli.jvm.repl package org.jetbrains.kotlin.cli.jvm.repl
import com.google.common.base.Throwables
import com.intellij.openapi.Disposable import com.intellij.openapi.Disposable
import com.intellij.openapi.vfs.CharsetToolkit import org.jetbrains.kotlin.cli.common.messages.CompilerMessageLocation
import com.intellij.psi.PsiFileFactory import org.jetbrains.kotlin.cli.common.messages.CompilerMessageSeverity
import com.intellij.psi.impl.PsiFileFactoryImpl import org.jetbrains.kotlin.cli.common.messages.MessageCollector
import com.intellij.testFramework.LightVirtualFile import org.jetbrains.kotlin.cli.common.messages.MessageRenderer
import org.jetbrains.kotlin.cli.common.messages.AnalyzerWithCompilerReport import org.jetbrains.kotlin.cli.common.repl.*
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.jvm.config.jvmClasspathRoots 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.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 org.jetbrains.kotlin.script.KotlinScriptDefinition
import java.io.PrintWriter import java.io.PrintWriter
import java.net.URLClassLoader import java.net.URLClassLoader
import java.util.concurrent.atomic.AtomicInteger
class ReplInterpreter( class ReplInterpreter(
disposable: Disposable, disposable: Disposable,
private val configuration: CompilerConfiguration, private val configuration: CompilerConfiguration,
private val replConfiguration: ReplConfiguration private val replConfiguration: ReplConfiguration
): ReplConfiguration by replConfiguration { ): ReplConfiguration by replConfiguration {
private var lineNumber = 0
private val earlierLines = arrayListOf<EarlierLine>() private val lineNumber = AtomicInteger()
private val previousIncompleteLines = arrayListOf<String>() private val previousIncompleteLines = arrayListOf<String>()
private val classLoader: ReplClassLoader = run { private val classLoader: ReplClassLoader = run {
val classpath = configuration.jvmClasspathRoots.map { it.toURI().toURL() } val classpath = configuration.jvmClasspathRoots.map { it.toURI().toURL() }
ReplClassLoader(URLClassLoader(classpath.toTypedArray(), null)) ReplClassLoader(URLClassLoader(classpath.toTypedArray(), null))
} }
private val environment = run { private val messageCollector = object : MessageCollector {
configuration.add(JVMConfigurationKeys.SCRIPT_DEFINITIONS, REPL_LINE_AS_SCRIPT_DEFINITION) private var hasErrors = false
KotlinCoreEnvironment.createForProduction(disposable, configuration, EnvironmentConfigFiles.JVM_CONFIG_FILES) 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 // TODO: add script definition with project-based resolving for IDEA repl
private val analyzerEngine = ReplCodeAnalyzer(environment) 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 { private val evalState by lazy { scriptEvaluator.createState() }
++lineNumber
fun eval(line: String): ReplEvalResult {
val fullText = (previousIncompleteLines + line).joinToString(separator = "\n") 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 { try {
val scriptClass = classLoader.loadClass("Line$lineNumber")
val constructorParams = earlierLines.map(EarlierLine::getScriptClass).toTypedArray() val evalRes = scriptEvaluator.compileAndEval(evalState, ReplCodeLine(lineNumber.getAndIncrement(), 0, fullText), null, object : InvokeWrapper {
val constructorArgs = earlierLines.map(EarlierLine::getScriptInstance).toTypedArray() override fun <T> invoke(body: () -> T): T = executeUserCode { body() }
})
val scriptInstanceConstructor = scriptClass.getConstructor(*constructorParams) when {
val scriptInstance = try { evalRes !is ReplEvalResult.Incomplete -> previousIncompleteLines.clear()
executeUserCode { scriptInstanceConstructor.newInstance(*constructorArgs) } allowIncompleteLines -> previousIncompleteLines.add(line)
else -> return ReplEvalResult.Error.CompileTime("incomplete code")
} }
catch (e: Throwable) { return evalRes
// ignore everything in the stack trace until this constructor call
return LineResult.Error.Runtime(renderStackTrace(e.cause!!, startFromMethodName = "${scriptClass.name}.<init>"))
}
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)
} }
catch (e: Throwable) { catch (e: Throwable) {
val writer = PrintWriter(System.err) val writer = PrintWriter(System.err)
@@ -177,59 +114,8 @@ class ReplInterpreter(
} }
companion object { companion object {
private val SCRIPT_RESULT_FIELD_NAME = "\$\$result"
private val REPL_LINE_AS_SCRIPT_DEFINITION = object : KotlinScriptDefinition(Any::class) { private val REPL_LINE_AS_SCRIPT_DEFINITION = object : KotlinScriptDefinition(Any::class) {
override val name = "Kotlin REPL" override val name = "Kotlin REPL"
} }
private fun renderStackTrace(cause: Throwable, startFromMethodName: String): String {
val newTrace = arrayListOf<StackTraceElement>()
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<ScriptDescriptor>,
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)
} }
} }
+1 -1
View File
@@ -1,6 +1,6 @@
>>> fun foo() = 765 >>> fun foo() = 765
>>> foo(1) >>> 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(1)
^ ^
>>> foo() >>> foo()
+1 -1
View File
@@ -6,4 +6,4 @@ java.lang.Exception: hi there
>>> fun bar(): Nothing = throw AssertionError() >>> fun bar(): Nothing = throw AssertionError()
>>> bar() >>> bar()
java.lang.AssertionError java.lang.AssertionError
at Line4.bar(Unknown Source) at Line_3.bar(Unknown Source)
+2 -6
View File
@@ -1,9 +1,5 @@
>>> class B { override fun toString(): String { return foo() } ; fun foo(): String { return error("message") } } >>> class B { override fun toString(): String { return foo() } ; fun foo(): String { return error("message") } }
>>> B().toString() >>> B().toString()
java.lang.IllegalStateException: message java.lang.IllegalStateException: message
at Line1$B.foo(line1.kts:1) at Line_0$B.foo(Line_0.kts:1)
at Line1$B.toString(line1.kts:1) at Line_0$B.toString(Line_0.kts:1)
>>> B()
java.lang.IllegalStateException: message
at Line1$B.foo(line1.kts:1)
at Line1$B.toString(line1.kts:1)
@@ -17,8 +17,8 @@
package org.jetbrains.kotlin.repl package org.jetbrains.kotlin.repl
import com.intellij.openapi.util.text.StringUtil 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.ConsoleReplConfiguration
import org.jetbrains.kotlin.cli.jvm.repl.LineResult
import org.jetbrains.kotlin.cli.jvm.repl.ReplInterpreter import org.jetbrains.kotlin.cli.jvm.repl.ReplInterpreter
import org.jetbrains.kotlin.test.ConfigurationKind import org.jetbrains.kotlin.test.ConfigurationKind
import org.jetbrains.kotlin.test.KotlinTestUtils 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 TRAILING_NEWLINE_REGEX = Regex("\n$")
private val INCOMPLETE_LINE_MESSAGE = "incomplete line" private val INCOMPLETE_LINE_MESSAGE = "incomplete line"
private val HISTORY_MISMATCH_LINE_MESSAGE = "history mismatch"
abstract class AbstractReplInterpreterTest : KtUsefulTestCase() { abstract class AbstractReplInterpreterTest : KtUsefulTestCase() {
init { init {
@@ -93,10 +94,11 @@ abstract class AbstractReplInterpreterTest : KtUsefulTestCase() {
} }
val actual = when (lineResult) { val actual = when (lineResult) {
is LineResult.ValueResult -> lineResult.valueAsString is ReplEvalResult.ValueResult -> lineResult.value.toString()
is LineResult.Error -> lineResult.errorText is ReplEvalResult.Error -> lineResult.message
LineResult.Incomplete -> INCOMPLETE_LINE_MESSAGE is ReplEvalResult.Incomplete -> INCOMPLETE_LINE_MESSAGE
LineResult.UnitResult -> "" is ReplEvalResult.UnitResult -> ""
is ReplEvalResult.HistoryMismatch -> HISTORY_MISMATCH_LINE_MESSAGE
} }
Assert.assertEquals( Assert.assertEquals(