JS IR: per-module .js generation support

This commit is contained in:
Anton Bannykh
2020-06-03 18:41:52 +03:00
parent deb5dc1057
commit da79f93c61
38 changed files with 1358 additions and 124 deletions
@@ -135,6 +135,9 @@ class K2JSCompilerArguments : CommonCompilerArguments() {
)
var irModuleName: String? by NullableStringFreezableVar(null)
@Argument(value = "-Xir-per-module", description = "Splits generated .js per-module")
var irPerModule: Boolean by FreezableVar(false)
@Argument(
value = "-Xinclude",
valueDescription = "<path>",
@@ -229,14 +229,19 @@ class K2JsIrCompiler : CLICompiler<K2JSCompilerArguments>() {
mainArguments = mainCallArguments,
generateFullJs = !arguments.irDce,
generateDceJs = arguments.irDce,
dceDriven = arguments.irDceDriven
dceDriven = arguments.irDceDriven,
multiModule = arguments.irPerModule,
relativeRequirePath = true
)
} catch (e: JsIrCompilationError) {
return COMPILATION_ERROR
}
val jsCode = if (arguments.irDce && !arguments.irDceDriven) compiledModule.dceJsCode!! else compiledModule.jsCode!!
outputFile.writeText(jsCode)
outputFile.writeText(jsCode.mainModule)
jsCode.dependencies.forEach { (name, content) ->
outputFile.resolveSibling("$name.js").writeText(content)
}
if (arguments.generateDts) {
val dtsFile = outputFile.withReplacedExtensionOrNull(outputFile.extension, "d.ts")!!
dtsFile.writeText(compiledModule.tsDefinitions ?: error("No ts definitions"))
@@ -8,10 +8,7 @@ package org.jetbrains.kotlin.ir.backend.js
import org.jetbrains.kotlin.backend.common.ir.isMemberOfOpenClass
import org.jetbrains.kotlin.ir.IrElement
import org.jetbrains.kotlin.ir.backend.js.export.isExported
import org.jetbrains.kotlin.ir.backend.js.utils.associatedObject
import org.jetbrains.kotlin.ir.backend.js.utils.getJsName
import org.jetbrains.kotlin.ir.backend.js.utils.getJsNameOrKotlinName
import org.jetbrains.kotlin.ir.backend.js.utils.isAssociatedObjectAnnotatedAnnotation
import org.jetbrains.kotlin.ir.backend.js.utils.*
import org.jetbrains.kotlin.ir.declarations.*
import org.jetbrains.kotlin.ir.expressions.*
import org.jetbrains.kotlin.ir.symbols.IrClassSymbol
@@ -26,11 +23,10 @@ import java.util.*
fun eliminateDeadDeclarations(
modules: Iterable<IrModuleFragment>,
context: JsIrBackendContext,
mainFunction: IrSimpleFunction?
context: JsIrBackendContext
) {
val allRoots = stageController.withInitialIr { buildRoots(modules, context, mainFunction) }
val allRoots = stageController.withInitialIr { buildRoots(modules, context) }
val usefulDeclarations = usefulDeclarations(allRoots, context)
@@ -43,7 +39,7 @@ private fun IrField.isConstant(): Boolean {
return correspondingPropertySymbol?.owner?.isConst ?: false
}
private fun buildRoots(modules: Iterable<IrModuleFragment>, context: JsIrBackendContext, mainFunction: IrSimpleFunction?): Iterable<IrDeclaration> {
private fun buildRoots(modules: Iterable<IrModuleFragment>, context: JsIrBackendContext): Iterable<IrDeclaration> {
val rootDeclarations =
(modules.flatMap { it.files } + context.packageLevelJsModules + context.externalPackageFragment.values).flatMapTo(mutableListOf()) { file ->
file.declarations.flatMap { if (it is IrProperty) listOfNotNull(it.backingField, it.getter, it.setter) else listOf(it) }
@@ -56,9 +52,9 @@ private fun buildRoots(modules: Iterable<IrModuleFragment>, context: JsIrBackend
}.filter { !(it is IrField && it.isConstant() && !it.isExported(context)) }
}
if (context.hasTests) rootDeclarations += context.testContainer
rootDeclarations += context.testRoots.values
if (mainFunction != null) {
JsMainFunctionDetector.getMainFunctionOrNull(modules.last())?.let { mainFunction ->
rootDeclarations += mainFunction
if (mainFunction.isSuspend) {
rootDeclarations += context.coroutineEmptyContinuation.owner
@@ -92,8 +92,12 @@ class JsIrBackendContext(
private val internalPackageFragmentDescriptor = EmptyPackageFragmentDescriptor(builtIns.builtInsModule, FqName("kotlin.js.internal"))
val implicitDeclarationFile = run {
IrFileImpl(object : SourceManager.FileEntry {
override val name = "<implicitDeclarations>"
syntheticFile("implicitDeclarations", irModuleFragment)
}
private fun syntheticFile(name: String, module: IrModuleFragment): IrFile {
return IrFileImpl(object : SourceManager.FileEntry {
override val name = "<$name>"
override val maxOffset = UNDEFINED_OFFSET
override fun getSourceRangeInfo(beginOffset: Int, endOffset: Int) =
@@ -110,20 +114,24 @@ class JsIrBackendContext(
override fun getLineNumber(offset: Int) = UNDEFINED_OFFSET
override fun getColumnNumber(offset: Int) = UNDEFINED_OFFSET
}, internalPackageFragmentDescriptor).also {
irModuleFragment.files += it
module.files += it
}
}
private var testContainerField: IrSimpleFunction? = null
private val testContainerFuns = mutableMapOf<IrModuleFragment, IrSimpleFunction>()
val hasTests get() = testContainerField != null
val testContainer: IrSimpleFunction
get() = testContainerField ?: JsIrBuilder.buildFunction("test fun", irBuiltIns.unitType, implicitDeclarationFile).apply {
body = JsIrBuilder.buildBlockBody(emptyList())
testContainerField = this
implicitDeclarationFile.declarations += this
fun createTestContainerFun(module: IrModuleFragment): IrSimpleFunction {
return testContainerFuns.getOrPut(module) {
val file = syntheticFile("tests", module)
JsIrBuilder.buildFunction("test fun", irBuiltIns.unitType, file).apply {
body = JsIrBuilder.buildBlockBody(emptyList())
file.declarations += this
}
}
}
val testRoots: Map<IrModuleFragment, IrSimpleFunction>
get() = testContainerFuns
override val mapping = JsMapping()
override val declarationFactory = JsDeclarationFactory(mapping)
@@ -618,12 +618,6 @@ private val callsLoweringPhase = makeBodyLoweringPhase(
description = "Handle intrinsics"
)
private val testGenerationPhase = makeJsModulePhase(
::TestGenerator,
name = "TestGenerationLowering",
description = "Generate invocations to kotlin.test suite and test functions"
).toModuleLowering()
private val staticMembersLoweringPhase = makeDeclarationTransformerPhase(
::StaticMembersLowering,
name = "StaticMembersLowering",
@@ -657,7 +651,6 @@ private val cleanupLoweringPhase = makeBodyLoweringPhase(
val loweringList = listOf<Lowering>(
scriptRemoveReceiverLowering,
validateIrBeforeLowering,
testGenerationPhase,
expectDeclarationsRemovingPhase,
stripTypeAliasDeclarationsPhase,
arrayConstructorPhase,
@@ -14,12 +14,10 @@ import org.jetbrains.kotlin.config.languageVersionSettings
import org.jetbrains.kotlin.ir.backend.js.lower.generateTests
import org.jetbrains.kotlin.ir.backend.js.lower.moveBodilessDeclarationsToSeparatePlace
import org.jetbrains.kotlin.ir.backend.js.transformers.irToJs.IrModuleToJsTransformer
import org.jetbrains.kotlin.ir.backend.js.utils.JsMainFunctionDetector
import org.jetbrains.kotlin.ir.backend.js.utils.NameTables
import org.jetbrains.kotlin.ir.declarations.IrModuleFragment
import org.jetbrains.kotlin.ir.declarations.StageController
import org.jetbrains.kotlin.ir.declarations.stageController
import org.jetbrains.kotlin.ir.util.DeclarationStubGenerator
import org.jetbrains.kotlin.ir.util.ExternalDependenciesGenerator
import org.jetbrains.kotlin.ir.util.noUnboundLeft
import org.jetbrains.kotlin.library.KotlinLibrary
@@ -27,11 +25,13 @@ import org.jetbrains.kotlin.library.resolver.KotlinLibraryResolveResult
import org.jetbrains.kotlin.name.FqName
class CompilerResult(
val jsCode: String?,
val dceJsCode: String?,
val jsCode: JsCode?,
val dceJsCode: JsCode?,
val tsDefinitions: String? = null
)
class JsCode(val mainModule: String, val dependencies: Iterable<Pair<String, String>> = emptyList())
fun compile(
project: Project,
mainModule: MainModule,
@@ -45,7 +45,9 @@ fun compile(
generateFullJs: Boolean = true,
generateDceJs: Boolean = false,
dceDriven: Boolean = false,
es6mode: Boolean = false
es6mode: Boolean = false,
multiModule: Boolean = false,
relativeRequirePath: Boolean = false
): CompilerResult {
stageController = object : StageController {}
@@ -54,19 +56,17 @@ fun compile(
val moduleDescriptor = moduleFragment.descriptor
val mainFunction = JsMainFunctionDetector.getMainFunctionOrNull(moduleFragment)
val context = JsIrBackendContext(moduleDescriptor, irBuiltIns, symbolTable, moduleFragment, exportedDeclarations, configuration, es6mode = es6mode)
// Load declarations referenced during `context` initialization
val irProviders = listOf(deserializer)
ExternalDependenciesGenerator(symbolTable, irProviders, configuration.languageVersionSettings).generateUnboundSymbolsAsDependencies()
val allModules = when (mainModule) {
is MainModule.SourceFiles -> dependencyModules + listOf(moduleFragment)
is MainModule.Klib -> dependencyModules
}
val context = JsIrBackendContext(moduleDescriptor, irBuiltIns, symbolTable, allModules.first(), exportedDeclarations, configuration, es6mode = es6mode)
// Load declarations referenced during `context` initialization
val irProviders = listOf(deserializer)
ExternalDependenciesGenerator(symbolTable, irProviders, configuration.languageVersionSettings).generateUnboundSymbolsAsDependencies()
deserializer.postProcess()
symbolTable.noUnboundLeft("Unbound symbols at the end of linker")
@@ -74,32 +74,42 @@ fun compile(
moveBodilessDeclarationsToSeparatePlace(context, module)
}
// TODO should be done incrementally
generateTests(context, allModules.last())
if (dceDriven) {
// TODO we should only generate tests for the current module
// TODO should be done incrementally
allModules.forEach { module ->
generateTests(context, module)
}
val controller = MutableController(context, pirLowerings)
stageController = controller
controller.currentStage = controller.lowerings.size + 1
eliminateDeadDeclarations(allModules, context, mainFunction)
eliminateDeadDeclarations(allModules, context)
// TODO investigate whether this is needed anymore
stageController = object : StageController {
override val currentStage: Int = controller.currentStage
}
val transformer = IrModuleToJsTransformer(context, mainFunction, mainArguments)
return transformer.generateModule(allModules, fullJs = true, dceJs = false)
val transformer = IrModuleToJsTransformer(
context,
mainArguments,
fullJs = true,
dceJs = false,
multiModule = multiModule,
relativeRequirePath = relativeRequirePath
)
return transformer.generateModule(allModules)
} else {
jsPhases.invokeToplevel(phaseConfig, context, allModules)
val transformer = IrModuleToJsTransformer(context, mainFunction, mainArguments)
return transformer.generateModule(allModules, generateFullJs, generateDceJs)
val transformer = IrModuleToJsTransformer(
context,
mainArguments,
fullJs = generateFullJs,
dceJs = generateDceJs,
multiModule = multiModule,
relativeRequirePath = relativeRequirePath
)
return transformer.generateModule(allModules)
}
}
@@ -111,6 +121,6 @@ fun generateJsCode(
moveBodilessDeclarationsToSeparatePlace(context, moduleFragment)
jsPhases.invokeToplevel(PhaseConfig(jsPhases), context, listOf(moduleFragment))
val transformer = IrModuleToJsTransformer(context, null, null, true, nameTables)
return transformer.generateModule(listOf(moduleFragment)).jsCode!!
val transformer = IrModuleToJsTransformer(context, null, true, nameTables)
return transformer.generateModule(listOf(moduleFragment)).jsCode!!.mainModule
}
@@ -8,11 +8,13 @@ package org.jetbrains.kotlin.ir.backend.js.export
import org.jetbrains.kotlin.ir.declarations.IrClass
import org.jetbrains.kotlin.ir.declarations.IrProperty
import org.jetbrains.kotlin.ir.declarations.IrSimpleFunction
import org.jetbrains.kotlin.serialization.js.ModuleKind
sealed class ExportedDeclaration
data class ExportedModule(
val name: String,
val moduleKind: ModuleKind,
val declarations: List<ExportedDeclaration>
)
@@ -11,7 +11,6 @@ import org.jetbrains.kotlin.descriptors.ClassKind
import org.jetbrains.kotlin.descriptors.Modality
import org.jetbrains.kotlin.descriptors.Visibilities
import org.jetbrains.kotlin.ir.backend.js.*
import org.jetbrains.kotlin.ir.backend.js.lower.ES6AddInternalParametersToConstructorPhase
import org.jetbrains.kotlin.ir.backend.js.lower.ES6AddInternalParametersToConstructorPhase.*
import org.jetbrains.kotlin.ir.backend.js.utils.getJsNameOrKotlinName
import org.jetbrains.kotlin.ir.backend.js.utils.isJsExport
@@ -22,11 +21,12 @@ import org.jetbrains.kotlin.ir.symbols.IrClassifierSymbol
import org.jetbrains.kotlin.ir.symbols.IrTypeParameterSymbol
import org.jetbrains.kotlin.ir.types.*
import org.jetbrains.kotlin.ir.util.*
import org.jetbrains.kotlin.js.config.JSConfigurationKeys
import org.jetbrains.kotlin.utils.addIfNotNull
class ExportModelGenerator(val context: JsIrBackendContext) {
private fun generateExport(file: IrPackageFragment): List<ExportedDeclaration> {
fun generateExport(file: IrPackageFragment): List<ExportedDeclaration> {
val namespaceFqName = file.fqName
val exports = file.declarations.flatMap { declaration -> listOfNotNull(exportDeclaration(declaration)) }
return when {
@@ -38,7 +38,8 @@ class ExportModelGenerator(val context: JsIrBackendContext) {
fun generateExport(modules: Iterable<IrModuleFragment>): ExportedModule =
ExportedModule(
sanitizeName(context.configuration[CommonConfigurationKeys.MODULE_NAME]!!),
context.configuration[CommonConfigurationKeys.MODULE_NAME]!!,
context.configuration[JSConfigurationKeys.MODULE_KIND]!!,
(context.externalPackageFragment.values + modules.flatMap { it.files }).flatMap {
generateExport(it)
}
@@ -5,12 +5,14 @@
package org.jetbrains.kotlin.ir.backend.js.export
import org.jetbrains.kotlin.ir.backend.js.utils.sanitizeName
// TODO: Support module kinds other than plain
fun ExportedModule.toTypeScript(): String {
val prefix = " type Nullable<T> = T | null | undefined\n"
val body = declarations.joinToString("\n") { it.toTypeScript(" ") }
return "declare namespace $name {\n$prefix$body\n}\n"
return "declare namespace ${sanitizeName(name)} {\n$prefix$body\n}\n"
}
fun List<ExportedDeclaration>.toTypeScript(indent: String): String =
@@ -21,6 +21,7 @@ import org.jetbrains.kotlin.ir.expressions.impl.IrConstImpl
import org.jetbrains.kotlin.ir.expressions.impl.IrFunctionExpressionImpl
import org.jetbrains.kotlin.ir.types.IrSimpleType
import org.jetbrains.kotlin.ir.types.classOrNull
import org.jetbrains.kotlin.ir.util.file
import org.jetbrains.kotlin.ir.util.render
import org.jetbrains.kotlin.ir.visitors.IrElementTransformerVoid
import org.jetbrains.kotlin.ir.visitors.transformChildrenVoid
@@ -28,7 +29,6 @@ import org.jetbrains.kotlin.name.Name
class PropertyReferenceLowering(private val context: JsIrBackendContext) : BodyLoweringPass {
private val implicitDeclarationFile = context.implicitDeclarationFile
private val referenceBuilderSymbol = context.kpropertyBuilder
private val localDelegateBuilderSymbol = context.klocalDelegateBuilder
private val jsClassSymbol = context.intrinsics.jsClass
@@ -40,7 +40,11 @@ class PropertyReferenceLowering(private val context: JsIrBackendContext) : BodyL
override fun lower(irBody: IrBody, container: IrDeclaration) {
newDeclarations.clear()
irBody.transformChildrenVoid(PropertyReferenceTransformer())
implicitDeclarationFile.declarations.addAll(newDeclarations)
if (!newDeclarations.isEmpty()) {
val file = container.file
newDeclarations.forEach { it.parent = file }
file.declarations.addAll(newDeclarations)
}
}
private inner class PropertyReferenceTransformer : IrElementTransformerVoid() {
@@ -55,8 +59,6 @@ class PropertyReferenceLowering(private val context: JsIrBackendContext) : BodyL
name = Name.identifier("${property.name.asString()}\$factory")
}
factoryDeclaration.parent = implicitDeclarationFile
val boundArguments = listOfNotNull(reference.dispatchReceiver, reference.extensionReceiver)
val valueParameters = ArrayList<IrValueParameter>(boundArguments.size)
@@ -22,14 +22,14 @@ import org.jetbrains.kotlin.ir.util.isEffectivelyExternal
import org.jetbrains.kotlin.name.FqName
fun generateTests(context: JsIrBackendContext, moduleFragment: IrModuleFragment) {
val generator = TestGenerator(context)
val generator = TestGenerator(context) { context.createTestContainerFun(moduleFragment) }
moduleFragment.files.forEach {
moduleFragment.files.toList().forEach {
generator.lower(it)
}
}
class TestGenerator(val context: JsIrBackendContext) : FileLoweringPass {
class TestGenerator(val context: JsIrBackendContext, val testContainerFactory: () -> IrSimpleFunction) : FileLoweringPass {
override fun lower(irFile: IrFile) {
irFile.declarations.forEach {
@@ -44,7 +44,7 @@ class TestGenerator(val context: JsIrBackendContext) : FileLoweringPass {
private val packageSuites = mutableMapOf<FqName, FunctionWithBody>()
private fun suiteForPackage(fqName: FqName) = packageSuites.getOrPut(fqName) {
context.suiteFun!!.createInvocation(fqName.asString(), context.testContainer)
context.suiteFun!!.createInvocation(fqName.asString(), testContainerFactory())
}
private data class FunctionWithBody(val function: IrSimpleFunction, val body: IrBlockBody)
@@ -5,8 +5,8 @@
package org.jetbrains.kotlin.ir.backend.js.transformers.irToJs
import org.jetbrains.kotlin.config.CommonConfigurationKeys
import org.jetbrains.kotlin.ir.backend.js.CompilerResult
import org.jetbrains.kotlin.ir.backend.js.JsCode
import org.jetbrains.kotlin.ir.backend.js.JsIrBackendContext
import org.jetbrains.kotlin.ir.backend.js.eliminateDeadDeclarations
import org.jetbrains.kotlin.ir.backend.js.export.ExportModelGenerator
@@ -26,16 +26,17 @@ import org.jetbrains.kotlin.utils.DFS
class IrModuleToJsTransformer(
private val backendContext: JsIrBackendContext,
private val mainFunction: IrSimpleFunction?,
private val mainArguments: List<String>?,
private val generateScriptModule: Boolean = false,
var namer: NameTables = NameTables(emptyList())
var namer: NameTables = NameTables(emptyList()),
private val fullJs: Boolean = true,
private val dceJs: Boolean = false,
private val multiModule: Boolean = false,
private val relativeRequirePath: Boolean = false
) {
val moduleName = backendContext.configuration[CommonConfigurationKeys.MODULE_NAME]!!
private val moduleKind = backendContext.configuration[JSConfigurationKeys.MODULE_KIND]!!
private val generateRegionComments = backendContext.configuration.getBoolean(JSConfigurationKeys.GENERATE_REGION_COMMENTS)
fun generateModule(modules: Iterable<IrModuleFragment>, fullJs: Boolean = true, dceJs: Boolean = false): CompilerResult {
fun generateModule(modules: Iterable<IrModuleFragment>): CompilerResult {
val additionalPackages = with(backendContext) {
externalPackageFragment.values + listOf(
bodilessBuiltInsPackageFragment,
@@ -50,6 +51,10 @@ class IrModuleToJsTransformer(
module.files.forEach { StaticMembersLowering(backendContext).lower(it) }
}
if (multiModule) {
breakCrossModuleFieldAccess(backendContext, modules)
}
modules.forEach { module ->
namer.merge(module.files, additionalPackages)
}
@@ -57,7 +62,7 @@ class IrModuleToJsTransformer(
val jsCode = if (fullJs) generateWrappedModuleBody(modules, exportedModule, namer) else null
val dceJsCode = if (dceJs) {
eliminateDeadDeclarations(modules, backendContext, mainFunction)
eliminateDeadDeclarations(modules, backendContext)
// 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())
@@ -68,11 +73,63 @@ class IrModuleToJsTransformer(
return CompilerResult(jsCode, dceJsCode, dts)
}
private fun generateWrappedModuleBody(modules: Iterable<IrModuleFragment>, exportedModule: ExportedModule, namer: NameTables): String {
val program = JsProgram()
private fun generateWrappedModuleBody(modules: Iterable<IrModuleFragment>, exportedModule: ExportedModule, namer: NameTables): JsCode {
if (multiModule) {
val nameGenerator = IrNamerImpl(
newNameTables = namer
val refInfo = buildCrossModuleReferenceInfo(modules)
val rM = modules.reversed()
val main = rM.first()
val others = rM.drop(1)
val mainModule = generateWrappedModuleBody2(
listOf(main),
others,
exportedModule,
namer,
refInfo
)
val dependencies = others.mapIndexed { index, module ->
val moduleName = sanitizeName(module.safeName)
val exportedDeclarations = ExportModelGenerator(backendContext).let { module.files.flatMap { file -> it.generateExport(file) } }
moduleName to generateWrappedModuleBody2(
listOf(module),
others.drop(index + 1),
ExportedModule(moduleName, exportedModule.moduleKind, exportedDeclarations),
namer,
refInfo
)
}.reversed()
return JsCode(mainModule, dependencies)
} else {
return JsCode(
generateWrappedModuleBody2(
modules,
emptyList(),
exportedModule,
namer,
EmptyCrossModuleReferenceInfo
)
)
}
}
private fun generateWrappedModuleBody2(
modules: Iterable<IrModuleFragment>,
dependencies: Iterable<IrModuleFragment>,
exportedModule: ExportedModule,
namer: NameTables,
refInfo: CrossModuleReferenceInfo
): String {
val nameGenerator = refInfo.withReferenceTracking(
IrNamerImpl(newNameTables = namer),
modules
)
val staticContext = JsStaticContext(
backendContext = backendContext,
@@ -83,9 +140,6 @@ class IrModuleToJsTransformer(
staticContext = staticContext
)
val rootFunction = JsFunction(program.rootScope, JsBlock(), "root function")
val internalModuleName = JsName("_")
val (importStatements, importedJsModules) =
generateImportStatements(
getNameForExternalDeclaration = { rootContext.getNameForStaticDeclaration(it) },
@@ -93,40 +147,96 @@ class IrModuleToJsTransformer(
)
val moduleBody = generateModuleBody(modules, rootContext)
val exportStatements = ExportModelToJsStatements(internalModuleName, namer)
.generateModuleExport(exportedModule)
val internalModuleName = JsName("_")
val exportStatements = ExportModelToJsStatements(internalModuleName, namer).generateModuleExport(exportedModule)
val callToMain = generateCallToMain(modules, rootContext)
val (crossModuleImports, importedKotlinModules) = generateCrossModuleImports(nameGenerator, modules, dependencies, { JsName(sanitizeName(it)) })
val crossModuleExports = generateCrossModuleExports(modules, refInfo, internalModuleName)
val program = JsProgram()
if (generateScriptModule) {
with(program.globalBlock) {
statements.addWithComment("block: imports", importStatements)
statements.addWithComment("block: imports", importStatements + crossModuleImports)
statements += moduleBody
statements.addWithComment("block: exports", exportStatements)
statements.addWithComment("block: exports", exportStatements + crossModuleExports)
}
} else {
with(rootFunction) {
val rootFunction = JsFunction(program.rootScope, JsBlock(), "root function").apply {
parameters += JsParameter(internalModuleName)
parameters += importedJsModules.map { JsParameter(it.internalName) }
parameters += (importedJsModules + importedKotlinModules).map { JsParameter(it.internalName) }
with(body) {
statements.addWithComment("block: imports", importStatements)
statements.addWithComment("block: imports", importStatements + crossModuleImports)
statements += moduleBody
statements.addWithComment("block: exports", exportStatements)
statements += generateCallToMain(rootContext)
statements.addWithComment("block: exports", exportStatements + crossModuleExports)
statements += callToMain
statements += JsReturn(internalModuleName.makeRef())
}
}
program.globalBlock.statements += ModuleWrapperTranslation.wrap(
moduleName,
exportedModule.name,
rootFunction,
importedJsModules,
importedJsModules + importedKotlinModules,
program,
kind = moduleKind
kind = exportedModule.moduleKind
)
}
return program.toString()
}
private fun generateCrossModuleImports(
namerWithImports: IrNamerWithImports,
currentModules: Iterable<IrModuleFragment>,
allowedDependencies: Iterable<IrModuleFragment>,
declareFreshGlobal: (String) -> JsName
): Pair<MutableList<JsStatement>, List<JsImportedModule>> {
val imports = mutableListOf<JsStatement>()
val modules = mutableListOf<JsImportedModule>()
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(moduleName.ident, moduleName, moduleName.makeRef(), relativeRequirePath)
names.forEach {
imports += JsVars(JsVars.JsVar(JsName(it), JsNameRef(it, JsNameRef("\$crossModule\$", moduleName.makeRef()))))
}
}
return imports to modules
}
private fun generateCrossModuleExports(
modules: Iterable<IrModuleFragment>,
refInfo: CrossModuleReferenceInfo,
internalModuleName: JsName
): List<JsStatement> {
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 fun generateModuleBody(modules: Iterable<IrModuleFragment>, context: JsGenerationContext): List<JsStatement> {
val statements = mutableListOf<JsStatement>().also {
if (!generateScriptModule) it += JsStringLiteral("use strict").makeStmt()
@@ -176,10 +286,12 @@ class IrModuleToJsTransformer(
statements.addWithComment("block: post-declaration", postDeclarationBlock.statements)
statements.addWithComment("block: init", context.staticContext.initializerBlock.statements)
if (backendContext.hasTests) {
statements.startRegion("block: tests")
statements += JsInvocation(context.getNameForStaticFunction(backendContext.testContainer).makeRef()).makeStmt()
statements.endRegion()
modules.forEach {
backendContext.testRoots[it]?.let { testContainer ->
statements.startRegion("block: tests")
statements += JsInvocation(context.getNameForStaticFunction(testContainer).makeRef()).makeStmt()
statements.endRegion()
}
}
return statements
@@ -198,8 +310,9 @@ class IrModuleToJsTransformer(
return listOfNotNull(mainArgumentsArray, continuation)
}
private fun generateCallToMain(rootContext: JsGenerationContext): List<JsStatement> {
private fun generateCallToMain(modules: Iterable<IrModuleFragment>, rootContext: JsGenerationContext): List<JsStatement> {
if (mainArguments == null) return emptyList() // in case `NO_MAIN` and `main(..)` exists
val mainFunction = JsMainFunctionDetector.getMainFunctionOrNull(modules.last())
return mainFunction?.let {
val jsName = rootContext.getNameForStaticFunction(it)
listOf(JsInvocation(jsName.makeRef(), generateMainArguments(it, rootContext)).makeStmt())
@@ -77,7 +77,7 @@ object ModuleWrapperTranslation {
val scope = program.scope
val defineName = scope.declareName("define")
val invocationArgs = listOf(
JsArrayLiteral(listOf(JsStringLiteral("exports")) + importedModules.map { JsStringLiteral(it.externalName) }),
JsArrayLiteral(listOf(JsStringLiteral("exports")) + importedModules.map { JsStringLiteral(it.requireName) }),
function
)
@@ -94,7 +94,7 @@ object ModuleWrapperTranslation {
val moduleName = scope.declareName("module")
val requireName = scope.declareName("require")
val invocationArgs = importedModules.map { JsInvocation(requireName.makeRef(), JsStringLiteral(it.externalName)) }
val invocationArgs = importedModules.map { JsInvocation(requireName.makeRef(), JsStringLiteral(it.requireName)) }
val invocation = JsInvocation(function, listOf(JsNameRef("exports", moduleName.makeRef())) + invocationArgs)
return listOf(invocation.makeStmt())
}
@@ -0,0 +1,217 @@
/*
* Copyright 2010-2020 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.ObsoleteDescriptorBasedAPI
import org.jetbrains.kotlin.ir.UNDEFINED_OFFSET
import org.jetbrains.kotlin.ir.backend.js.JsIrBackendContext
import org.jetbrains.kotlin.ir.backend.js.utils.IrNamer
import org.jetbrains.kotlin.ir.backend.js.utils.sanitizeName
import org.jetbrains.kotlin.ir.builders.declarations.addValueParameter
import org.jetbrains.kotlin.ir.builders.declarations.buildFun
import org.jetbrains.kotlin.ir.declarations.*
import org.jetbrains.kotlin.ir.expressions.IrCall
import org.jetbrains.kotlin.ir.expressions.IrExpression
import org.jetbrains.kotlin.ir.expressions.IrGetField
import org.jetbrains.kotlin.ir.expressions.IrSetField
import org.jetbrains.kotlin.ir.expressions.impl.*
import org.jetbrains.kotlin.ir.util.constructedClass
import org.jetbrains.kotlin.ir.util.isEffectivelyExternal
import org.jetbrains.kotlin.ir.visitors.IrElementTransformerVoid
import org.jetbrains.kotlin.ir.visitors.transformChildrenVoid
import org.jetbrains.kotlin.js.backend.ast.*
import org.jetbrains.kotlin.name.Name
interface CrossModuleReferenceInfo {
fun exports(module: IrModuleFragment): List<String>
fun withReferenceTracking(namer: IrNamer, excludedModules: Iterable<IrModuleFragment>): IrNamerWithImports
}
interface IrNamerWithImports : IrNamer {
fun imports(): List<Pair<IrModuleFragment, List<String>>>
}
object EmptyCrossModuleReferenceInfo : CrossModuleReferenceInfo {
override fun exports(module: IrModuleFragment): List<String> = emptyList()
override fun withReferenceTracking(namer: IrNamer, excludedModules: Iterable<IrModuleFragment>): IrNamerWithImports =
object : IrNamerWithImports, IrNamer by namer {
override fun imports() = emptyList<Pair<IrModuleFragment, List<String>>>()
}
}
private class CrossModuleReferenceInfoImpl(val topLevelDeclarationToModule: Map<IrDeclaration, IrModuleFragment>) :
CrossModuleReferenceInfo {
private val exportedNames: MutableMap<IrModuleFragment, MutableSet<String>> = mutableMapOf()
override fun exports(module: IrModuleFragment): List<String> {
return exportedNames[module]?.toList() ?: emptyList()
}
override fun withReferenceTracking(namer: IrNamer, excludedModules: Iterable<IrModuleFragment>): IrNamerWithImports {
val excludedModulesSet = excludedModules.toSet()
return object : IrNamerWithImports, IrNamer by namer {
override fun imports(): List<Pair<IrModuleFragment, List<String>>> {
return importedNames.entries.map { (module, names) -> module to names.toList() }
}
private val importedNames: MutableMap<IrModuleFragment, MutableSet<String>> = mutableMapOf()
private fun JsName.track(d: IrDeclaration): JsName {
topLevelDeclarationToModule[d]?.let { module ->
if (module !in excludedModulesSet) {
importedNames.getOrPut(module) { mutableSetOf() } += this.ident
exportedNames.getOrPut(module) { mutableSetOf() } += this.ident
}
}
return this
}
override fun getNameForConstructor(constructor: IrConstructor): JsName {
return namer.getNameForConstructor(constructor).track(constructor.constructedClass)
}
override fun getNameForField(field: IrField): JsName {
return namer.getNameForField(field).track(field)
}
override fun getNameForClass(klass: IrClass): JsName {
return namer.getNameForClass(klass).track(klass)
}
override fun getNameForStaticFunction(function: IrSimpleFunction): JsName {
return namer.getNameForStaticFunction(function).track(function)
}
override fun getNameForStaticDeclaration(declaration: IrDeclarationWithName): JsName {
return namer.getNameForStaticDeclaration(declaration).track(declaration)
}
override fun getNameForProperty(property: IrProperty): JsName {
return namer.getNameForProperty(property).track(property)
}
}
}
}
fun buildCrossModuleReferenceInfo(modules: Iterable<IrModuleFragment>): CrossModuleReferenceInfo {
val map = mutableMapOf<IrDeclaration, IrModuleFragment>()
modules.forEach { module ->
module.files.forEach { file ->
file.declarations.forEach { declaration ->
map[declaration] = module
}
}
}
return CrossModuleReferenceInfoImpl(map)
}
@OptIn(ObsoleteDescriptorBasedAPI::class)
fun breakCrossModuleFieldAccess(
context: JsIrBackendContext,
modules: Iterable<IrModuleFragment>
) {
val fieldToGetter = mutableMapOf<IrField, IrSimpleFunction>()
fun IrField.getter(): IrSimpleFunction {
return fieldToGetter.getOrPut(this) {
val fieldName = name
val getter = buildFun {
name = Name.identifier("get-$fieldName")
returnType = type
}
getter.body = IrBlockBodyImpl(
UNDEFINED_OFFSET, UNDEFINED_OFFSET, listOf(
IrReturnImpl(
UNDEFINED_OFFSET, UNDEFINED_OFFSET, type, getter.symbol,
IrGetFieldImpl(UNDEFINED_OFFSET, UNDEFINED_OFFSET, symbol, type)
)
)
)
getter.parent = parent
(parent as IrDeclarationContainer).declarations += getter
getter
}
}
val fieldToSetter = mutableMapOf<IrField, IrSimpleFunction>()
fun IrField.setter(): IrSimpleFunction {
return fieldToSetter.getOrPut(this) {
val fieldName = name
val setter = buildFun {
name = Name.identifier("set-$fieldName")
returnType = context.irBuiltIns.unitType
}
val param = setter.addValueParameter("value", type)
setter.body = IrBlockBodyImpl(
UNDEFINED_OFFSET, UNDEFINED_OFFSET, listOf(
IrSetFieldImpl(UNDEFINED_OFFSET, UNDEFINED_OFFSET, symbol, type).apply {
value = IrGetValueImpl(UNDEFINED_OFFSET, UNDEFINED_OFFSET, param.symbol)
}
)
)
setter.parent = parent
(parent as IrDeclarationContainer).declarations += setter
setter
}
}
modules.reversed().forEach { module ->
val moduleFields = module.files.flatMap { it.declarations.filterIsInstance<IrField>() }.toSet()
fun IrField.transformAccess(fn: IrField.() -> IrCall): IrCall? {
if (parent !is IrPackageFragment || isEffectivelyExternal() || this in moduleFields) return null
return fn()
}
module.transformChildrenVoid(object : IrElementTransformerVoid() {
override fun visitGetField(expression: IrGetField): IrExpression {
expression.transformChildrenVoid(this)
return expression.symbol.owner.transformAccess {
val getter = getter()
IrCallImpl(UNDEFINED_OFFSET, UNDEFINED_OFFSET, getter.returnType, getter.symbol)
} ?: expression
}
override fun visitSetField(expression: IrSetField): IrExpression {
expression.transformChildrenVoid(this)
return expression.symbol.owner.transformAccess {
val setter = setter()
IrCallImpl(UNDEFINED_OFFSET, UNDEFINED_OFFSET, setter.returnType, setter.symbol).apply {
putValueArgument(0, expression.value)
}
} ?: expression
}
})
}
}
val IrModuleFragment.safeName: String
get() {
var result = name.asString()
if (result.startsWith('<')) result = result.substring(1)
if (result.endsWith('>')) result = result.substring(0, result.length - 1)
return sanitizeName("kotlin-$result")
}
+1
View File
@@ -11,6 +11,7 @@ where advanced options include:
Print declarations' reachability info to stdout during performing DCE
-Xir-module-name=<name> Specify a compilation module name for IR backend
-Xir-only Disables pre-IR backend
-Xir-per-module Splits generated .js per-module
-Xir-produce-js Generates JS file using IR backend. Also disables pre-IR backend
-Xir-produce-klib-dir Generate unpacked KLIB into parent directory of output JS file.
In combination with -meta-info generates both IR and pre-IR versions of library.