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.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)
@@ -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<EarlierLine>()
private val lineNumber = AtomicInteger()
private val previousIncompleteLines = arrayListOf<String>()
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 <T> 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}.<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)
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<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
>>> 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 -1
View File
@@ -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)
+2 -6
View File
@@ -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)
at Line_0$B.foo(Line_0.kts:1)
at Line_0$B.toString(Line_0.kts:1)
@@ -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(