Skip to content

Commit

Permalink
[JS Plain Objects] Remove dispatchReceiver from JsPlainObject invoke …
Browse files Browse the repository at this point in the history
…operator by marking it with @JsNoDispatchReceiver

^KT-68904 Fixed
^KT-70078 Fixed
^KT-72598 Fixed
  • Loading branch information
JSMonk authored and Space Team committed Jan 10, 2025
1 parent ee6eba5 commit caf8978
Show file tree
Hide file tree
Showing 12 changed files with 148 additions and 8 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,11 @@ private val stripTypeAliasDeclarationsPhase = makeIrModulePhase<JsIrBackendConte
name = "StripTypeAliasDeclarations",
)

private val noDispatchReceiverApplyingPhase = makeIrModulePhase(
{ _: JsIrBackendContext -> NoDispatchReceiverAnnotationApplyingLowering },
name = "NoDispatchReceiverAnnotationApplyingLowering",
)

private val jsCodeOutliningPhase = makeIrModulePhase(
{ context: JsIrBackendContext -> JsCodeOutliningLowering(context, context.intrinsics, context.dynamicType) },
name = "JsCodeOutliningLowering",
Expand Down Expand Up @@ -741,6 +746,7 @@ fun getJsLowerings(
): List<SimpleNamedCompilerPhase<JsIrBackendContext, IrModuleFragment, IrModuleFragment>> = listOfNotNull(
// BEGIN: Common Native/JS/Wasm prefix.
validateIrBeforeLowering,
noDispatchReceiverApplyingPhase,
jsCodeOutliningPhase,
lateinitPhase,
sharedVariablesLoweringPhase,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ class MoveBodilessDeclarationsToSeparatePlaceLowering(private val context: JsIrB
}
}

if (irFile.getJsModule() != null || irFile.getJsQualifier() != null) {
if (declaration.isEffectivelyExternal() && (irFile.getJsModule() != null || irFile.getJsQualifier() != null)) {
externalPackageFragment.declarations += declaration
declaration.parent = externalPackageFragment

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
/*
* Copyright 2010-2024 JetBrains s.r.o. and Kotlin Programming Language contributors.
* Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file.
*/

package org.jetbrains.kotlin.ir.backend.js.lower

import org.jetbrains.kotlin.backend.common.BodyLoweringPass
import org.jetbrains.kotlin.ir.declarations.IrDeclaration
import org.jetbrains.kotlin.ir.declarations.IrParameterKind
import org.jetbrains.kotlin.ir.expressions.IrBody
import org.jetbrains.kotlin.ir.expressions.IrCall
import org.jetbrains.kotlin.ir.expressions.IrExpression
import org.jetbrains.kotlin.ir.util.hasAnnotation
import org.jetbrains.kotlin.ir.visitors.IrElementTransformerVoid
import org.jetbrains.kotlin.ir.visitors.transformChildrenVoid
import org.jetbrains.kotlin.name.JsStandardClassIds

object NoDispatchReceiverAnnotationApplyingLowering : BodyLoweringPass {
override fun lower(irBody: IrBody, container: IrDeclaration) {
irBody.transformChildrenVoid(object : IrElementTransformerVoid() {
override fun visitCall(expression: IrCall): IrExpression {
val callee = expression.symbol.owner
if (callee.hasAnnotation(JsStandardClassIds.Annotations.JsNoDispatchReceiver)) {
expression.removeDispatchReceiver()
callee.parameters = callee.parameters.filter { it.kind != IrParameterKind.DispatchReceiver }
}
return super.visitCall(expression)
}
})
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,9 @@ object JsStandardClassIds {
@JvmField
val JsImplicitExport = "JsImplicitExport".jsId()

@JvmField
val JsNoDispatchReceiver = "JsNoDispatchReceiver".jsId()

@JvmField
val JsStatic = "JsStatic".jsId()

Expand Down
9 changes: 9 additions & 0 deletions libraries/stdlib/js/runtime/kotlinJsHacks.kt
Original file line number Diff line number Diff line change
Expand Up @@ -72,3 +72,12 @@ internal annotation class JsGenerator
*/
@Target(AnnotationTarget.CLASS)
internal annotation class JsImplicitExport(val couldBeConvertedToExplicitExport: Boolean)

/**
* The annotation is needed for annotating function declarations that should not accept any dispatch receiver
* It's used only internally (for now, only in js-plain-object plugin)
*/
@Target(AnnotationTarget.FUNCTION)
@PublishedApi
@Suppress("unused") // used by @JsPlainObject compiler plugin
internal annotation class JsNoDispatchReceiver
Original file line number Diff line number Diff line change
Expand Up @@ -14215,6 +14215,11 @@ open annotation class kotlin.js/JsFileName : kotlin/Annotation { // kotlin.js/Js
final fun <get-name>(): kotlin/String // kotlin.js/JsFileName.name.<get-name>|<get-name>(){}[0]
}

// Targets: [js]
open annotation class kotlin.js/JsNoDispatchReceiver : kotlin/Annotation { // kotlin.js/JsNoDispatchReceiver|null[0]
constructor <init>() // kotlin.js/JsNoDispatchReceiver.<init>|<init>(){}[0]
}

// Targets: [js]
open annotation class kotlin.js/JsNonModule : kotlin/Annotation { // kotlin.js/JsNonModule|null[0]
constructor <init>() // kotlin.js/JsNonModule.<init>|<init>(){}[0]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,9 +30,10 @@ import org.jetbrains.kotlinx.jspo.compiler.resolve.StandardIds
import kotlin.math.abs

private class MoveExternalInlineFunctionsWithBodiesOutsideLowering(private val context: IrPluginContext) : DeclarationTransformer {
private val jsFunction = context.referenceFunctions(StandardIds.JS_FUNCTION_ID).single()
private val EXPECTED_ORIGIN = IrDeclarationOrigin.GeneratedByPlugin(JsPlainObjectsPluginKey)

private val jsFunction = context.referenceFunctions(StandardIds.JS_FUNCTION_ID).single()

override fun transformFlat(declaration: IrDeclaration): List<IrDeclaration>? {
val file = declaration.file
val parent = declaration.parentClassOrNull
Expand Down Expand Up @@ -68,10 +69,15 @@ private class MoveExternalInlineFunctionsWithBodiesOutsideLowering(private val c

copyParametersFrom(declaration, substitutionMap)

extensionReceiverParameter = dispatchReceiverParameter
dispatchReceiverParameter = null

returnType = returnType.substitute(substitutionMap)

if (parent.isCompanion) {
parameters = parameters.filter { it.kind != IrParameterKind.DispatchReceiver }
} else {
dispatchReceiverParameter?.kind = IrParameterKind.ExtensionReceiver
}

body = when (declaration.name) {
StandardNames.DATA_CLASS_COPY -> generateBodyForCopyFunction()
OperatorNameConventions.INVOKE -> generateBodyForFactoryFunction()
Expand All @@ -95,7 +101,7 @@ private class MoveExternalInlineFunctionsWithBodiesOutsideLowering(private val c
proxyFunction.symbol,
proxyFunction.typeParameters.size,
).apply {
declaration.dispatchReceiverParameter?.let {
declaration.dispatchReceiverParameter.takeIf { declaration.parentClassOrNull?.isCompanion != true }?.let {
extensionReceiver = IrGetValueImpl(UNDEFINED_OFFSET, UNDEFINED_OFFSET, it.symbol)
}

Expand Down Expand Up @@ -185,8 +191,9 @@ private class MoveExternalInlineFunctionsWithBodiesOutsideLowering(private val c
jsFunction,
0,
).apply {
val objectAssignCall = "Object.assign({}, ${selfName.identifier}, ${createValueParametersObject(declaration.valueParameters)})"
putValueArgument(0, objectAssignCall.toIrConst(context.irBuiltIns.stringType))
val objectAssignCall =
"Object.assign({}, ${selfName.identifier}, ${createValueParametersObject(declaration.parameters.filter { it.kind == IrParameterKind.Regular })})"
arguments[0] = objectAssignCall.toIrConst(context.irBuiltIns.stringType)
}
)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,9 @@ import org.jetbrains.kotlin.fir.declarations.impl.FirResolvedDeclarationStatusIm
import org.jetbrains.kotlin.fir.declarations.origin
import org.jetbrains.kotlin.fir.declarations.utils.isCompanion
import org.jetbrains.kotlin.fir.expressions.FirExpression
import org.jetbrains.kotlin.fir.expressions.builder.buildAnnotation
import org.jetbrains.kotlin.fir.expressions.builder.buildPropertyAccessExpression
import org.jetbrains.kotlin.fir.expressions.impl.FirEmptyAnnotationArgumentMapping
import org.jetbrains.kotlin.fir.extensions.FirDeclarationGenerationExtension
import org.jetbrains.kotlin.fir.extensions.MemberGenerationContext
import org.jetbrains.kotlin.fir.extensions.NestedClassGenerationContext
Expand All @@ -35,8 +37,10 @@ import org.jetbrains.kotlin.fir.scopes.kotlinScopeProvider
import org.jetbrains.kotlin.fir.symbols.SymbolInternals
import org.jetbrains.kotlin.fir.symbols.impl.*
import org.jetbrains.kotlin.fir.types.*
import org.jetbrains.kotlin.fir.types.builder.buildResolvedTypeRef
import org.jetbrains.kotlin.fir.types.impl.ConeTypeParameterTypeImpl
import org.jetbrains.kotlin.name.CallableId
import org.jetbrains.kotlin.name.JsStandardClassIds
import org.jetbrains.kotlin.name.Name
import org.jetbrains.kotlin.name.SpecialNames
import org.jetbrains.kotlin.util.OperatorNameConventions
Expand Down Expand Up @@ -68,6 +72,7 @@ import org.jetbrains.kotlinx.jspo.compiler.resolve.StandardIds
* Admin.Companion.invoke(chat, name)
*
* companion object {
* @JsNoDispatchReceiver
* inline operator fun invoke(chat: Chat, email: String? = VOID): Admin =
* js("{ chat: chat, name: name }")
* }
Expand Down Expand Up @@ -268,6 +273,17 @@ class JsPlainObjectsFunctionsGenerator(session: FirSession) : FirDeclarationGene
dispatchReceiverType =
if (parent.isCompanion) parent.defaultType() else replacedJsPlainObjectType.coneType as ConeSimpleKotlinType

if (parent.isCompanion) {
annotations += buildAnnotation {
annotationTypeRef = buildResolvedTypeRef {
val annotationClassId = JsStandardClassIds.Annotations.JsNoDispatchReceiver
coneType = annotationClassId.toLookupTag()
.constructClassType(typeArguments = ConeTypeProjection.EMPTY_ARRAY, isMarkedNullable = false)
}
argumentMapping = FirEmptyAnnotationArgumentMapping
}
}

jsPlainObjectProperties.mapTo(valueParameters) {
val typeRef = it.resolvedReturnTypeRef
buildValueParameter {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
// MODULE: lib
// FILE: lib.kt
@file:JsQualifier("foo.bar.baz")
package foo

import kotlinx.js.JsPlainObject

@JsPlainObject
external interface User {
var name: String
val age: Int
}

// MODULE: main(lib)
// FILE: main.kt
import foo.User

fun box(): String {
val user = User(name = "Name", age = 10)

if (user.name != "Name") return "Fail: problem with `name` property"
if (user.age != 10) return "Fail: problem with `age` property"

val json = js("JSON.stringify(user)")
if (json != "{\"age\":10,\"name\":\"Name\"}") return "Fail: got the next json: $json"

return "OK"
}
23 changes: 23 additions & 0 deletions plugins/js-plain-objects/compiler-plugin/testData/box/nested.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package foo

import kotlinx.js.JsPlainObject

external class Parent {
@JsPlainObject
interface User {
var name: String
val age: Int
}
}

fun box(): String {
val user = Parent.User(name = "Name", age = 10)

if (user.name != "Name") return "Fail: problem with `name` property"
if (user.age != 10) return "Fail: problem with `age` property"

val json = js("JSON.stringify(user)")
if (json != "{\"age\":10,\"name\":\"Name\"}") return "Fail: got the next json: $json"

return "OK"
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
// IGNORE_BACKEND: JS_IR
// ISSUE: KT-70078

import kotlinx.js.JsPlainObject
Expand Down

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

0 comments on commit caf8978

Please sign in to comment.