[JS] Support evaluation of const intrinsics for K2

#KT-56023 Fixed
#KT-51582 Fixed
This commit is contained in:
Ivan Kylchik
2023-02-08 14:40:58 +01:00
committed by Space Team
parent e981b1f958
commit 7cf9dda8dc
36 changed files with 413 additions and 40 deletions
@@ -20,10 +20,10 @@ import org.jetbrains.kotlin.ir.interpreter.checker.IrConstTransformer
class ConstEvaluationLowering(
val context: CommonBackendContext,
private val suppressErrors: Boolean = context.configuration.getBoolean(CommonConfigurationKeys.IGNORE_CONST_OPTIMIZATION_ERRORS),
configuration: IrInterpreterConfiguration = IrInterpreterConfiguration(printOnlyExceptionMessage = true),
private val onWarning: (IrFile, IrElement, IrErrorExpression) -> Unit = { _, _, _ -> },
private val onError: (IrFile, IrElement, IrErrorExpression) -> Unit = { _, _, _ -> },
) : FileLoweringPass {
val configuration = IrInterpreterConfiguration(printOnlyExceptionMessage = true)
val interpreter = IrInterpreter(IrInterpreterEnvironment(context.irBuiltIns, configuration), emptyMap())
override fun lower(irFile: IrFile) {
@@ -151,13 +151,21 @@ internal class DefaultCallInterceptor(override val interpreter: IrInterpreter) :
}
val receiverType = irFunction.dispatchReceiverParameter?.type ?: irFunction.extensionReceiverParameter?.type
val argsType = listOfNotNull(receiverType) + irFunction.valueParameters.map { it.type }
val argsType = (listOfNotNull(receiverType) + irFunction.valueParameters.map { it.type }).map {
// TODO: for consistency with current K/JS implementation Float constant should be treated as a Double (KT-35422)
if (environment.configuration.treatFloatInSpecialWay && it.isFloat()) irBuiltIns.doubleType else it
}
val argsValues = args.wrap(this, irFunction)
// TODO replace unary, binary, ternary functions with vararg
withExceptionHandler(environment) {
val result = when (argsType.size) {
1 -> interpretUnaryFunction(methodName, argsType[0].getOnlyName(), argsValues[0])
1 -> when {
methodName == "toString" && environment.configuration.treatFloatInSpecialWay && (argsValues[0] is Double || argsValues[0] is Float) -> {
argsValues[0].specialToStringForJs()
}
else -> interpretUnaryFunction(methodName, argsType[0].getOnlyName(), argsValues[0])
}
2 -> when (methodName) {
"rangeTo" -> return calculateRangeTo(irFunction.returnType, args)
else -> interpretBinaryFunction(
@@ -8,8 +8,10 @@ package org.jetbrains.kotlin.ir.interpreter
import org.jetbrains.kotlin.descriptors.DescriptorVisibilities
import org.jetbrains.kotlin.ir.IrElement
import org.jetbrains.kotlin.ir.IrStatement
import org.jetbrains.kotlin.ir.UNDEFINED_OFFSET
import org.jetbrains.kotlin.ir.declarations.*
import org.jetbrains.kotlin.ir.expressions.*
import org.jetbrains.kotlin.ir.expressions.impl.IrCallImpl
import org.jetbrains.kotlin.ir.interpreter.exceptions.InterpreterError
import org.jetbrains.kotlin.ir.interpreter.exceptions.handleUserException
import org.jetbrains.kotlin.ir.interpreter.exceptions.verify
@@ -112,6 +114,19 @@ private fun unfoldConstructor(constructor: IrConstructor, callStack: CallStack)
private fun unfoldValueParameters(expression: IrFunctionAccessExpression, environment: IrInterpreterEnvironment) {
val callStack = environment.callStack
val irFunction = expression.symbol.owner
if (irFunction.name.asString() == "<get-name>" && expression.dispatchReceiver is IrGetEnumValue) {
// this optimization allow us to avoid creation of enum object when we try to interpret simple `name` call; see KT-53480
callStack.pushSimpleInstruction(expression)
callStack.pushSimpleInstruction(irFunction.dispatchReceiverParameter!!)
val enumEntry = (expression.dispatchReceiver as IrGetEnumValue).symbol.owner
callStack.pushState(enumEntry.toState(environment.irBuiltIns))
return
}
// TODO do the same thing but for "KCallable.name" with "this" as receiver
val hasDefaults = (0 until expression.valueArgumentsCount).any { expression.getValueArgument(it) == null }
if (hasDefaults) {
environment.getCachedFunction(expression.symbol, fromDelegatingCall = expression is IrDelegatingConstructorCall)?.let {
@@ -166,7 +181,6 @@ private fun unfoldValueParameters(expression: IrFunctionAccessExpression, enviro
).owner.createCall().apply { environment.irBuiltIns.copyArgs(expression, this) }
callStack.pushCompoundInstruction(callToDefault)
} else {
val irFunction = expression.symbol.owner
callStack.pushSimpleInstruction(expression)
fun IrValueParameter.schedule(arg: IrExpression?) {
@@ -277,14 +291,6 @@ private fun unfoldGetEnumValue(expression: IrGetEnumValue, environment: IrInterp
val callStack = environment.callStack
environment.mapOfEnums[expression.symbol]?.let { return callStack.pushState(it) }
val frameOwner = callStack.currentFrameOwner
if (frameOwner is IrCall && frameOwner.dispatchReceiver is IrGetEnumValue && frameOwner.symbol.owner.name.asString() == "<get-name>") {
// this optimization allow us to avoid creation of enum object when we try to interpret simple `name` call; see KT-53480
val enumEntry = (frameOwner.dispatchReceiver as IrGetEnumValue).symbol.owner
callStack.pushState(enumEntry.toState(environment.irBuiltIns))
return
}
callStack.pushSimpleInstruction(expression)
val enumEntry = expression.symbol.owner
val enumClass = enumEntry.symbol.owner.parentAsClass
@@ -389,6 +395,13 @@ private fun unfoldStringConcatenation(expression: IrStringConcatenation, environ
// this callback is used to check the need for an explicit toString call
val explicitToStringCheck = fun() {
when (val state = callStack.peekState()) {
is Primitive<*> -> {
// This block is not really needed, but this way it is easier to handle `toString` with `treatFloatInSpecialWay` enabled.
callStack.popState()
val toStringCall = IrCallImpl.fromSymbolOwner(UNDEFINED_OFFSET, UNDEFINED_OFFSET, environment.irBuiltIns.memberToString)
callStack.pushSimpleInstruction(toStringCall)
callStack.pushState(state)
}
is Common -> {
callStack.popState()
// TODO this check can be dropped after serialization introduction
@@ -293,6 +293,7 @@ class IrInterpreter(internal val environment: IrInterpreterEnvironment, internal
return callStack.pushCompoundInstruction(constructorCall)
}
callStack.pushState(expression.toPrimitive())
}
@@ -11,12 +11,14 @@ package org.jetbrains.kotlin.ir.interpreter
* @param createNonCompileTimeObjects
* 'true' - interpreter will construct object and initialize its properties despite the fact it is not marked as compile time;
* 'false' - interpreter will create a representation of empty object, that can be used to get const properties
* @param treatFloatInSpecialWay interpret float const as if it was a double and avoid `toFloat` evaluation if possible
*/
// TODO maybe create some sort of builder
class IrInterpreterConfiguration(
data class IrInterpreterConfiguration(
val maxStack: Int = 10_000,
val maxCommands: Int = 1_000_000,
val createNonCompileTimeObjects: Boolean = false,
val printOnlyExceptionMessage: Boolean = false,
val collapseStackTraceFromJDK: Boolean = true,
val treatFloatInSpecialWay: Boolean = false,
)
@@ -9,6 +9,7 @@ import org.jetbrains.kotlin.ir.IrBuiltIns
import org.jetbrains.kotlin.ir.declarations.IrClass
import org.jetbrains.kotlin.ir.declarations.IrModuleFragment
import org.jetbrains.kotlin.ir.expressions.IrExpression
import org.jetbrains.kotlin.ir.expressions.impl.IrConstImpl
import org.jetbrains.kotlin.ir.expressions.impl.IrErrorExpressionImpl
import org.jetbrains.kotlin.ir.interpreter.proxy.Proxy
import org.jetbrains.kotlin.ir.interpreter.stack.CallStack
@@ -118,8 +119,9 @@ class IrInterpreterEnvironment(
return when (state) {
is Primitive<*> ->
when {
state.value == null -> state.value.toIrConst(type, start, end)
type.isPrimitiveType() || type.isString() -> state.value.toIrConst(type, start, end)
configuration.treatFloatInSpecialWay && state.value is Float -> IrConstImpl.float(start, end, type, state.value)
configuration.treatFloatInSpecialWay && state.value is Double -> IrConstImpl.double(start, end, type, state.value)
state.value == null || type.isPrimitiveType() || type.isString() -> state.value.toIrConst(type, start, end)
else -> original // TODO support for arrays
}
is ExceptionState -> {
@@ -30,6 +30,7 @@ import org.jetbrains.kotlin.types.Variance
import org.jetbrains.kotlin.util.capitalizeDecapitalize.capitalizeAsciiOnly
import org.jetbrains.kotlin.utils.keysToMap
import java.lang.invoke.MethodType
import kotlin.math.floor
val intrinsicConstEvaluationAnnotation = FqName("kotlin.internal.IntrinsicConstEvaluation")
val compileTimeAnnotation = FqName("kotlin.CompileTimeCalculation")
@@ -131,7 +132,9 @@ fun IrFunctionAccessExpression.getVarargType(index: Int): IrType? {
}
}
internal fun IrFunction.getCapitalizedFileName() = this.file.name.replace(".kt", "Kt").capitalizeAsciiOnly()
internal fun IrFunction.getCapitalizedFileName(): String {
return this.fileOrNull?.name?.replace(".kt", "Kt")?.capitalizeAsciiOnly() ?: "<UNKNOWN>"
}
internal fun IrClass.isSubclassOfThrowable(): Boolean {
return generateSequence(this) { irClass ->
@@ -314,6 +317,14 @@ internal fun State.unsignedToString(): String {
}
}
internal fun Any?.specialToStringForJs(): String {
return when {
this is Float && !this.isInfinite() && floor(this) == this -> this.toInt().toString()
this is Double && !this.isInfinite() && floor(this) == this -> this.toLong().toString()
else -> this.toString()
}
}
internal fun IrEnumEntry.toState(irBuiltIns: IrBuiltIns): Common {
val enumClass = this.symbol.owner.parentAsClass
val enumEntries = enumClass.declarations.filterIsInstance<IrEnumEntry>()
@@ -9,6 +9,7 @@ import org.jetbrains.kotlin.ir.IrElement
import org.jetbrains.kotlin.ir.IrStatement
import org.jetbrains.kotlin.ir.declarations.*
import org.jetbrains.kotlin.ir.expressions.*
import org.jetbrains.kotlin.ir.interpreter.IrInterpreterConfiguration
import org.jetbrains.kotlin.ir.interpreter.accessesTopLevelOrObjectField
import org.jetbrains.kotlin.ir.interpreter.fqName
import org.jetbrains.kotlin.ir.interpreter.isAccessToNotNullableObject
@@ -18,7 +19,9 @@ import org.jetbrains.kotlin.ir.util.statements
import org.jetbrains.kotlin.ir.visitors.IrElementVisitor
class IrCompileTimeChecker(
containingDeclaration: IrElement? = null, private val mode: EvaluationMode = EvaluationMode.WITH_ANNOTATIONS
containingDeclaration: IrElement? = null,
private val mode: EvaluationMode = EvaluationMode.WITH_ANNOTATIONS,
private val interpreterConfiguration: IrInterpreterConfiguration,
) : IrElementVisitor<Boolean, Nothing?> {
private var contextExpression: IrCall? = null
private val visitedStack = mutableListOf<IrElement>().apply { if (containingDeclaration != null) add(containingDeclaration) }
@@ -67,6 +70,13 @@ class IrCompileTimeChecker(
val owner = expression.symbol.owner
if (!mode.canEvaluateFunction(owner, expression)) return false
// We disable `toFloat` folding on K/JS till `toFloat` is fixed (KT-35422)
// This check must be placed here instead of CallInterceptor because we still
// want to evaluate (1) `const val` expressions and (2) values in annotations.
if (owner.name.asString() == "toFloat" && interpreterConfiguration.treatFloatInSpecialWay) {
return super.visitCall(expression, data)
}
return expression.saveContext {
val dispatchReceiverComputable = expression.dispatchReceiver?.accept(this, null) ?: true
val extensionReceiverComputable = expression.extensionReceiver?.accept(this, null) ?: true
@@ -15,6 +15,7 @@ import org.jetbrains.kotlin.ir.expressions.*
import org.jetbrains.kotlin.ir.expressions.impl.IrConstImpl
import org.jetbrains.kotlin.ir.expressions.impl.IrVarargImpl
import org.jetbrains.kotlin.ir.interpreter.IrInterpreter
import org.jetbrains.kotlin.ir.interpreter.IrInterpreterConfiguration
import org.jetbrains.kotlin.ir.interpreter.isPrimitiveArray
import org.jetbrains.kotlin.ir.interpreter.toIrConst
import org.jetbrains.kotlin.ir.types.*
@@ -49,9 +50,12 @@ class IrConstTransformer(
return this
}
private fun IrExpression.canBeInterpreted(containingDeclaration: IrElement? = null): Boolean {
private fun IrExpression.canBeInterpreted(
containingDeclaration: IrElement? = null,
configuration: IrInterpreterConfiguration = interpreter.environment.configuration
): Boolean {
return try {
this.accept(IrCompileTimeChecker(containingDeclaration, mode = mode), null)
this.accept(IrCompileTimeChecker(containingDeclaration, mode, configuration), null)
} catch (e: Throwable) {
if (suppressExceptions) {
return false
@@ -87,7 +91,9 @@ class IrConstTransformer(
val expression = initializer?.expression ?: return declaration
if (expression is IrConst<*>) return declaration
val isConst = declaration.correspondingPropertySymbol?.owner?.isConst == true
if (isConst && expression.canBeInterpreted(declaration)) {
if (!isConst) return super.visitField(declaration)
if (expression.canBeInterpreted(declaration, interpreter.environment.configuration.copy(treatFloatInSpecialWay = false))) {
initializer.expression = expression.interpret(failAsError = true)
}
@@ -129,7 +135,7 @@ class IrConstTransformer(
}
private fun IrExpression.transformSingleArg(expectedType: IrType): IrExpression {
if (this.canBeInterpreted()) {
if (this.canBeInterpreted(configuration = interpreter.environment.configuration.copy(treatFloatInSpecialWay = false))) {
return this.interpret(failAsError = true).convertToConstIfPossible(expectedType)
} else if (this is IrConstructorCall) {
transformAnnotation(this)
@@ -14,6 +14,7 @@ import org.jetbrains.kotlin.ir.interpreter.proxy.reflection.ReflectionProxy.Comp
import org.jetbrains.kotlin.ir.interpreter.state.*
import org.jetbrains.kotlin.ir.interpreter.state.reflection.ReflectionState
import org.jetbrains.kotlin.ir.types.isArray
import org.jetbrains.kotlin.ir.types.isFloat
import java.lang.invoke.MethodType
internal interface Proxy {
@@ -34,6 +35,8 @@ internal fun State.wrap(callInterceptor: CallInterceptor, remainArraysAsIs: Bool
is Primitive<*> -> when {
this.isNull() -> null
this.type.isArray() || this.type.isPrimitiveArray() -> if (remainArraysAsIs) this else this.value
// TODO: for consistency with current K/JS implementation Float constant should be treated as a Double (KT-35422)
this.type.isFloat() && callInterceptor.environment.configuration.treatFloatInSpecialWay -> this.value.toString().toDouble()
else -> this.value
}
is Common -> this.asProxy(callInterceptor, extendFrom)