Scripting: switch legacy CLI REPL to the new infrastructure

This commit is contained in:
Ilya Chernikov
2022-05-13 20:44:44 +02:00
committed by teamcity
parent 6784cb63aa
commit b36d1be5f8
16 changed files with 185 additions and 62 deletions
@@ -98,7 +98,7 @@ abstract class KotlinJsr223JvmScriptEngineBase(protected val myFactory: ScriptEn
is ReplEvalResult.Error ->
when {
result is ReplEvalResult.Error.Runtime && result.cause != null ->
throw ScriptException(result.cause)
throw ScriptException((result.cause as? java.lang.Exception) ?: RuntimeException(result.cause))
result is ReplEvalResult.Error.CompileTime && result.location != null ->
throw ScriptException(result.message, result.location.path, result.location.line, result.location.column)
else -> throw ScriptException(result.message)
@@ -148,7 +148,7 @@ sealed class ReplEvalResult : Serializable {
}
sealed class Error(val message: String) : ReplEvalResult() {
class Runtime(message: String, val cause: Exception? = null) : Error(message) {
class Runtime(message: String, val cause: Throwable? = null) : Error(message) {
companion object { private val serialVersionUID: Long = 1L }
}
+2
View File
@@ -1,5 +1,6 @@
>>> throw Exception("hi there")
java.lang.Exception: hi there
at Line_0.<init>(Line_0.kts:1)
>>> fun foo() = 2
>>> foo()
2
@@ -7,3 +8,4 @@ java.lang.Exception: hi there
>>> bar()
java.lang.AssertionError
at Line_3.bar(Line_3.kts:1)
at Line_4.<init>(Line_4.kts:1)
+1
View File
@@ -3,3 +3,4 @@
java.lang.IllegalStateException: message
at Line_0$B.foo(Line_0.kts:1)
at Line_0$B.toString(Line_0.kts:1)
at Line_1.<init>(Line_1.kts:1)
+3 -3
View File
@@ -2,7 +2,7 @@
java.lang.RuntimeException
at Line_0$x$1.invoke(Line_0.kts:1)
at Line_0$x$1.invoke(Line_0.kts:1)
at Line_0.<init>(Line_0.kts:1)
>>> x
error: unresolved reference: x
x
^
java.lang.NullPointerException
at Line_1.<init>(Line_1.kts:1)
-3
View File
@@ -2,9 +2,6 @@
error: expecting an element
)(
^
error: expecting an expression
)(
^
>>> fun foo() = 98
>>> foo()
98
@@ -17,7 +17,10 @@
package org.jetbrains.kotlin.repl
import com.intellij.openapi.util.text.StringUtil
import org.jetbrains.kotlin.cli.common.messages.CompilerMessageSeverity
import org.jetbrains.kotlin.cli.common.messages.MessageRenderer
import org.jetbrains.kotlin.cli.common.repl.ReplEvalResult
import org.jetbrains.kotlin.cli.jvm.compiler.KotlinCoreEnvironment
import org.jetbrains.kotlin.script.loadScriptingPlugin
import org.jetbrains.kotlin.scripting.compiler.plugin.repl.ReplInterpreter
import org.jetbrains.kotlin.scripting.compiler.plugin.repl.configuration.ConsoleReplConfiguration
@@ -86,8 +89,14 @@ abstract class AbstractReplInterpreterTest : KtUsefulTestCase() {
protected fun doTest(path: String) {
val configuration = KotlinTestUtils.newConfiguration(ConfigurationKind.ALL, TestJdkKind.MOCK_JDK)
loadScriptingPlugin(configuration)
val projectEnvironment =
KotlinCoreEnvironment.ProjectEnvironment(
testRootDisposable,
KotlinCoreEnvironment.getOrCreateApplicationEnvironmentForTests(testRootDisposable, configuration),
configuration
)
val repl = ReplInterpreter(
testRootDisposable, configuration,
projectEnvironment, configuration,
ConsoleReplConfiguration()
)
@@ -100,6 +109,7 @@ abstract class AbstractReplInterpreterTest : KtUsefulTestCase() {
val actual = when (lineResult) {
is ReplEvalResult.ValueResult -> lineResult.value.toString()
is ReplEvalResult.Error.CompileTime -> MessageRenderer.WITHOUT_PATHS.render(CompilerMessageSeverity.ERROR, lineResult.message, lineResult.location)
is ReplEvalResult.Error -> lineResult.message
is ReplEvalResult.Incomplete -> INCOMPLETE_LINE_MESSAGE
is ReplEvalResult.UnitResult -> ""
@@ -98,6 +98,10 @@ data class ScriptDiagnostic(
}
}
fun ScriptDiagnostic.isError() =
(severity == ScriptDiagnostic.Severity.ERROR || severity == ScriptDiagnostic.Severity.FATAL) &&
(code == ScriptDiagnostic.unspecifiedException || code == ScriptDiagnostic.unspecifiedError)
/**
* The result wrapper with diagnostics container
*/
@@ -456,8 +456,10 @@ class ReplTest : TestCase() {
is ResultValue.Unit -> Assert.assertNull("#$index: Expected $expectedVal, got Unit", expectedVal)
is ResultValue.Error -> Assert.assertTrue(
"#$index: Expected $expectedVal, got Error: ${actualVal.error}",
expectedVal is Throwable && expectedVal.message == actualVal.error.message
&& expectedVal.cause?.message == actualVal.error.cause?.message
((expectedVal as? Throwable) ?: (expectedVal as? ResultValue.Error)?.error).let {
it != null && it.message == actualVal.error.message
&& it.cause?.message == actualVal.error.cause?.message
}
)
is ResultValue.NotEvaluated -> Assert.assertEquals(
"#$index: Expected $expectedVal, got NotEvaluated",
@@ -298,7 +298,7 @@ class ScriptingHostTest : TestCase() {
"println(\"$greeting\")".toScriptSource(),
{},
{
scriptExecutionWrapper {
scriptExecutionWrapper<Any?> {
val outStream = ByteArrayOutputStream()
val prevOut = System.out
System.setOut(PrintStream(outStream))
@@ -57,8 +57,13 @@ class BasicJvmReplEvaluator(val scriptEvaluator: ScriptEvaluator = BasicJvmScrip
}
KJvmEvaluatedSnippet(snippetVal, currentConfiguration, retVal)
}
else ->
KJvmEvaluatedSnippet(snippetVal, currentConfiguration, ResultValue.NotEvaluated)
else -> {
val firstError = evalRes.reports.find { it.isError() }
KJvmEvaluatedSnippet(
snippetVal, currentConfiguration,
firstError?.exception?.let { ResultValue.Error(it) } ?: ResultValue.NotEvaluated
)
}
}
val newNode = lastEvaluatedSnippet.add(newEvalRes)
@@ -5,6 +5,7 @@
package kotlin.script.experimental.jvm.util
import java.io.ByteArrayOutputStream
import java.io.PrintStream
import kotlin.script.experimental.api.ResultValue
@@ -42,3 +43,11 @@ fun ResultValue.Error.renderError(stream: PrintStream) {
}
}
}
fun ResultValue.Error.renderError(): String =
ByteArrayOutputStream().use { os ->
val ps = PrintStream(os)
renderError(ps)
ps.flush()
os.toString()
}
@@ -10,7 +10,6 @@ import org.jetbrains.kotlin.cli.common.ExitCode
import org.jetbrains.kotlin.cli.common.arguments.CommonCompilerArguments
import org.jetbrains.kotlin.cli.common.extensions.ShellExtension
import org.jetbrains.kotlin.config.CompilerConfiguration
import org.jetbrains.kotlin.config.JVMConfigurationKeys
import org.jetbrains.kotlin.scripting.compiler.plugin.repl.ReplFromTerminal
class JvmCliReplShellExtension : ShellExtension {
@@ -22,8 +21,7 @@ class JvmCliReplShellExtension : ShellExtension {
configuration: CompilerConfiguration,
projectEnvironment: JavaCoreProjectEnvironment
): ExitCode {
configuration.put(JVMConfigurationKeys.IR, false)
ReplFromTerminal.run(projectEnvironment.parentDisposable, configuration)
ReplFromTerminal.run(projectEnvironment, configuration)
return ExitCode.OK
}
}
@@ -81,13 +81,13 @@ open class GenericReplCompiler(
val analysisResult = compilerState.analyzerEngine.analyzeReplLine(psiFile, codeLine)
AnalyzerWithCompilerReport.reportDiagnostics(analysisResult.diagnostics, errorHolder, renderDiagnosticName = false)
val scriptDescriptor = when (analysisResult) {
is ReplCodeAnalyzerBase.ReplLineAnalysisResult.WithErrors -> {
return ReplCompileResult.Error(errorHolder.renderMessage())
}
is ReplCodeAnalyzerBase.ReplLineAnalysisResult.Successful -> {
(analysisResult.scriptDescriptor as? ScriptDescriptor)
?: error("Unexpected script descriptor type ${analysisResult.scriptDescriptor::class}")
}
is ReplCodeAnalyzerBase.ReplLineAnalysisResult.WithErrors -> {
return ReplCompileResult.Error(errorHolder.renderMessage())
}
else -> error("Unexpected result ${analysisResult::class.java}")
}
@@ -5,7 +5,7 @@
package org.jetbrains.kotlin.scripting.compiler.plugin.repl
import com.intellij.openapi.Disposable
import com.intellij.core.JavaCoreProjectEnvironment
import com.intellij.openapi.util.io.FileUtil
import org.jetbrains.kotlin.cli.common.CLIConfigurationKeys
import org.jetbrains.kotlin.cli.common.messages.GroupingMessageCollector
@@ -24,12 +24,12 @@ import java.util.concurrent.Executors
import java.util.concurrent.Future
class ReplFromTerminal(
disposable: Disposable,
projectEnvironment: JavaCoreProjectEnvironment,
compilerConfiguration: CompilerConfiguration,
private val replConfiguration: ReplConfiguration
) {
private val replInitializer: Future<ReplInterpreter> = Executors.newSingleThreadExecutor().submit(Callable {
ReplInterpreter(disposable, compilerConfiguration, replConfiguration)
ReplInterpreter(projectEnvironment, compilerConfiguration, replConfiguration)
})
private val replInterpreter: ReplInterpreter
@@ -109,7 +109,7 @@ class ReplFromTerminal(
}
}
is ReplEvalResult.Error.Runtime -> writer.outputRuntimeError(evalResult.message)
is ReplEvalResult.Error.CompileTime -> writer.outputRuntimeError(evalResult.message)
is ReplEvalResult.Error.CompileTime -> writer.outputCompileError(evalResult.message)
is ReplEvalResult.Incomplete -> writer.notifyIncomplete()
is ReplEvalResult.HistoryMismatch -> {} // assuming handled elsewhere
}
@@ -153,11 +153,11 @@ class ReplFromTerminal(
return listOf(*command.split(" ".toRegex()).dropLastWhile(String::isEmpty).toTypedArray())
}
fun run(disposable: Disposable, configuration: CompilerConfiguration) {
fun run(projectEnvironment: JavaCoreProjectEnvironment, configuration: CompilerConfiguration) {
val replIdeMode = System.getProperty("kotlin.repl.ideMode") == "true"
val replConfiguration = if (replIdeMode) IdeReplConfiguration() else ConsoleReplConfiguration()
return try {
ReplFromTerminal(disposable, configuration, replConfiguration).doRun()
ReplFromTerminal(projectEnvironment, configuration, replConfiguration).doRun()
} catch (e: Exception) {
replConfiguration.exceptionReporter.report(e)
throw e
@@ -5,29 +5,98 @@
package org.jetbrains.kotlin.scripting.compiler.plugin.repl
import com.intellij.openapi.Disposable
import com.intellij.core.JavaCoreProjectEnvironment
import org.jetbrains.kotlin.cli.common.CLIConfigurationKeys
import org.jetbrains.kotlin.cli.common.messages.CompilerMessageSourceLocation
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.common.messages.*
import org.jetbrains.kotlin.cli.common.repl.ReplClassLoader
import org.jetbrains.kotlin.cli.common.repl.ReplEvalResult
import org.jetbrains.kotlin.cli.jvm.compiler.EnvironmentConfigFiles
import org.jetbrains.kotlin.cli.jvm.compiler.KotlinCoreEnvironment
import org.jetbrains.kotlin.cli.jvm.compiler.messageCollector
import org.jetbrains.kotlin.cli.jvm.config.JvmClasspathRoot
import org.jetbrains.kotlin.cli.jvm.config.JvmModulePathRoot
import org.jetbrains.kotlin.config.CompilerConfiguration
import org.jetbrains.kotlin.scripting.compiler.plugin.impl.KJvmReplCompilerBase
import org.jetbrains.kotlin.scripting.compiler.plugin.impl.ReplCompilationState
import org.jetbrains.kotlin.scripting.compiler.plugin.impl.ScriptDiagnosticsMessageCollector
import org.jetbrains.kotlin.scripting.compiler.plugin.impl.createCompilationContextFromEnvironment
import org.jetbrains.kotlin.scripting.compiler.plugin.repl.configuration.ReplConfiguration
import org.jetbrains.kotlin.scripting.definitions.KotlinScriptDefinition
import org.jetbrains.kotlin.scripting.definitions.ScriptDefinition
import org.jetbrains.kotlin.scripting.definitions.ScriptDefinitionProvider
import java.io.PrintWriter
import java.net.URLClassLoader
import java.util.concurrent.atomic.AtomicInteger
import kotlin.script.experimental.api.*
import kotlin.script.experimental.host.ScriptingHostConfiguration
import kotlin.script.experimental.host.toScriptSource
import kotlin.script.experimental.host.withDefaultsFrom
import kotlin.script.experimental.impl.internalScriptingRunSuspend
import kotlin.script.experimental.jvm.BasicJvmReplEvaluator
import kotlin.script.experimental.jvm.defaultJvmScriptingHostConfiguration
import kotlin.script.experimental.jvm.util.renderError
class ReplInterpreter(
disposable: Disposable,
projectEnvironment: JavaCoreProjectEnvironment,
private val configuration: CompilerConfiguration,
private val replConfiguration: ReplConfiguration
) {
private val hostConfiguration: ScriptingHostConfiguration
private val compilationConfiguration: ScriptCompilationConfiguration
private val evaluationConfiguration: ScriptEvaluationConfiguration
private val replState: JvmReplCompilerState<*>
init {
val scriptDefinition: ScriptDefinition? =
ScriptDefinitionProvider.getInstance(projectEnvironment.project)?.getDefaultDefinition()
hostConfiguration = scriptDefinition?.hostConfiguration.withDefaultsFrom(defaultJvmScriptingHostConfiguration)
val environment = (projectEnvironment as? KotlinCoreEnvironment.ProjectEnvironment)?.let {
KotlinCoreEnvironment.createForProduction(it, configuration, EnvironmentConfigFiles.JVM_CONFIG_FILES)
}
?: KotlinCoreEnvironment.createForProduction(
projectEnvironment.parentDisposable, configuration, EnvironmentConfigFiles.JVM_CONFIG_FILES
)
val context =
createCompilationContextFromEnvironment(
scriptDefinition?.compilationConfiguration.with {
hostConfiguration(this@ReplInterpreter.hostConfiguration)
},
environment,
ScriptDiagnosticsMessageCollector(environment.messageCollector)
)
compilationConfiguration = context.baseScriptCompilationConfiguration
evaluationConfiguration = scriptDefinition?.evaluationConfiguration.with {
hostConfiguration(this@ReplInterpreter.hostConfiguration)
scriptExecutionWrapper<Any> { replConfiguration.executionInterceptor.execute(it) }
constructorArgs(emptyArray<String>())
}
replState = JvmReplCompilerState(
{
ReplCompilationState(
context,
analyzerInit = { context1, resolutionFilter ->
ReplCodeAnalyzerBase(context1.environment, implicitsResolutionFilter = resolutionFilter)
},
implicitsResolutionFilter = ReplImplicitsExtensionsResolutionFilter()
)
}
)
}
private val compiler = KJvmReplCompilerBase<ReplCodeAnalyzerBase>(hostConfiguration, replState)
private val evaluator = BasicJvmReplEvaluator()
private val lineNumber = AtomicInteger()
private fun nextSnippet(code: String) =
code.toScriptSource(
"Line_${lineNumber.getAndIncrement()}.${compilationConfiguration[ScriptCompilationConfiguration.fileExtension]}"
)
private val previousIncompleteLines = arrayListOf<String>()
private val classpathRoots = configuration.getList(CLIConfigurationKeys.CONTENT_ROOTS).mapNotNull { root ->
@@ -75,33 +144,65 @@ class ReplInterpreter(
override fun hasErrors(): Boolean = hasErrors
}
// TODO: add script definition with project-based resolving for IDEA repl
private val scriptCompiler: ReplCompilerWithoutCheck by lazy {
GenericReplCompiler(
disposable,
REPL_LINE_AS_SCRIPT_DEFINITION,
configuration,
messageCollector
)
}
private val scriptEvaluator: ReplFullEvaluator by lazy {
GenericReplCompilingEvaluator(scriptCompiler, classpathRoots, classLoader, null, ReplRepeatingMode.REPEAT_ANY_PREVIOUS)
}
private val evalState by lazy { scriptEvaluator.createState() }
fun eval(line: String): ReplEvalResult {
val fullText = (previousIncompleteLines + line).joinToString(separator = "\n")
try {
val evalRes = scriptEvaluator.compileAndEval(
evalState,
ReplCodeLine(lineNumber.getAndIncrement(), 0, fullText),
null,
object : InvokeWrapper {
override fun <T> invoke(body: () -> T): T = replConfiguration.executionInterceptor.execute(body)
val snippet = nextSnippet(fullText)
fun SourceCode.Location.toCompilerMessageLocation() =
CompilerMessageLocation.create(
snippet.name,
start.line, start.col,
snippet.text.lines().getOrNull(start.line - 1)
)
fun ResultWithDiagnostics<*>.reportToMessageCollector() {
for (it in reports) {
val diagnosticSeverity = when (it.severity) {
ScriptDiagnostic.Severity.ERROR -> CompilerMessageSeverity.ERROR
ScriptDiagnostic.Severity.FATAL -> CompilerMessageSeverity.EXCEPTION
ScriptDiagnostic.Severity.WARNING -> CompilerMessageSeverity.WARNING
else -> continue
}
messageCollector.report(diagnosticSeverity, it.message, it.location?.toCompilerMessageLocation())
}
)
}
@Suppress("DEPRECATION_ERROR")
val evalRes: ReplEvalResult = internalScriptingRunSuspend {
when (val compileResult = compiler.compile(listOf(snippet), compilationConfiguration)) {
is ResultWithDiagnostics.Failure -> {
val incompleteReport = compileResult.reports.find { it.code == ScriptDiagnostic.incompleteCode }
val firstError = compileResult.reports.find { it.isError() }
if (incompleteReport != null)
ReplEvalResult.Incomplete(incompleteReport.message)
else {
compileResult.reportToMessageCollector()
ReplEvalResult.Error.CompileTime(firstError?.message ?: "", firstError?.location?.toCompilerMessageLocation())
}
}
is ResultWithDiagnostics.Success -> {
compileResult.reportToMessageCollector()
val evalResult = evaluator.eval(compileResult.value, evaluationConfiguration)
when (evalResult) {
is ResultWithDiagnostics.Success -> {
when (val evalValue = evalResult.value.get().result) {
is ResultValue.Unit -> ReplEvalResult.UnitResult()
is ResultValue.Value -> ReplEvalResult.ValueResult(evalValue.name, evalValue.value, evalValue.type)
is ResultValue.Error -> ReplEvalResult.Error.Runtime(evalValue.renderError())
else -> ReplEvalResult.Error.Runtime("Error: snippet is not evaluated")
}
}
else -> {
val firstError = evalResult.reports.find { it.isError() }
evalResult.reportToMessageCollector()
ReplEvalResult.Error.Runtime(firstError?.message ?: "", firstError?.exception)
}
}
}
}
}
when {
evalRes !is ReplEvalResult.Incomplete -> previousIncompleteLines.clear()
@@ -120,10 +221,4 @@ class ReplInterpreter(
fun dumpClasses(out: PrintWriter) {
classLoader.dumpClasses(out)
}
companion object {
private val REPL_LINE_AS_SCRIPT_DEFINITION = object : KotlinScriptDefinition(Any::class) {
override val name = "Kotlin REPL"
}
}
}