From 95ab5e1b7ed93135f0d82cc406358032b7952c86 Mon Sep 17 00:00:00 2001 From: Anton Bannykh Date: Sun, 26 Sep 2021 11:24:43 +0300 Subject: [PATCH] [JS IR] generate JS separately for each file and link at the end Name clashes are handled as a post-processing step --- .../irToJs/IrModuleToJsTransformer.kt | 388 +++--------------- .../transformers/irToJs/JsClassGenerator.kt | 6 +- .../irToJs/JsIrProgramFragment.kt | 232 ++++++++++- .../transformers/irToJs/JsNameLinkingNamer.kt | 125 ++++++ .../irToJs/resolveTemporaryNames.kt | 119 ++++++ .../kotlin/ir/backend/js/utils/IrNamer.kt | 2 +- 6 files changed, 537 insertions(+), 335 deletions(-) create mode 100644 compiler/ir/backend.js/src/org/jetbrains/kotlin/ir/backend/js/transformers/irToJs/JsNameLinkingNamer.kt create mode 100644 compiler/ir/backend.js/src/org/jetbrains/kotlin/ir/backend/js/transformers/irToJs/resolveTemporaryNames.kt diff --git a/compiler/ir/backend.js/src/org/jetbrains/kotlin/ir/backend/js/transformers/irToJs/IrModuleToJsTransformer.kt b/compiler/ir/backend.js/src/org/jetbrains/kotlin/ir/backend/js/transformers/irToJs/IrModuleToJsTransformer.kt index f091507cf84..f60ac79290e 100644 --- a/compiler/ir/backend.js/src/org/jetbrains/kotlin/ir/backend/js/transformers/irToJs/IrModuleToJsTransformer.kt +++ b/compiler/ir/backend.js/src/org/jetbrains/kotlin/ir/backend/js/transformers/irToJs/IrModuleToJsTransformer.kt @@ -14,7 +14,6 @@ import org.jetbrains.kotlin.ir.backend.js.export.* import org.jetbrains.kotlin.ir.backend.js.lower.StaticMembersLowering import org.jetbrains.kotlin.ir.backend.js.utils.* import org.jetbrains.kotlin.ir.declarations.* -import org.jetbrains.kotlin.ir.symbols.IrClassSymbol import org.jetbrains.kotlin.ir.util.isEffectivelyExternal import org.jetbrains.kotlin.ir.util.isInterface import org.jetbrains.kotlin.js.backend.JsToStringGenerationVisitor @@ -27,7 +26,6 @@ import org.jetbrains.kotlin.js.sourceMap.SourceFilePathResolver import org.jetbrains.kotlin.js.sourceMap.SourceMap3Builder import org.jetbrains.kotlin.js.sourceMap.SourceMapBuilderConsumer import org.jetbrains.kotlin.js.util.TextOutputImpl -import org.jetbrains.kotlin.utils.DFS import java.io.File class IrModuleToJsTransformer( @@ -62,25 +60,12 @@ class IrModuleToJsTransformer( module.files.forEach { StaticMembersLowering(backendContext).lower(it) } } - val additionalPackages = with(backendContext) { - externalPackageFragment.values + listOf( - bodilessBuiltInsPackageFragment, - ) + packageLevelJsModules - } - - modules.forEach { module -> - namer.merge(module.files, additionalPackages) - } - - val jsCode = if (fullJs) generateWrappedModuleBody(modules, generateProgramFragments(modules, exportData, namer), namer) else null + val jsCode = if (fullJs) generateWrappedModuleBody(mainModuleName, modules, generateProgramFragments(modules, exportData)) else null val dceJsCode = if (dceJs) { eliminateDeadDeclarations(modules, backendContext, removeUnusedAssociatedObjects) - // Use a fresh namer for DCE so that we could compare the result with DCE-driven - // TODO: is this mode relevant for scripting? If yes, refactor so that the external name tables are used here when needed. - val namer = NameTables(emptyList(), context = backendContext) - namer.merge(modules.flatMap { it.files }, additionalPackages) - generateWrappedModuleBody(modules, generateProgramFragments(modules, exportData, namer), namer) + + generateWrappedModuleBody(mainModuleName, modules, generateProgramFragments(modules, exportData)) } else null return CompilerResult(jsCode, dceJsCode, dts) @@ -89,33 +74,13 @@ class IrModuleToJsTransformer( private fun generateProgramFragments( modules: Iterable, exportData: Map>>, - namer: NameTables, ): Map { - val nameGenerator = IrNamerImpl(newNameTables = namer, backendContext) - val fragments = mutableMapOf() modules.forEach { m -> m.files.forEach { f -> - val fragment = JsIrProgramFragment(f.fqName.asString()) - val exports = exportData[m]!![f]!! // TODO - - val internalModuleName = JsName("_", false) - val globalNames = NameTable(namer.globalNames) - val exportStatements = - ExportModelToJsStatements(nameGenerator, { globalNames.declareFreshName(it, it) }).generateModuleExport( - ExportedModule(mainModuleName, moduleKind, exports), - internalModuleName, - ) - - fragment.exports.statements += exportStatements - - if (exports.isNotEmpty()) { - fragment.dts = exports.toTypeScript(moduleKind) - } - - fragments[f] = fragment + fragments[f] = generateProgramFragment(f, exports) } } @@ -123,123 +88,19 @@ class IrModuleToJsTransformer( } private fun generateWrappedModuleBody( - modules: Iterable, - fragments: Map, - namer: NameTables - ): CompilationOutputs { - if (multiModule) { - - val refInfo = buildCrossModuleReferenceInfo(modules) - - val rM = modules.reversed() - - val main = rM.first() - val others = rM.drop(1) - - val mainModule = generateWrappedModuleBody2( - mainModuleName, - listOf(main), - others, - fragments, - namer, - refInfo, - generateMainCall = true - ) - - val dependencies = others.mapIndexed { index, module -> - val moduleName = module.externalModuleName() - - moduleName to generateWrappedModuleBody2( - moduleName, - listOf(module), - others.drop(index + 1), - fragments, - namer, - refInfo, - generateMainCall = false - ) - }.reversed() - - return CompilationOutputs(mainModule.jsCode, mainModule.jsProgram, mainModule.sourceMap, dependencies) - } else { - return generateWrappedModuleBody2( - mainModuleName, - modules, - emptyList(), - fragments, - namer, - EmptyCrossModuleReferenceInfo - ) - } - } - - private fun generateWrappedModuleBody2( moduleName: String, modules: Iterable, - dependencies: Iterable, fragments: Map, - namer: NameTables, - refInfo: CrossModuleReferenceInfo, - generateMainCall: Boolean = true ): CompilationOutputs { + val program = Merger( + moduleName, + moduleKind, + modules.map { it.files.map { fragments[it]!! } }, + generateScriptModule, + generateRegionComments + ).merge() - val nameGenerator = refInfo.withReferenceTracking( - IrNamerImpl(newNameTables = namer, backendContext), - modules - ) - val staticContext = JsStaticContext( - backendContext = backendContext, - irNamer = nameGenerator, - globalNameScope = namer.globalNames - ) - - val (importStatements, importedJsModules) = - generateImportStatements( - getNameForExternalDeclaration = { staticContext.getNameForStaticDeclaration(it) }, - declareFreshGlobal = { JsName(sanitizeName(it), false) } // TODO: Declare fresh name - ) - - val (moduleBody, callToMain, exportStatements) = generateModuleBody(modules, staticContext, fragments) - - val internalModuleName = JsName("_", false) - - val (crossModuleImports, importedKotlinModules) = generateCrossModuleImports( - nameGenerator, - modules, - dependencies, - { JsName(sanitizeName(it), false) }) - val crossModuleExports = generateCrossModuleExports(modules, refInfo, internalModuleName) - - val program = JsProgram() - if (generateScriptModule) { - with(program.globalBlock) { - statements.addWithComment("block: imports", importStatements + crossModuleImports) - statements += moduleBody - statements.addWithComment("block: exports", exportStatements + crossModuleExports) - } - } else { - val rootFunction = JsFunction(program.rootScope, JsBlock(), "root function").apply { - parameters += JsParameter(internalModuleName) - parameters += (importedJsModules + importedKotlinModules).map { JsParameter(it.internalName) } - with(body) { - statements.addWithComment("block: imports", importStatements + crossModuleImports) - statements += moduleBody - statements.addWithComment("block: exports", exportStatements + crossModuleExports) - if (generateMainCall && callToMain != null) { - statements += callToMain - } - statements += JsReturn(internalModuleName.makeRef()) - } - } - - program.globalBlock.statements += ModuleWrapperTranslation.wrap( - moduleName, - rootFunction, - importedJsModules + importedKotlinModules, - program, - kind = moduleKind - ) - } + program.resolveTemporaryNames() val jsCode = TextOutputImpl() @@ -279,79 +140,35 @@ class IrModuleToJsTransformer( ) } - private fun IrModuleFragment.externalModuleName(): String { - return moduleToName[this] ?: sanitizeName(safeName) - } - - private fun generateCrossModuleImports( - namerWithImports: IrNamerWithImports, - currentModules: Iterable, - allowedDependencies: Iterable, - declareFreshGlobal: (String) -> JsName - ): Pair, List> { - val imports = mutableListOf() - val modules = mutableListOf() - - namerWithImports.imports().forEach { (module, names) -> - check(module in allowedDependencies) { - val deps = if (names.size > 10) "[${names.take(10).joinToString()}, ...]" else "$names" - "Module ${currentModules.map { it.name.asString() }} depend on module ${module.name.asString()} via $deps" - } - - val moduleName = declareFreshGlobal(module.safeName) - modules += JsImportedModule(module.externalModuleName(), moduleName, null, relativeRequirePath) - - names.forEach { - imports += JsVars(JsVars.JsVar(JsName(it, false), JsNameRef(it, JsNameRef("\$crossModule\$", moduleName.makeRef())))) - } - } - - return imports to modules - } - - private fun generateCrossModuleExports( - modules: Iterable, - refInfo: CrossModuleReferenceInfo, - internalModuleName: JsName - ): List { - return modules.flatMap { - refInfo.exports(it).map { - jsAssignment( - JsNameRef(it, JsNameRef("\$crossModule\$", internalModuleName.makeRef())), - JsNameRef(it) - ).makeStmt() - } - }.let { - if (!it.isEmpty()) { - val createExportBlock = jsAssignment( - JsNameRef("\$crossModule\$", internalModuleName.makeRef()), - JsAstUtils.or(JsNameRef("\$crossModule\$", internalModuleName.makeRef()), JsObjectLiteral()) - ).makeStmt() - return listOf(createExportBlock) + it - } else it - } - } - - private data class ModuleBody(val statements: List, val mainFunction: JsStatement?, val exportStatements: List) - - private fun generateModuleBody( - modules: Iterable, - staticContext: JsStaticContext, - fragments: Map - ): ModuleBody { - val thisModuleFragments = modules.map { it.files.map { fragments[it]!!.fillProgramFragment(it, staticContext) } } - - return merge(thisModuleFragments, staticContext) - } - private val generateFilePaths = backendContext.configuration.getBoolean(JSConfigurationKeys.GENERATE_COMMENTS_WITH_FILE_PATH) private val pathPrefixMap = backendContext.configuration.getMap(JSConfigurationKeys.FILE_PATHS_PREFIX_MAP) - private fun JsIrProgramFragment.fillProgramFragment(file: IrFile, staticContext: JsStaticContext): JsIrProgramFragment { - require(staticContext.classModels.isEmpty()) - require(staticContext.initializerBlock.statements.isEmpty()) + private fun generateProgramFragment(file: IrFile, exports: List): JsIrProgramFragment { + val nameGenerator = JsNameLinkingNamer(backendContext) + + val staticContext = JsStaticContext( + backendContext = backendContext, + irNamer = nameGenerator, + globalNameScope = namer.globalNames + ) + + val result = JsIrProgramFragment(file.fqName.asString()) + + val internalModuleName = JsName("_", false) + val globalNames = NameTable(namer.globalNames) + val exportStatements = ExportModelToJsStatements(staticContext, { globalNames.declareFreshName(it, it) }).generateModuleExport( + ExportedModule(mainModuleName, moduleKind, exports), + internalModuleName, + ) + + result.exports.statements += exportStatements + + if (exports.isNotEmpty()) { + result.dts = exports.toTypeScript(moduleKind) + } + + val statements = result.declarations.statements - val statements = this.declarations.statements val fileStatements = file.accept(IrFileToJsTransformer(), staticContext).statements if (fileStatements.isNotEmpty()) { @@ -376,84 +193,51 @@ class IrModuleToJsTransformer( } statements.addAll(fileStatements) - statements.endRegion() + if (generateRegionComments) { + statements += JsSingleLineComment("endregion") + } } - this.classes += staticContext.classModels - this.initializers.statements += staticContext.initializerBlock.statements + staticContext.classModels.entries.forEach { (symbol, model) -> + result.classes[nameGenerator.getNameForClass(symbol.owner)] = model + } + + result.initializers.statements += staticContext.initializerBlock.statements if (mainArguments != null) { JsMainFunctionDetector(backendContext).getMainFunctionOrNull(file)?.let { val jsName = staticContext.getNameForStaticFunction(it) val generateArgv = it.valueParameters.firstOrNull()?.isStringArrayParameter() ?: false val generateContinuation = it.isLoweredSuspendFunction(backendContext) - this.mainFunction = JsInvocation(jsName.makeRef(), generateMainArguments(generateArgv, generateContinuation, staticContext)).makeStmt() + result.mainFunction = JsInvocation(jsName.makeRef(), generateMainArguments(generateArgv, generateContinuation, staticContext)).makeStmt() } } backendContext.testFunsPerFile[file]?.let { - this.testFunInvocation = JsInvocation(staticContext.getNameForStaticFunction(it).makeRef()).makeStmt() + result.testFunInvocation = JsInvocation(staticContext.getNameForStaticFunction(it).makeRef()).makeStmt() + result.suiteFn = staticContext.getNameForStaticFunction(backendContext.suiteFun!!.owner) } - staticContext.classModels.clear() - staticContext.initializerBlock.statements.clear() - return this - } - private fun merge(fragments: List>, staticContext: JsStaticContext): ModuleBody { - val statements = mutableListOf().also { - if (!generateScriptModule) it += JsStringLiteral("use strict").makeStmt() + result.importedModules += nameGenerator.importedModules + + fun computeTag(declaration: IrDeclaration): String { + // TODO proper tags + return System.identityHashCode(declaration).toString() } - val preDeclarationBlock = JsGlobalBlock() - val postDeclarationBlock = JsGlobalBlock() - - statements.addWithComment("block: pre-declaration", preDeclarationBlock) - - val classModels = mutableMapOf() - val initializerBlock = JsGlobalBlock() - fragments.forEach { - it.forEach { - statements += it.declarations.statements - classModels += it.classes - initializerBlock.statements += it.initializers.statements - } + nameGenerator.nameMap.entries.forEach { (declaration, name) -> + val tag = computeTag(declaration) + result.nameBindings[tag] = name } - // sort member forwarding code - processClassModels(classModels, preDeclarationBlock, postDeclarationBlock) - - statements.addWithComment("block: post-declaration", postDeclarationBlock.statements) - statements.addWithComment("block: init", initializerBlock.statements) - - val lastModuleFragments = fragments.last() - - // Merge test function invocations - if (lastModuleFragments.any { it.testFunInvocation != null }) { - val testFunBody = JsBlock() - val testFun = JsFunction(emptyScope, testFunBody, "root test fun") - val suiteFunRef = staticContext.getNameForStaticFunction(backendContext.suiteFun!!.owner).makeRef() - - val tests = lastModuleFragments.filter { it.testFunInvocation != null } - .groupBy({ it.packageFqn }) { it.testFunInvocation } // String -> [IrSimpleFunction] - - for ((pkg, testCalls) in tests) { - val pkgTestFun = JsFunction(emptyScope, JsBlock(), "test fun for $pkg") - pkgTestFun.body.statements += testCalls - testFun.body.statements += JsInvocation(suiteFunRef, JsStringLiteral(pkg), JsBooleanLiteral(false), pkgTestFun).makeStmt() - } - - statements.startRegion("block: tests") - statements += JsInvocation(testFun).makeStmt() - statements.endRegion() + nameGenerator.imports.entries.forEach { (declaration, importExpression) -> + val tag = computeTag(declaration) + result.imports[tag] = importExpression } - val callToMain = lastModuleFragments.sortedBy { it.packageFqn }.firstNotNullOfOrNull { it.mainFunction } - - val exportStatements = fragments.flatMap { it.flatMap { it.exports.statements } } - - return ModuleBody(statements, callToMain, exportStatements) + return result } private fun generateMainArguments( @@ -525,54 +309,4 @@ class IrModuleToJsTransformer( val importedJsModules = (declarationLevelJsModules + packageLevelJsModules).distinctBy { it.key } return Pair(importStatements, importedJsModules) } - - - private fun MutableList.startRegion(description: String = "") { - if (generateRegionComments) { - this += JsSingleLineComment("region $description") - } - } - - private fun MutableList.endRegion() { - if (generateRegionComments) { - this += JsSingleLineComment("endregion") - } - } - - private fun MutableList.addWithComment(regionDescription: String = "", block: JsBlock) { - startRegion(regionDescription) - this += block - endRegion() - } - - private fun MutableList.addWithComment(regionDescription: String = "", statements: List) { - if (statements.isEmpty()) return - - startRegion(regionDescription) - this += statements - endRegion() - } -} - -fun processClassModels( - classModelMap: Map, - preDeclarationBlock: JsBlock, - postDeclarationBlock: JsBlock -) { - val declarationHandler = object : DFS.AbstractNodeHandler() { - override fun result() {} - override fun afterChildren(current: IrClassSymbol) { - classModelMap[current]?.let { - preDeclarationBlock.statements += it.preDeclarationBlock.statements - postDeclarationBlock.statements += it.postDeclarationBlock.statements - } - } - } - - DFS.dfs( - classModelMap.keys, - { klass -> classModelMap[klass]?.superClasses ?: emptyList() }, - declarationHandler - ) -} - +} \ No newline at end of file diff --git a/compiler/ir/backend.js/src/org/jetbrains/kotlin/ir/backend/js/transformers/irToJs/JsClassGenerator.kt b/compiler/ir/backend.js/src/org/jetbrains/kotlin/ir/backend/js/transformers/irToJs/JsClassGenerator.kt index a3f6c30eaa5..bcd0a5a5dbb 100644 --- a/compiler/ir/backend.js/src/org/jetbrains/kotlin/ir/backend/js/transformers/irToJs/JsClassGenerator.kt +++ b/compiler/ir/backend.js/src/org/jetbrains/kotlin/ir/backend/js/transformers/irToJs/JsClassGenerator.kt @@ -32,7 +32,7 @@ class JsClassGenerator(private val irClass: IrClass, val context: JsGenerationCo } private val classPrototypeRef = prototypeOf(classNameRef) private val classBlock = JsGlobalBlock() - private val classModel = JsIrClassModel(irClass) + private val classModel = JsIrClassModel(irClass.superTypes.map { context.getNameForClass((it.classifierOrFail as IrClassSymbol).owner) }) private val es6mode = context.staticContext.backendContext.es6mode @@ -364,9 +364,7 @@ class JsClassGenerator(private val irClass: IrClass, val context: JsGenerationCo private val IrClassifierSymbol.isInterface get() = (owner as? IrClass)?.isInterface == true private val IrClassifierSymbol.isEffectivelyExternal get() = (owner as? IrDeclaration)?.isEffectivelyExternal() == true -class JsIrClassModel(val klass: IrClass) { - val superClasses = klass.superTypes.map { it.classifierOrFail as IrClassSymbol } - +class JsIrClassModel(val superClasses: List) { val preDeclarationBlock = JsGlobalBlock() val postDeclarationBlock = JsGlobalBlock() } diff --git a/compiler/ir/backend.js/src/org/jetbrains/kotlin/ir/backend/js/transformers/irToJs/JsIrProgramFragment.kt b/compiler/ir/backend.js/src/org/jetbrains/kotlin/ir/backend/js/transformers/irToJs/JsIrProgramFragment.kt index 9ce03492fac..60e037d53d3 100644 --- a/compiler/ir/backend.js/src/org/jetbrains/kotlin/ir/backend/js/transformers/irToJs/JsIrProgramFragment.kt +++ b/compiler/ir/backend.js/src/org/jetbrains/kotlin/ir/backend/js/transformers/irToJs/JsIrProgramFragment.kt @@ -5,16 +5,242 @@ package org.jetbrains.kotlin.ir.backend.js.transformers.irToJs +import org.jetbrains.kotlin.ir.backend.js.utils.emptyScope +import org.jetbrains.kotlin.ir.declarations.IrDeclaration import org.jetbrains.kotlin.ir.symbols.IrClassSymbol -import org.jetbrains.kotlin.js.backend.ast.JsGlobalBlock -import org.jetbrains.kotlin.js.backend.ast.JsStatement +import org.jetbrains.kotlin.js.backend.ast.* +import org.jetbrains.kotlin.serialization.js.ModuleKind +import org.jetbrains.kotlin.utils.DFS class JsIrProgramFragment(val packageFqn: String) { + val nameBindings = mutableMapOf() val declarations = JsGlobalBlock() val exports = JsGlobalBlock() + val importedModules = mutableListOf() + val imports = mutableMapOf() var dts: String? = null - val classes = mutableMapOf() + val classes = mutableMapOf() val initializers = JsGlobalBlock() var mainFunction: JsStatement? = null var testFunInvocation: JsStatement? = null + var suiteFn: JsName? = null +} + +class Merger( + private val moduleName: String, + private val moduleKind: ModuleKind, + private val fragments: List>, + private val generateScriptModule: Boolean, + private val generateRegionComments: Boolean, +) { + + private val importStatements = mutableMapOf() + private val importedModulesMap = mutableMapOf() + + private fun linkJsNames() { + val nameMap = mutableMapOf() + + fragments.flatMap { it }.forEach { f -> + f.buildRenames(nameMap).run { + rename(f.declarations) + rename(f.exports) + + f.imports.entries.forEach { (declaration, importExpression) -> + val importName = nameMap[declaration] ?: error("Missing name for declaration '${declaration}'") + importStatements.putIfAbsent(declaration, JsVars(JsVars.JsVar(importName, rename(importExpression)))) + } + + val classModels = mutableMapOf() + f.classes + f.classes.clear() + classModels.entries.forEach { (name, model) -> + f.classes[rename(name)] = JsIrClassModel(model.superClasses.map { rename(it) }).also { + it.preDeclarationBlock.statements += model.preDeclarationBlock.statements + it.postDeclarationBlock.statements += model.postDeclarationBlock.statements + rename(it.preDeclarationBlock) + rename(it.postDeclarationBlock) + } + } + + rename(f.initializers) + f.mainFunction?.let { rename(it) } + f.testFunInvocation?.let { rename(it) } + f.suiteFn?.let { f.suiteFn = rename(it) } + } + } + } + + private fun JsIrProgramFragment.buildRenames(nameMap: MutableMap): Map { + val result = mutableMapOf() + + this.importedModules.forEach { module -> + val existingModule = importedModulesMap.getOrPut(module.key) { module } + if (existingModule !== module) { + result[module.internalName] = existingModule.internalName + } + } + + this.nameBindings.entries.forEach { (tag, name) -> + val existingName = nameMap.getOrPut(tag) { name } + if (existingName !== name) { + result[name] = existingName + } + } + + return result + } + + private fun Map.rename(name: JsName): JsName = getOrElse(name) { name } + + private fun Map.rename(rootNode: T): T { + rootNode.accept(object : RecursiveJsVisitor() { + override fun visitElement(node: JsNode) { + super.visitElement(node) + if (node is HasName) { + node.name = node.name?.let { rename(it) } + } + } + }) + return rootNode + } + + fun merge(): JsProgram { + linkJsNames() + + val moduleBody = mutableListOf().also { + if (!generateScriptModule) it += JsStringLiteral("use strict").makeStmt() + } + + val preDeclarationBlock = JsGlobalBlock() + val postDeclarationBlock = JsGlobalBlock() + + moduleBody.addWithComment("block: pre-declaration", preDeclarationBlock) + + val classModels = mutableMapOf() + val initializerBlock = JsGlobalBlock() + fragments.forEach { + it.forEach { + moduleBody += it.declarations.statements + classModels += it.classes + initializerBlock.statements += it.initializers.statements + } + } + + // sort member forwarding code + processClassModels(classModels, preDeclarationBlock, postDeclarationBlock) + + moduleBody.addWithComment("block: post-declaration", postDeclarationBlock.statements) + moduleBody.addWithComment("block: init", initializerBlock.statements) + + val lastModuleFragments = fragments.last() + + // Merge test function invocations + if (lastModuleFragments.any { it.testFunInvocation != null }) { + val testFunBody = JsBlock() + val testFun = JsFunction(emptyScope, testFunBody, "root test fun") + val suiteFunRef = lastModuleFragments.firstNotNullOf { it.suiteFn }.makeRef() + + val tests = lastModuleFragments.filter { it.testFunInvocation != null } + .groupBy({ it.packageFqn }) { it.testFunInvocation } // String -> [IrSimpleFunction] + + for ((pkg, testCalls) in tests) { + val pkgTestFun = JsFunction(emptyScope, JsBlock(), "test fun for $pkg") + pkgTestFun.body.statements += testCalls + testFun.body.statements += JsInvocation(suiteFunRef, JsStringLiteral(pkg), JsBooleanLiteral(false), pkgTestFun).makeStmt() + } + + moduleBody.startRegion("block: tests") + moduleBody += JsInvocation(testFun).makeStmt() + moduleBody.endRegion() + } + + val callToMain = lastModuleFragments.sortedBy { it.packageFqn }.firstNotNullOfOrNull { it.mainFunction } + + val exportStatements = fragments.flatMap { it.flatMap { it.exports.statements } } + + val importedJsModules = this.importedModulesMap.values.toList() + val importStatements = this.importStatements.values.toList() + + val internalModuleName = JsName("_", false) + + val program = JsProgram() + + if (generateScriptModule) { + with(program.globalBlock) { + this.statements.addWithComment("block: imports", importStatements) + this.statements += moduleBody + this.statements.addWithComment("block: exports", exportStatements) + } + } else { + val rootFunction = JsFunction(program.rootScope, JsBlock(), "root function").apply { + parameters += JsParameter(internalModuleName) + parameters += (importedJsModules).map { JsParameter(it.internalName) } + with(body) { + this.statements.addWithComment("block: imports", importStatements) + this.statements += moduleBody + this.statements.addWithComment("block: exports", exportStatements) + callToMain?.let { this.statements += it } + this.statements += JsReturn(internalModuleName.makeRef()) + } + } + + program.globalBlock.statements += ModuleWrapperTranslation.wrap( + moduleName, + rootFunction, + importedJsModules, + program, + kind = moduleKind + ) + } + + return program + } + + + private fun processClassModels( + classModelMap: Map, + preDeclarationBlock: JsBlock, + postDeclarationBlock: JsBlock + ) { + val declarationHandler = object : DFS.AbstractNodeHandler() { + override fun result() {} + override fun afterChildren(current: JsName) { + classModelMap[current]?.let { + preDeclarationBlock.statements += it.preDeclarationBlock.statements + postDeclarationBlock.statements += it.postDeclarationBlock.statements + } + } + } + + DFS.dfs( + classModelMap.keys, + { klass -> classModelMap[klass]?.superClasses ?: emptyList() }, + declarationHandler + ) + } + + private fun MutableList.startRegion(description: String = "") { + if (generateRegionComments) { + this += JsSingleLineComment("region $description") + } + } + + private fun MutableList.endRegion() { + if (generateRegionComments) { + this += JsSingleLineComment("endregion") + } + } + + private fun MutableList.addWithComment(regionDescription: String = "", block: JsBlock) { + startRegion(regionDescription) + this += block + endRegion() + } + + private fun MutableList.addWithComment(regionDescription: String = "", statements: List) { + if (statements.isEmpty()) return + + startRegion(regionDescription) + this += statements + endRegion() + } } \ No newline at end of file diff --git a/compiler/ir/backend.js/src/org/jetbrains/kotlin/ir/backend/js/transformers/irToJs/JsNameLinkingNamer.kt b/compiler/ir/backend.js/src/org/jetbrains/kotlin/ir/backend/js/transformers/irToJs/JsNameLinkingNamer.kt new file mode 100644 index 00000000000..187d39531bf --- /dev/null +++ b/compiler/ir/backend.js/src/org/jetbrains/kotlin/ir/backend/js/transformers/irToJs/JsNameLinkingNamer.kt @@ -0,0 +1,125 @@ +/* + * Copyright 2010-2021 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.transformers.irToJs + +import org.jetbrains.kotlin.ir.backend.js.JsIrBackendContext +import org.jetbrains.kotlin.ir.backend.js.utils.* +import org.jetbrains.kotlin.ir.declarations.* +import org.jetbrains.kotlin.ir.symbols.IrClassSymbol +import org.jetbrains.kotlin.ir.types.IrSimpleType +import org.jetbrains.kotlin.ir.util.isEffectivelyExternal +import org.jetbrains.kotlin.ir.util.parentAsClass +import org.jetbrains.kotlin.ir.util.render +import org.jetbrains.kotlin.js.backend.ast.* +import org.jetbrains.kotlin.utils.DFS +import org.jetbrains.kotlin.utils.addToStdlib.safeAs + +class JsNameLinkingNamer(private val context: JsIrBackendContext) : IrNamerBase() { + + val nameMap = mutableMapOf() + + private fun IrDeclarationWithName.getName(prefix: String = ""): JsName { + return nameMap.getOrPut(this) { JsName(sanitizeName(prefix + getJsNameOrKotlinName().asString()), true) } + } + + val importedModules = mutableListOf() + val imports = mutableMapOf() + + override fun getNameForStaticDeclaration(declaration: IrDeclarationWithName): JsName { + + if (declaration.isEffectivelyExternal()) { + val jsModule: String? = declaration.getJsModule() + val maybeParentFile: IrFile? = declaration.parent as? IrFile + val fileJsModule: String? = maybeParentFile?.getJsModule() + val jsQualifier: String? = maybeParentFile?.getJsQualifier() + + when { + jsModule != null -> { + val name = JsName(declaration.getJsNameOrKotlinName().asString(), false) + importedModules += JsImportedModule(jsModule, name, name.makeRef()) + return name + } + + fileJsModule != null -> { + if (declaration !in nameMap) { + val moduleName = JsName("\$module\$$jsModule", true) + importedModules += JsImportedModule(fileJsModule, moduleName, null) + val qualifiedReference = + if (jsQualifier == null) moduleName.makeRef() else JsNameRef(jsQualifier, moduleName.makeRef()) + imports[declaration] = JsNameRef(declaration.getJsNameOrKotlinName().identifier, qualifiedReference) + return declaration.getName() + } + } + + else -> { + var name = declaration.getJsNameOrKotlinName().identifier + if (jsQualifier != null) + name = "$jsQualifier.$name" + + return name.toJsName(temporary = false) + } + } + } + + return declaration.getName() + } + + override fun getNameForMemberFunction(function: IrSimpleFunction): JsName { + require(function.dispatchReceiverParameter != null) + val signature = jsFunctionSignature(function, context) + return signature.toJsName() + } + + override fun getNameForMemberField(field: IrField): JsName { + require(!field.isStatic) + // TODO this looks funny. Rethink. + return JsName(field.parentAsClass.fieldData().pickFieldName(field), false) + } + + private class FieldData(private val nameCnt: Map) { + fun pickFieldName(field: IrField): String { + val fieldName = field.safeName() + val cnt = nameCnt[fieldName] ?: error("Unexpected field: ${field.render()}") + return fieldName + "_$cnt" + } + } + + private val fieldDataCache = mutableMapOf() + + private fun IrClass.fieldData(): FieldData { + return fieldDataCache.getOrPut(this) { + val nameCnt = mutableMapOf() + + val allClasses = DFS.topologicalOrder(listOf(this)) { node -> + node.superTypes.mapNotNull { + it.safeAs()?.classifier.safeAs()?.owner + } + } + + allClasses.forEach { + it.declarations.forEach { + when { + it is IrField -> { + val safeName = it.safeName() + nameCnt[safeName] = nameCnt.getOrDefault(safeName, 0) + 1 + } + it is IrFunction && it.dispatchReceiverParameter != null -> { + nameCnt[jsFunctionSignature(it, context)] = 1 // avoid clashes with member functions + } + } + } + } + + return FieldData(nameCnt) + } + } +} + +private fun IrField.safeName(): String { + return sanitizeName(name.asString()).let { + if (it.lastOrNull()!!.isDigit()) it + "_" else it // Avoid name clashes + } +} \ No newline at end of file diff --git a/compiler/ir/backend.js/src/org/jetbrains/kotlin/ir/backend/js/transformers/irToJs/resolveTemporaryNames.kt b/compiler/ir/backend.js/src/org/jetbrains/kotlin/ir/backend/js/transformers/irToJs/resolveTemporaryNames.kt new file mode 100644 index 00000000000..10610b3ea2e --- /dev/null +++ b/compiler/ir/backend.js/src/org/jetbrains/kotlin/ir/backend/js/transformers/irToJs/resolveTemporaryNames.kt @@ -0,0 +1,119 @@ +/* + * Copyright 2010-2021 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.transformers.irToJs + +import org.jetbrains.kotlin.js.backend.ast.* + +fun JsNode.resolveTemporaryNames() { + val renamings = resolveNames() + accept(object : RecursiveJsVisitor() { + override fun visitElement(node: JsNode) { + super.visitElement(node) + if (node is HasName) { + val name = node.name + if (name != null) { + renamings[name]?.let { node.name = it } + } + } + } + }) +} + +private fun JsNode.resolveNames(): Map { + val rootScope = computeScopes().liftUsedNames() + val replacements = mutableMapOf() + fun traverse(scope: Scope) { + // Don't clash with non-temporary names declared in current scope. It's for rare cases like `_` or `Kotlin` names, + // since most of local declarations are temporary. + val occupiedNames = scope.declaredNames.asSequence().filter { !it.isTemporary }.map { it.ident }.toMutableSet() + + // Don't clash with non-temporary names used in current scope. It's ok to clash with unused names. + // Don't clash with used temporary names from outer scopes that get their resolved names. For example, + // when function declares temporary name `foo` and inner function both declares temporary name `foo` and uses + // (directly or propagates to inner scope) outer `foo`, we should resolve distinct strings for these `foo` names. + // Outer `foo` resolves first, so when traversing inner scope, we should take it into account. + occupiedNames += scope.usedNames.asSequence().mapNotNull { if (!it.isTemporary) it.ident else replacements[it]?.ident } + + for (temporaryName in scope.declaredNames.asSequence().filter { it.isTemporary }) { + var resolvedName = temporaryName.ident + var suffix = 0 + while (resolvedName in JsDeclarationScope.RESERVED_WORDS || !occupiedNames.add(resolvedName)) { + resolvedName = "${temporaryName.ident}_${suffix++}" + } + replacements[temporaryName] = JsDynamicScope.declareName(resolvedName).apply { copyMetadataFrom(temporaryName) } + occupiedNames += resolvedName + } + scope.children.forEach(::traverse) + } + + traverse(rootScope) + + return replacements +} + +// When name is used by inner scope, it's implicitly used by outer scopes +private fun Scope.liftUsedNames(): Scope { + fun traverse(scope: Scope) { + scope.children.forEach { child -> + scope.usedNames += child.declaredNames + traverse(child) + scope.usedNames += child.usedNames + } + } + traverse(this) + return this +} + +private fun JsNode.computeScopes(): Scope { + val rootScope = Scope() + accept(object : RecursiveJsVisitor() { + var currentScope: Scope = rootScope + + override fun visitFunction(x: JsFunction) { + x.name?.let { currentScope.declaredNames += it } + val oldScope = currentScope + currentScope = Scope().apply { + parent = currentScope + currentScope.children += this + } + currentScope.declaredNames += x.parameters.map { it.name } + super.visitFunction(x) + currentScope = oldScope + } + + override fun visitCatch(x: JsCatch) { + currentScope.declaredNames += x.parameter.name + super.visitCatch(x) + } + + override fun visit(x: JsVars.JsVar) { + currentScope.declaredNames += x.name + super.visit(x) + } + + override fun visitNameRef(nameRef: JsNameRef) { + if (nameRef.qualifier == null) { + val name = nameRef.name + currentScope.usedNames += name ?: JsDynamicScope.declareName(nameRef.ident) + } + + super.visitNameRef(nameRef) + } + + override fun visitBreak(x: JsBreak) {} + + override fun visitContinue(x: JsContinue) {} + }) + + return rootScope +} + +private class Scope { + var parent: Scope? = null + val declaredNames = mutableSetOf() + val usedNames = mutableSetOf() + val children = mutableSetOf() +} \ No newline at end of file diff --git a/compiler/ir/backend.js/src/org/jetbrains/kotlin/ir/backend/js/utils/IrNamer.kt b/compiler/ir/backend.js/src/org/jetbrains/kotlin/ir/backend/js/utils/IrNamer.kt index f401d082059..0fccae8cefe 100644 --- a/compiler/ir/backend.js/src/org/jetbrains/kotlin/ir/backend/js/utils/IrNamer.kt +++ b/compiler/ir/backend.js/src/org/jetbrains/kotlin/ir/backend/js/utils/IrNamer.kt @@ -28,7 +28,7 @@ abstract class IrNamerBase : IrNamer { abstract override fun getNameForMemberField(field: IrField): JsName abstract override fun getNameForStaticDeclaration(declaration: IrDeclarationWithName): JsName - protected fun String.toJsName() = JsName(this) + protected fun String.toJsName(temporary: Boolean = true) = JsName(this, temporary) override fun getNameForStaticFunction(function: IrSimpleFunction): JsName = getNameForStaticDeclaration(function)