[JS IR] Detect broken cross-module references

The patch adds an error if the module can not find the cross-module reference.

 The patch removes the DCE optimization which eliminates implement() intrinsic,
 because it leads to a broken cross-module reference and
 broken JS code with implement() call, albeit in an unreachable block.
This commit is contained in:
Alexander Korepanov
2023-01-04 17:06:26 +01:00
committed by Space Team
parent e6efde76dc
commit 155777e3fa
10 changed files with 219 additions and 25 deletions
@@ -27,13 +27,6 @@ internal class JsUsefulDeclarationProcessor(
private val hashCodeMethod = getMethodOfAny("hashCode")
override val bodyVisitor: BodyVisitorBase = object : BodyVisitorBase() {
override fun visitFunctionAccess(expression: IrFunctionAccessExpression, data: IrDeclaration) {
if (expression.symbol != context.intrinsics.implementSymbol) {
// Just ignore implement to not include large chunk of code inside small applications if it's not needed
super.visitFunctionAccess(expression, data)
}
}
override fun visitCall(expression: IrCall, data: IrDeclaration) {
super.visitCall(expression, data)
when (expression.symbol) {
@@ -259,4 +252,4 @@ private fun Collection<IrClass>.filterDescendantsOf(bases: Collection<IrClass>):
}
return this.filter { overridesAnyBase(it) }
}
}
@@ -20,7 +20,7 @@ class JsMultiModuleCache(private val moduleArtifacts: List<ModuleArtifact>) {
}
private enum class NameType(val typeMask: Int) {
DEFINITIONS(0b01), NAME_BINDINGS(0b10)
DEFINITIONS(0b1), NAME_BINDINGS(0b10), OPTIONAL_IMPORTS(0b100)
}
class CachedModuleInfo(val artifact: ModuleArtifact, val jsIrHeader: JsIrModuleHeader, var crossModuleReferencesHash: ICHash = ICHash())
@@ -30,6 +30,7 @@ class JsMultiModuleCache(private val moduleArtifacts: List<ModuleArtifact>) {
private fun ModuleArtifact.fetchModuleInfo() = File(artifactsDir, JS_MODULE_HEADER).useCodedInputIfExists {
val definitions = mutableSetOf<String>()
val nameBindings = mutableMapOf<String, String>()
val optionalCrossModuleImports = hashSetOf<String>()
val crossModuleReferencesHash = ICHash.fromProtoStream(this)
val hasJsExports = readBool()
@@ -39,13 +40,24 @@ class JsMultiModuleCache(private val moduleArtifacts: List<ModuleArtifact>) {
if (mask and NameType.DEFINITIONS.typeMask != 0) {
definitions += tag
}
if (mask and NameType.OPTIONAL_IMPORTS.typeMask != 0) {
optionalCrossModuleImports += tag
}
if (mask and NameType.NAME_BINDINGS.typeMask != 0) {
nameBindings[tag] = readString()
}
}
CachedModuleInfo(
artifact = this@fetchModuleInfo,
jsIrHeader = JsIrModuleHeader(moduleSafeName, moduleExternalName, definitions, nameBindings, hasJsExports, null),
jsIrHeader = JsIrModuleHeader(
moduleName = moduleSafeName,
externalModuleName = moduleExternalName,
definitions = definitions,
nameBindings = nameBindings,
optionalCrossModuleImports = optionalCrossModuleImports,
hasJsExports = hasJsExports,
associatedModule = null
),
crossModuleReferencesHash = crossModuleReferencesHash
)
}
@@ -56,6 +68,10 @@ class JsMultiModuleCache(private val moduleArtifacts: List<ModuleArtifact>) {
for ((tag, name) in jsIrHeader.nameBindings) {
names[tag] = NameType.NAME_BINDINGS.typeMask to name
}
for (tag in jsIrHeader.optionalCrossModuleImports) {
val maskAndName = names[tag]
names[tag] = ((maskAndName?.first ?: 0) or NameType.OPTIONAL_IMPORTS.typeMask) to maskAndName?.second
}
for (tag in jsIrHeader.definitions) {
val maskAndName = names[tag]
names[tag] = ((maskAndName?.first ?: 0) or NameType.DEFINITIONS.typeMask) to maskAndName?.second
@@ -5,11 +5,13 @@
package org.jetbrains.kotlin.ir.backend.js.transformers.irToJs
import org.jetbrains.kotlin.backend.common.serialization.checkIsFunctionInterface
import org.jetbrains.kotlin.config.CommonConfigurationKeys
import org.jetbrains.kotlin.ir.backend.js.*
import org.jetbrains.kotlin.ir.backend.js.dce.eliminateDeadDeclarations
import org.jetbrains.kotlin.ir.backend.js.export.*
import org.jetbrains.kotlin.ir.backend.js.lower.StaticMembersLowering
import org.jetbrains.kotlin.ir.backend.js.lower.isBuiltInClass
import org.jetbrains.kotlin.ir.backend.js.utils.*
import org.jetbrains.kotlin.ir.declarations.*
import org.jetbrains.kotlin.ir.util.isInterface
@@ -321,12 +323,16 @@ class IrModuleToJsTransformer(
nameGenerator.nameMap.entries.forEach { (declaration, name) ->
computeTag(declaration)?.let { tag ->
result.nameBindings[tag] = name
if (isBuiltInClass(declaration) || checkIsFunctionInterface(declaration.symbol.signature)) {
result.optionalCrossModuleImports += tag
}
}
}
nameGenerator.imports.entries.forEach { (declaration, importExpression) ->
val tag = computeTag(declaration) ?: error("No tag for imported declaration ${declaration.render()}")
result.imports[tag] = importExpression
result.optionalCrossModuleImports += tag
}
fileExports.file.declarations.forEach {
@@ -13,6 +13,7 @@ import org.jetbrains.kotlin.serialization.js.ModuleKind
class JsIrProgramFragment(val packageFqn: String) {
val nameBindings = mutableMapOf<String, JsName>()
val optionalCrossModuleImports = hashSetOf<String>()
val declarations = JsCompositeBlock()
val exports = JsCompositeBlock()
val importedModules = mutableListOf<JsImportedModule>()
@@ -35,6 +36,7 @@ class JsIrModule(
fun makeModuleHeader(): JsIrModuleHeader {
val nameBindings = mutableMapOf<String, String>()
val definitions = mutableSetOf<String>()
val optionalCrossModuleImports = hashSetOf<String>()
var hasJsExports = false
for (fragment in fragments) {
hasJsExports = hasJsExports || !fragment.exports.isEmpty
@@ -42,8 +44,17 @@ class JsIrModule(
nameBindings[tag] = name.toString()
}
definitions += fragment.definitions
optionalCrossModuleImports += fragment.optionalCrossModuleImports
}
return JsIrModuleHeader(moduleName, externalModuleName, definitions, nameBindings, hasJsExports, this)
return JsIrModuleHeader(
moduleName = moduleName,
externalModuleName = externalModuleName,
definitions = definitions,
nameBindings = nameBindings,
optionalCrossModuleImports = optionalCrossModuleImports,
hasJsExports = hasJsExports,
associatedModule = this
)
}
}
@@ -52,6 +63,7 @@ class JsIrModuleHeader(
val externalModuleName: String,
val definitions: Set<String>,
val nameBindings: Map<String, String>,
val optionalCrossModuleImports: Set<String>,
val hasJsExports: Boolean,
var associatedModule: JsIrModule?
) {
@@ -100,7 +112,13 @@ class CrossModuleDependenciesResolver(
for (header in headers) {
val builder = headerToBuilder[header]!!
for (tag in header.externalNames) {
val fromModuleBuilder = definitionModule[tag] ?: continue // TODO error?
val fromModuleBuilder = definitionModule[tag]
if (fromModuleBuilder == null) {
if (tag in header.optionalCrossModuleImports) {
continue
}
error("Internal error: cannot find external signature '$tag' for module ${header.moduleName}")
}
builder.imports += CrossModuleRef(fromModuleBuilder, tag)
fromModuleBuilder.exports += tag
@@ -111,8 +129,6 @@ class CrossModuleDependenciesResolver(
}
}
private fun String.prettyTag() = takeWhile { c -> c != '|' }
private class CrossModuleRef(val module: JsIrModuleCrossModuleReferecenceBuilder, val tag: String)
private class JsIrModuleCrossModuleReferecenceBuilder(
@@ -153,7 +169,7 @@ private class JsIrModuleCrossModuleReferecenceBuilder(
val tag = crossModuleRef.tag
require(crossModuleRef.module::exportNames.isInitialized) {
// This situation appears in case of a dependent module redefine a symbol (function) from their dependency
"Cross module dependency resolution failed due to symbol '${tag.prettyTag()}' redefinition"
"Cross module dependency resolution failed due to signature '$tag' redefinition"
}
val exportedAs = crossModuleRef.module.exportNames[tag]!!
val moduleName = import(crossModuleRef.module.header)
@@ -207,7 +223,7 @@ class CrossModuleReferences(
fun initJsImportsForModule(module: JsIrModule) {
val tagToName = module.fragments.flatMap { it.nameBindings.entries }.associate { it.key to it.value }
jsImports = imports.entries.associate {
val importedAs = tagToName[it.key] ?: error("Internal error: cannot find imported name for symbol ${it.key.prettyTag()}")
val importedAs = tagToName[it.key] ?: error("Internal error: cannot find imported name for signature ${it.key}")
val exportRef = JsNameRef(
it.value.exportedAs,
it.value.moduleExporter.let {
@@ -68,6 +68,8 @@ class JsIrAstDeserializer : JsAstDeserializerBase() {
deserializeString(nameBindingProto.signatureId) to deserializeName(nameBindingProto.nameId)
}
proto.optionalCrossModuleImportsList.mapTo(fragment.optionalCrossModuleImports) { deserializeString(it) }
proto.irClassModelList.associateTo(fragment.classes) { clsProto -> deserialize(clsProto) }
if (proto.hasTestsInvocation()) {
@@ -70,6 +70,10 @@ class JsIrAstSerializer: JsAstSerializerBase() {
fragmentBuilder.addNameBinding(nameBindingBuilder)
}
fragment.optionalCrossModuleImports.forEach {
fragmentBuilder.addOptionalCrossModuleImports(serialize(it))
}
fragment.classes.entries.forEach { (name, model) -> fragmentBuilder.addIrClassModel(serialize(name, model)) }
fragment.testFunInvocation?.let {
@@ -5,8 +5,17 @@
package org.jetbrains.kotlin.backend.common.serialization
import org.jetbrains.kotlin.ir.util.IdSignature
import java.util.regex.Pattern
internal val functionPattern = Pattern.compile("^K?(Suspend)?Function\\d+$")
internal val functionalPackages = listOf("kotlin", "kotlin.coroutines", "kotlin.reflect")
fun checkIsFunctionInterface(idSig: IdSignature?): Boolean {
val publicSig = idSig?.asPublic()
return publicSig != null &&
publicSig.packageFqName in functionalPackages &&
publicSig.declarationFqName.isNotEmpty() &&
functionPattern.matcher(publicSig.firstNameSegment).find()
}
@@ -124,14 +124,6 @@ class IrModuleDeserializerWithBuiltIns(
symbol.signature to symbol
}.toMap()
private fun checkIsFunctionInterface(idSig: IdSignature): Boolean {
val publicSig = idSig.asPublic()
return publicSig != null &&
publicSig.packageFqName in functionalPackages &&
publicSig.declarationFqName.isNotEmpty() &&
functionPattern.matcher(publicSig.firstNameSegment).find()
}
override operator fun contains(idSig: IdSignature): Boolean {
val topLevel = idSig.topLevelSignature()
if (topLevel in irBuiltInsMap) return true