[K/JS] Rework ES modules part with squashed JsImport and right renaming strategy inside import/export statements

This commit is contained in:
Artem Kobzar
2023-03-16 10:28:39 +00:00
committed by Space Team
parent 3c0048bfd8
commit 948c511284
28 changed files with 382 additions and 257 deletions
@@ -90,6 +90,7 @@ public class K2JSCompiler extends CLICompiler<K2JSCompilerArguments> {
moduleKindMap.put(K2JsArgumentConstants.MODULE_COMMONJS, ModuleKind.COMMON_JS);
moduleKindMap.put(K2JsArgumentConstants.MODULE_AMD, ModuleKind.AMD);
moduleKindMap.put(K2JsArgumentConstants.MODULE_UMD, ModuleKind.UMD);
moduleKindMap.put(K2JsArgumentConstants.MODULE_ES, ModuleKind.ES);
sourceMapContentEmbeddingMap.put(K2JsArgumentConstants.SOURCE_MAP_SOURCE_CONTENT_ALWAYS, SourceMapSourceEmbedding.ALWAYS);
sourceMapContentEmbeddingMap.put(K2JsArgumentConstants.SOURCE_MAP_SOURCE_CONTENT_NEVER, SourceMapSourceEmbedding.NEVER);
@@ -44,6 +44,8 @@ interface DelegateFactory {
}
object DefaultDelegateFactory : DelegateFactory {
fun <K : IrDeclaration, V> newDeclarationToValueMapping(): Mapping.Delegate<K, V> = newMappingImpl()
override fun <K : IrDeclaration, V : IrDeclaration> newDeclarationToDeclarationMapping(): Mapping.Delegate<K, V> = newMappingImpl()
override fun <K : IrDeclaration, V : Collection<IrDeclaration>> newDeclarationToDeclarationCollectionMapping(): Mapping.Delegate<K, V> = newMappingImpl()
@@ -11,8 +11,8 @@ import org.jetbrains.kotlin.ir.backend.js.utils.MutableReference
import org.jetbrains.kotlin.ir.declarations.*
class JsMapping : DefaultMapping() {
val esClassWhichNeedBoxParameters = mutableSetOf<IrClass>()
val esClassToPossibilityForOptimization = mutableMapOf<IrClass, MutableReference<Boolean>>()
val esClassWhichNeedBoxParameters = DefaultDelegateFactory.newDeclarationToValueMapping<IrClass, Boolean>()
val esClassToPossibilityForOptimization = DefaultDelegateFactory.newDeclarationToValueMapping<IrClass, MutableReference<Boolean>>()
val outerThisFieldSymbols = DefaultDelegateFactory.newDeclarationToDeclarationMapping<IrClass, IrField>()
val innerClassConstructors = DefaultDelegateFactory.newDeclarationToDeclarationMapping<IrConstructor, IrConstructor>()
@@ -83,7 +83,7 @@ class ExportModelToJsStatements(
namespace != null ->
listOf(jsAssignment(jsElementAccess(declaration.name, namespace), JsNameRef(name)).makeStmt())
esModules -> listOf(JsExport(name, alias = JsName(declaration.name, false)))
esModules -> listOf(JsExport(name.makeRef(), alias = JsName(declaration.name, false)))
else -> emptyList()
}
}
@@ -96,7 +96,7 @@ class ExportModelToJsStatements(
when {
namespace == null -> {
val property = declaration.generateTopLevelGetters()
listOf(JsVars(property), JsExport(property.name, JsName(declaration.name, false)))
listOf(JsVars(property), JsExport(property.name.makeRef(), JsName(declaration.name, false)))
}
es6mode && declaration.isMember -> {
val jsClass = parentClass?.getCorrespondingJsClass() ?: error("Expect to have parentClass at this point")
@@ -168,7 +168,7 @@ class ExportModelToJsStatements(
}
val klassExport = when {
namespace != null -> jsAssignment(newNameSpace, JsNameRef(name)).makeStmt()
esModules -> JsExport(name, alias = JsName(declaration.name, false))
esModules -> JsExport(name.makeRef(), alias = JsName(declaration.name, false))
else -> null
}
@@ -21,6 +21,7 @@ import org.jetbrains.kotlin.js.config.JSConfigurationKeys
import org.jetbrains.kotlin.library.impl.buffer
import org.jetbrains.kotlin.protobuf.CodedInputStream
import org.jetbrains.kotlin.protobuf.CodedOutputStream
import org.jetbrains.kotlin.serialization.js.ModuleKind
import java.security.MessageDigest
internal fun Hash128Bits.toProtoStream(out: CodedOutputStream) {
@@ -164,6 +165,13 @@ internal fun CrossModuleReferences.crossModuleReferencesHashForIC() = HashCalcul
val import = imports[tag]!!
update(tag)
update(import.exportedAs)
update(import.moduleExporter.toString())
if (moduleKind == ModuleKind.ES) {
update(import.moduleExporter.internalName.toString())
update(import.moduleExporter.externalName)
update(import.moduleExporter.relativeRequirePath ?: "")
} else {
update(import.moduleExporter.internalName.toString())
}
}
}.finalize()
@@ -25,7 +25,7 @@ import org.jetbrains.kotlin.ir.visitors.transformChildrenVoid
import org.jetbrains.kotlin.util.collectionUtils.filterIsInstanceAnd
class ES6ConstructorBoxParameterOptimizationLowering(private val context: JsIrBackendContext) : BodyLoweringPass {
private val esClassWhichNeedBoxParameters = context.mapping.esClassWhichNeedBoxParameters
private val IrClass.needsOfBoxParameter by context.mapping.esClassWhichNeedBoxParameters
override fun lower(irBody: IrBody, container: IrDeclaration) {
if (!context.es6mode) return
@@ -85,12 +85,12 @@ class ES6ConstructorBoxParameterOptimizationLowering(private val context: JsIrBa
}
private fun IrClass.requiredToHaveBoxParameter(): Boolean {
return esClassWhichNeedBoxParameters.contains(this)
return needsOfBoxParameter == true
}
}
class ES6CollectConstructorsWhichNeedBoxParameters(private val context: JsIrBackendContext) : DeclarationTransformer {
private val esClassWhichNeedBoxParameters = context.mapping.esClassWhichNeedBoxParameters
private var IrClass.needsOfBoxParameter by context.mapping.esClassWhichNeedBoxParameters
override fun transformFlat(declaration: IrDeclaration): List<IrDeclaration>? {
if (!context.es6mode || declaration !is IrClass) return null
@@ -134,7 +134,7 @@ class ES6CollectConstructorsWhichNeedBoxParameters(private val context: JsIrBack
private fun IrClass.addToClassListWhichNeedBoxParameter() {
if (isExternal) return
esClassWhichNeedBoxParameters.add(this)
needsOfBoxParameter = true
superClass?.addToClassListWhichNeedBoxParameter()
}
}
@@ -177,8 +177,8 @@ class ES6PrimaryConstructorUsageOptimizationLowering(private val context: JsIrBa
* Otherwise, we can generate a simple ES-class constructor in each class of the hierarchy
*/
class ES6CollectPrimaryConstructorsWhichCouldBeOptimizedLowering(private val context: JsIrBackendContext) : DeclarationTransformer {
private val esClassWhichNeedBoxParameters = context.mapping.esClassWhichNeedBoxParameters
private val esClassToPossibilityForOptimization = context.mapping.esClassToPossibilityForOptimization
private val IrClass.needsOfBoxParameter by context.mapping.esClassWhichNeedBoxParameters
private var IrClass.possibilityToOptimizeForEsClass by context.mapping.esClassToPossibilityForOptimization
override fun transformFlat(declaration: IrDeclaration): List<IrDeclaration>? {
if (
@@ -186,7 +186,7 @@ class ES6CollectPrimaryConstructorsWhichCouldBeOptimizedLowering(private val con
declaration is IrClass &&
!declaration.isExternal &&
!context.inlineClassesUtils.isClassInlineLike(declaration) &&
!esClassToPossibilityForOptimization.contains(declaration)
declaration.possibilityToOptimizeForEsClass == null
) {
declaration.checkIfCanBeOptimized()
}
@@ -199,7 +199,7 @@ class ES6CollectPrimaryConstructorsWhichCouldBeOptimizedLowering(private val con
var nearestOptimizationDecision: MutableReference<Boolean>? = null
while (currentClass != null && !currentClass.isExternal) {
val currentClassOptimizationDecision = esClassToPossibilityForOptimization[currentClass]
val currentClassOptimizationDecision = currentClass.possibilityToOptimizeForEsClass
if (currentClassOptimizationDecision != null) {
nearestOptimizationDecision = currentClassOptimizationDecision
@@ -214,8 +214,8 @@ class ES6CollectPrimaryConstructorsWhichCouldBeOptimizedLowering(private val con
}
currentClass = this
while (currentClass != null && !currentClass.isExternal && !esClassToPossibilityForOptimization.contains(currentClass)) {
esClassToPossibilityForOptimization[currentClass] = nearestOptimizationDecision
while (currentClass != null && !currentClass.isExternal && currentClass.possibilityToOptimizeForEsClass == null) {
currentClass.possibilityToOptimizeForEsClass = nearestOptimizationDecision
if (nearestOptimizationDecision.value && !currentClass.canBeOptimized()) {
nearestOptimizationDecision.value = false
@@ -249,7 +249,7 @@ class ES6CollectPrimaryConstructorsWhichCouldBeOptimizedLowering(private val con
}
private fun IrClass.isSubclassOfExternalClassWithRequiredBoxParameter(): Boolean {
return superClass?.isExternal == true && esClassWhichNeedBoxParameters.contains(this)
return superClass?.isExternal == true && needsOfBoxParameter == true
}
}
@@ -7,11 +7,19 @@ package org.jetbrains.kotlin.ir.backend.js.transformers.irToJs
import org.jetbrains.kotlin.ir.backend.js.export.TypeScriptFragment
import org.jetbrains.kotlin.ir.backend.js.export.toTypeScript
import org.jetbrains.kotlin.js.backend.ast.ESM_EXTENSION
import org.jetbrains.kotlin.js.backend.ast.JsProgram
import org.jetbrains.kotlin.js.backend.ast.REGULAR_EXTENSION
import org.jetbrains.kotlin.serialization.js.ModuleKind
import java.io.File
import java.nio.file.Files
val ModuleKind.extension: String
get() = when (this) {
ModuleKind.ES -> ESM_EXTENSION
else -> REGULAR_EXTENSION
}
abstract class CompilationOutputs {
var dependencies: Collection<Pair<String, CompilationOutputs>> = emptyList()
@@ -35,10 +43,10 @@ abstract class CompilationOutputs {
}
dependencies.forEach { (name, content) ->
outputDir.resolve("$name.js").writeAsJsFile(content)
outputDir.resolve("$name${moduleKind.extension}").writeAsJsFile(content)
}
val outputJsFile = outputDir.resolve("$outputName.js")
val outputJsFile = outputDir.resolve("$outputName${moduleKind.extension}")
outputJsFile.writeAsJsFile(this)
if (genDTS) {
@@ -94,12 +94,14 @@ class CrossModuleDependenciesResolver(
private val headers: List<JsIrModuleHeader>
) {
fun resolveCrossModuleDependencies(relativeRequirePath: Boolean): Map<JsIrModuleHeader, CrossModuleReferences> {
val headerToBuilder = headers.associateWith { JsIrModuleCrossModuleReferecenceBuilder(moduleKind, it, relativeRequirePath) }
val definitionModule = mutableMapOf<String, JsIrModuleCrossModuleReferecenceBuilder>()
val headerToBuilder = headers.associateWith { JsIrModuleCrossModuleReferenceBuilder(moduleKind, it, relativeRequirePath) }
val definitionModule = mutableMapOf<String, JsIrModuleCrossModuleReferenceBuilder>()
val mainModuleHeader = headers.last()
val otherModuleHeaders = headers.dropLast(1)
headerToBuilder[mainModuleHeader]!!.transitiveJsExportFrom = otherModuleHeaders
if (moduleKind != ModuleKind.ES) {
val mainModuleHeader = headers.last()
val otherModuleHeaders = headers.dropLast(1)
headerToBuilder[mainModuleHeader]!!.transitiveJsExportFrom = otherModuleHeaders
}
for (header in headers) {
val builder = headerToBuilder[header]!!
@@ -130,9 +132,9 @@ class CrossModuleDependenciesResolver(
}
}
private class CrossModuleRef(val module: JsIrModuleCrossModuleReferecenceBuilder, val tag: String)
private class CrossModuleRef(val module: JsIrModuleCrossModuleReferenceBuilder, val tag: String)
private class JsIrModuleCrossModuleReferecenceBuilder(
private class JsIrModuleCrossModuleReferenceBuilder(
val moduleKind: ModuleKind,
val header: JsIrModuleHeader,
val relativeRequirePath: Boolean
@@ -150,20 +152,15 @@ private class JsIrModuleCrossModuleReferecenceBuilder(
fun buildCrossModuleRefs(): CrossModuleReferences {
buildExportNames()
val isImportOptional = moduleKind == ModuleKind.ES
val importedModules = mutableMapOf<JsIrModuleHeader, JsImportedModule>()
fun import(moduleHeader: JsIrModuleHeader): JsName {
return importedModules.getOrPut(moduleHeader) {
val jsModuleName = JsName(moduleHeader.moduleName, false)
val relativeRequirePath = relativeRequirePath(moduleHeader)
JsImportedModule(
moduleHeader.externalModuleName,
jsModuleName,
null,
relativeRequirePath
)
}.internalName
fun import(moduleHeader: JsIrModuleHeader): JsImportedModule {
return if (isImportOptional) {
moduleHeader.toJsImportedModule()
} else {
importedModules.getOrPut(moduleHeader) { moduleHeader.toJsImportedModule() }
}
}
val resultImports = imports.associate { crossModuleRef ->
@@ -173,13 +170,13 @@ private class JsIrModuleCrossModuleReferecenceBuilder(
"Cross module dependency resolution failed due to signature '$tag' redefinition"
}
val exportedAs = crossModuleRef.module.exportNames[tag]!!
val moduleName = import(crossModuleRef.module.header)
val importedModule = import(crossModuleRef.module.header)
tag to CrossModuleImport(exportedAs, moduleName)
tag to CrossModuleImport(exportedAs, importedModule)
}
val transitiveExport = transitiveJsExportFrom.mapNotNull {
if (!it.hasJsExports) null else CrossModuleTransitiveExport(import(it), it.externalModuleName)
if (!it.hasJsExports) null else CrossModuleTransitiveExport(import(it).internalName, it.externalModuleName)
}
return CrossModuleReferences(
moduleKind,
@@ -190,12 +187,22 @@ private class JsIrModuleCrossModuleReferecenceBuilder(
)
}
private fun JsIrModuleHeader.toJsImportedModule(): JsImportedModule {
val jsModuleName = JsName(moduleName, false)
val relativeRequirePath = relativeRequirePath(this)
return JsImportedModule(
externalModuleName,
jsModuleName,
null,
relativeRequirePath
)
}
private fun relativeRequirePath(moduleHeader: JsIrModuleHeader): String? {
if (!this.relativeRequirePath) return null
val parentMain = File(header.externalModuleName).parentFile
if (parentMain == null) return "./${moduleHeader.externalModuleName}"
val parentMain = File(header.externalModuleName).parentFile ?: return "./${moduleHeader.externalModuleName}"
val relativePath = File(moduleHeader.externalModuleName)
.toRelativeString(parentMain)
@@ -206,10 +213,12 @@ private class JsIrModuleCrossModuleReferecenceBuilder(
}
}
class CrossModuleImport(val exportedAs: String, val moduleExporter: JsName)
class CrossModuleImport(val exportedAs: String, val moduleExporter: JsImportedModule)
class CrossModuleTransitiveExport(val internalName: JsName, val externalName: String)
fun CrossModuleTransitiveExport.getRequireEsmName() = "$externalName$ESM_EXTENSION"
class CrossModuleReferences(
val moduleKind: ModuleKind,
val importedModules: List<JsImportedModule>, // additional Kotlin imported modules
@@ -218,28 +227,45 @@ class CrossModuleReferences(
val imports: Map<String, CrossModuleImport>, // tag -> import statement
) {
// built from imports
var jsImports = emptyMap<String, JsVars.JsVar>() // tag -> import statement
var jsImports = emptyMap<String, JsStatement>() // tag -> import statement
private set
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 signature ${it.key}")
val exportRef = JsNameRef(
it.value.exportedAs,
it.value.moduleExporter.let {
if (moduleKind == ModuleKind.ES) {
it.makeRef()
} else {
ReservedJsNames.makeCrossModuleNameRef(it)
}
}
)
it.key to JsVars.JsVar(importedAs, exportRef)
it.key to it.value.generateCrossModuleImportStatement(importedAs)
}
}
private fun CrossModuleImport.generateCrossModuleImportStatement(importedAs: JsName): JsStatement {
return when (moduleKind) {
ModuleKind.ES -> generateJsImportStatement(importedAs)
else -> generateImportVariableDeclaration(importedAs)
}
}
private fun CrossModuleImport.generateImportVariableDeclaration(importedAs: JsName): JsStatement {
val exportRef = JsNameRef(exportedAs, ReservedJsNames.makeCrossModuleNameRef(moduleExporter.internalName))
return JsVars(JsVars.JsVar(importedAs, exportRef))
}
private fun CrossModuleImport.generateJsImportStatement(importedAs: JsName): JsStatement {
return JsImport(
moduleExporter.getRequireName(true),
JsImport.Element(JsName(exportedAs, false), importedAs.makeRef())
)
}
companion object {
fun Empty(moduleKind: ModuleKind) = CrossModuleReferences(moduleKind, listOf(), emptyList(), emptyMap(), emptyMap())
}
}
fun JsStatement.renameImportedSymbolInternalName(newName: JsName): JsStatement {
return when (this) {
is JsImport -> JsImport(module, JsImport.Element((target as JsImport.Target.Elements).elements.single().name, newName.makeRef()))
is JsVars -> JsVars(JsVars.JsVar(newName, vars.single().initExpression))
else -> error("Unexpected cross-module import statement ${this::class.qualifiedName}")
}
}
@@ -58,10 +58,10 @@ class Merger(
}
}
for ((tag, jsVar) in crossModuleReferences.jsImports) {
for ((tag, crossModuleJsImport) in crossModuleReferences.jsImports) {
val importName = nameMap[tag] ?: error("Missing name for declaration '$tag'")
importStatements.putIfAbsent(tag, JsVars(JsVars.JsVar(importName, jsVar.initExpression)))
importStatements.putIfAbsent(tag, crossModuleJsImport.renameImportedSymbolInternalName(importName))
}
if (crossModuleReferences.exports.isNotEmpty()) {
@@ -70,7 +70,7 @@ class Merger(
if (isEsModules) {
val exportedElements = crossModuleReferences.exports.entries.map { (tag, hash) ->
val internalName = nameMap[tag] ?: error("Missing name for declaration '$tag'")
JsExport.Element(internalName, JsName(hash, false))
JsExport.Element(internalName.makeRef(), JsName(hash, false))
}
additionalExports += JsExport(JsExport.Subject.Elements(exportedElements))
@@ -143,7 +143,7 @@ class Merger(
val exportedElements = currentModuleExportStatements.takeIf { it.isNotEmpty() }
?.asSequence()
?.flatMap { (it.subject as JsExport.Subject.Elements).elements }
?.distinctBy { (it.alias ?: it.name).ident }
?.distinctBy { it.alias?.ident ?: it.name.ident }
?.map { if (it.name.ident == it.alias?.ident) JsExport.Element(it.name, null) else it }
?.toList()
@@ -176,7 +176,7 @@ class Merger(
private fun transitiveJsExport(): List<JsStatement> {
return if (isEsModules) {
crossModuleReferences.transitiveJsExportFrom.map {
JsExport(JsExport.Subject.All, it.externalName)
JsExport(JsExport.Subject.All, it.getRequireEsmName())
}
} else {
val internalModuleName = ReservedJsNames.makeInternalModuleName()
@@ -8,6 +8,7 @@ package org.jetbrains.kotlin.ir.backend.js.transformers.irToJs
import org.jetbrains.kotlin.js.backend.ast.*
import org.jetbrains.kotlin.js.common.isValidES5Identifier
import org.jetbrains.kotlin.serialization.js.ModuleKind
import org.jetbrains.kotlin.utils.addToStdlib.partitionIsInstance
object ModuleWrapperTranslation {
object Namer {
@@ -105,7 +106,7 @@ object ModuleWrapperTranslation {
val scope = program.scope
val defineName = scope.declareName("define")
val invocationArgs = listOf(
JsArrayLiteral(listOf(JsStringLiteral("exports")) + importedModules.map { JsStringLiteral(it.requireName) }),
JsArrayLiteral(listOf(JsStringLiteral("exports")) + importedModules.map { JsStringLiteral(it.getRequireName()) }),
function
)
@@ -122,23 +123,32 @@ object ModuleWrapperTranslation {
val moduleName = scope.declareName("module")
val requireName = scope.declareName("require")
val invocationArgs = importedModules.map { JsInvocation(requireName.makeRef(), JsStringLiteral(it.requireName)) }
val invocationArgs = importedModules.map { JsInvocation(requireName.makeRef(), JsStringLiteral(it.getRequireName())) }
val invocation = JsInvocation(function, listOf(JsNameRef("exports", moduleName.makeRef())) + invocationArgs)
return listOf(invocation.makeStmt())
}
private fun wrapEsModule(function: JsFunction, importedModules: List<JsImportedModule>): List<JsStatement> {
val (alreadyPresentedImportStatements, restStatements) = function.body.statements.partitionIsInstance<JsStatement, JsImport>()
val importStatements = importedModules.zip(function.parameters.drop(1)).map {
JsImport(
it.first.externalName,
it.first.getRequireName(true),
if (it.first.plainReference == null) {
JsImport.Target.All(alias = it.second.name)
JsImport.Target.All(alias = it.second.name.makeRef())
} else {
JsImport.Target.Default(name = it.second.name)
JsImport.Target.Default(name = it.second.name.makeRef())
}
)
}
return importStatements + function.body.statements.dropLast(1)
val alreadyPresentedImportStatementsWithoutDuplicates = alreadyPresentedImportStatements
.groupBy { it.module }
.map { (module, import) ->
JsImport(module, *import.flatMap { it.elements }.toTypedArray())
}
return importStatements + alreadyPresentedImportStatementsWithoutDuplicates + restStatements.dropLast(1)
}
@@ -116,6 +116,21 @@ private fun JsNode.computeScopes(): Scope {
super.visitNameRef(nameRef)
}
override fun visitImport(import: JsImport) {
when (val target = import.target) {
is JsImport.Target.All -> target.alias.name?.let { currentScope.declaredNames += it }
is JsImport.Target.Default -> target.name.name?.let { currentScope.declaredNames += it }
is JsImport.Target.Elements -> target.elements.forEach { element ->
if (element.alias != null) {
element.alias?.name?.let { currentScope.declaredNames += it }
} else {
currentScope.declaredNames += element.name
}
}
}
super.visitImport(import)
}
override fun visitBreak(x: JsBreak) {}
override fun visitContinue(x: JsContinue) {}