[K/N] add testing setup for header klibs and caches

^KT-65443
This commit is contained in:
Johan Bay
2023-11-29 10:26:40 +01:00
committed by Space Team
parent 48ce542e95
commit b50111cde5
14 changed files with 136 additions and 26 deletions
@@ -2,6 +2,7 @@
// Looks like the call to `k` fails as `k` is not exported from the lib module.
// IGNORE_NATIVE: cacheMode=STATIC_EVERYWHERE
// IGNORE_NATIVE: cacheMode=STATIC_PER_FILE_EVERYWHERE
// IGNORE_NATIVE: cacheMode=STATIC_USE_HEADERS_EVERYWHERE
// MODULE: lib
// FILE: Z.kt
package z
@@ -2,6 +2,7 @@
// KT-64511: lateinit is not lowered with caches
// DISABLE_NATIVE: cacheMode=STATIC_EVERYWHERE
// DISABLE_NATIVE: cacheMode=STATIC_PER_FILE_EVERYWHERE
// DISABLE_NATIVE: cacheMode=STATIC_USE_HEADERS_EVERYWHERE
// MODULE: lib
// FILE: lib.kt
@@ -1,5 +1,6 @@
// ISSUE: KT-58421
// TARGET_BACKEND: JVM
// IGNORE_NATIVE: cacheMode=STATIC_USE_HEADERS_EVERYWHERE
// MODULE: lib
// FILE: ContinuationImpl.kt
@@ -2,6 +2,7 @@
// test is disabled now because of https://youtrack.jetbrains.com/issue/KT-55426
// IGNORE_NATIVE: cacheMode=STATIC_EVERYWHERE
// IGNORE_NATIVE: cacheMode=STATIC_PER_FILE_EVERYWHERE
// IGNORE_NATIVE: cacheMode=STATIC_USE_HEADERS_EVERYWHERE
// MODULE: lib
// FILE: lib.kt
@@ -6,6 +6,7 @@
// kotlin.AssertionError: Expected <class codegen.kclass.kclass0.MainKt$1>, actual <class codegen.kclass.kclass0.box$$inlined$getHasFoo$1>.
// IGNORE_NATIVE: cacheMode=STATIC_EVERYWHERE
// IGNORE_NATIVE: cacheMode=STATIC_PER_FILE_EVERYWHERE
// IGNORE_NATIVE: cacheMode=STATIC_USE_HEADERS_EVERYWHERE
package codegen.kclass.kclass0
import kotlin.test.*
@@ -13,6 +13,8 @@ import org.jetbrains.kotlin.konan.test.blackbox.support.TestModule
import org.jetbrains.kotlin.konan.test.blackbox.support.compilation.*
import org.jetbrains.kotlin.konan.test.blackbox.support.compilation.TestCompilationArtifact.KLIB
import org.jetbrains.kotlin.konan.test.blackbox.support.compilation.TestCompilationArtifact.KLIBStaticCache
import org.jetbrains.kotlin.konan.test.blackbox.support.compilation.TestCompilationArtifact.KLIBStaticCacheImpl
import org.jetbrains.kotlin.konan.test.blackbox.support.compilation.TestCompilationArtifact.KLIBStaticCacheHeader
import org.jetbrains.kotlin.konan.test.blackbox.support.compilation.TestCompilationResult.Companion.assertSuccess
import org.jetbrains.kotlin.konan.test.blackbox.support.group.UsePartialLinkage
import org.jetbrains.kotlin.konan.test.blackbox.support.runner.TestExecutable
@@ -147,13 +149,28 @@ abstract class AbstractNativeKlibEvolutionTest : AbstractNativeSimpleTest() {
) {
val klib = module.klibFile
if (useHeaders) {
val compilation = StaticCacheCompilation(
settings = testRunSettings,
freeCompilerArgs = COMPILER_ARGS_FOR_STATIC_CACHE_AND_EXECUTABLE,
options = staticCacheCompilationOptions,
pipelineType = testRunSettings.get(),
dependencies = moduleDependencies.map {
it.klibFile.toStaticCacheArtifact().toDependency()
it.klibFile.toHeaderCacheArtifact().toDependency()
} + klib.toKlib().toDependency(),
createHeaderCache = true,
expectedArtifact = klib.toHeaderCacheArtifact()
)
compilation.trigger()
}
val compilation = StaticCacheCompilation(
settings = testRunSettings,
freeCompilerArgs = COMPILER_ARGS_FOR_STATIC_CACHE_AND_EXECUTABLE,
options = staticCacheCompilationOptions,
pipelineType = testRunSettings.get(),
dependencies = moduleDependencies.map {
if (useHeaders) it.klibFile.toHeaderCacheArtifact().toDependency() else it.klibFile.toStaticCacheArtifact().toDependency()
} + klib.toKlib().toDependency(),
expectedArtifact = klib.toStaticCacheArtifact()
)
@@ -283,6 +300,7 @@ abstract class AbstractNativeKlibEvolutionTest : AbstractNativeSimpleTest() {
private val buildDir: File get() = testRunSettings.get<Binaries>().testBinariesDir
private val useStaticCacheForUserLibraries: Boolean get() = testRunSettings.get<CacheMode>().useStaticCacheForUserLibraries
private val useHeaders: Boolean get() = testRunSettings.get<CacheMode>().useHeaders
companion object {
private val COMPILER_ARGS_FOR_KLIB = TestCompilerArgs.EMPTY
@@ -301,11 +319,16 @@ private fun File.resolveKlibFileWithVersion(moduleName: String, version: Int): F
resolveModuleWithVersion(moduleName, version).resolve("${moduleName}.klib")
private fun File.toKlib(): KLIB = KLIB(this)
private fun File.toStaticCacheArtifact() = KLIBStaticCache(
private fun File.toStaticCacheArtifact() = KLIBStaticCacheImpl(
cacheDir = parentFile.resolve(STATIC_CACHE_DIR_NAME).apply { mkdirs() },
klib = KLIB(this)
)
private fun File.toHeaderCacheArtifact() = KLIBStaticCacheHeader(
cacheDir = parentFile.resolve(HEADER_CACHE_DIR_NAME).apply { mkdirs() },
klib = KLIB(this)
)
private fun KLIB.toDependency() = ExistingDependency(this, TestCompilationDependencyType.Library)
private fun KLIB.toIncludedDependency() = ExistingDependency(this, TestCompilationDependencyType.IncludedLibrary)
private fun KLIBStaticCache.toDependency() = ExistingDependency(this, TestCompilationDependencyType.LibraryStaticCache)
@@ -209,7 +209,7 @@ abstract class AbstractNativePartialLinkageTest : AbstractNativeSimpleTest() {
private fun KLIB.toFriendDependency() = ExistingDependency(this, FriendLibrary)
private fun KLIBStaticCache.toDependency() = ExistingDependency(this, LibraryStaticCache)
private fun KLIB.toStaticCacheArtifact() = KLIBStaticCache(
private fun KLIB.toStaticCacheArtifact() = KLIBStaticCacheImpl(
cacheDir = klibFile.parentFile.resolve(STATIC_CACHE_DIR_NAME).apply { mkdirs() },
klib = this
)
@@ -201,7 +201,7 @@ internal fun AbstractNativeSimpleTest.compileToStaticCache(
this += klib.asLibraryDependency()
dependencies.mapTo(this) { it.asStaticCacheDependency() }
},
expectedArtifact = TestCompilationArtifact.KLIBStaticCache(cacheDir, klib)
expectedArtifact = TestCompilationArtifact.KLIBStaticCacheImpl(cacheDir, klib)
)
return compilation.result.assertSuccess().resultingArtifact
}
@@ -277,8 +277,10 @@ internal object NativeTestSupport {
CacheMode.Alias.STATIC_ONLY_DIST -> false
CacheMode.Alias.STATIC_EVERYWHERE -> true
CacheMode.Alias.STATIC_PER_FILE_EVERYWHERE -> true
CacheMode.Alias.STATIC_USE_HEADERS_EVERYWHERE -> true
}
val makePerFileCaches = cacheMode == CacheMode.Alias.STATIC_PER_FILE_EVERYWHERE
val useHeaders = cacheMode == CacheMode.Alias.STATIC_USE_HEADERS_EVERYWHERE
return if (defaultCache == CacheMode.Alias.NO)
CacheMode.WithoutCache
@@ -288,6 +290,7 @@ internal object NativeTestSupport {
optimizationMode,
useStaticCacheForUserLibraries,
makePerFileCaches,
useHeaders,
cacheMode
)
}
@@ -224,7 +224,6 @@ abstract class SourceBasedCompilation<A : TestCompilationArtifact>(
}
override fun applyDependencies(argsBuilder: ArgsBuilder): Unit = with(argsBuilder) {
addFlattened(dependencies.libraries) { library -> listOf("-l", library.path) }
dependencies.friends.takeIf(Collection<*>::isNotEmpty)?.let { friends ->
add("-friend-modules", friends.joinToString(File.pathSeparator) { friend -> friend.path })
}
@@ -269,15 +268,29 @@ internal class LibraryCompilation(
dependencies = CategorizedDependencies(dependencies),
expectedArtifact = expectedArtifact
) {
private val useHeaders: Boolean = settings.get<CacheMode>().useHeaders
override val binaryOptions get() = BinaryOptions.RuntimeAssertionsMode.defaultForTesting(optimizationMode, freeCompilerArgs.assertionsMode)
override fun applySpecificArgs(argsBuilder: ArgsBuilder) = with(argsBuilder) {
add(
"-produce", "library",
"-output", expectedArtifact.path
"-output", expectedArtifact.path,
)
if (useHeaders) {
add("-Xheader-klib-path=${expectedArtifact.headerKlib.path}")
}
super.applySpecificArgs(argsBuilder)
}
override fun applyDependencies(argsBuilder: ArgsBuilder): Unit = with(argsBuilder) {
super.applyDependencies(argsBuilder)
addFlattened(dependencies.libraries) { library ->
listOf(
"-l",
library.headerKlib.takeIf { useHeaders && it.exists() }?.path ?: library.path
)
}
}
}
internal class ObjCFrameworkCompilation(
@@ -317,6 +330,7 @@ internal class ObjCFrameworkCompilation(
}
override fun applyDependencies(argsBuilder: ArgsBuilder) = with(argsBuilder) {
addFlattened(dependencies.libraries) { library -> listOf("-l", library.path) }
exportedLibraries.forEach {
assertTrue(it in dependencies.libraries)
add("-Xexport-library=${it.path}")
@@ -366,6 +380,11 @@ internal class BinaryLibraryCompilation(
)
super.applySpecificArgs(argsBuilder)
}
override fun applyDependencies(argsBuilder: ArgsBuilder): Unit = with(argsBuilder) {
super.applyDependencies(argsBuilder)
addFlattened(dependencies.libraries) { library -> listOf("-l", library.path) }
}
}
internal class GivenLibraryCompilation(givenArtifact: KLIB) : TestCompilation<KLIB>() {
@@ -553,6 +572,7 @@ class ExecutableCompilation(
override fun applyDependencies(argsBuilder: ArgsBuilder): Unit = with(argsBuilder) {
super.applyDependencies(argsBuilder)
addFlattened(dependencies.libraries) { library -> listOf("-l", library.path) }
}
override fun postCompileCheck() {
@@ -600,8 +620,9 @@ internal class StaticCacheCompilation(
private val options: Options,
private val pipelineType: PipelineType,
dependencies: Iterable<TestCompilationDependency<*>>,
expectedArtifact: KLIBStaticCache,
makePerFileCacheOverride: Boolean? = null,
private val createHeaderCache: Boolean = false,
expectedArtifact: KLIBStaticCache
) : BasicCompilation<KLIBStaticCache>(
targets = settings.get(),
home = settings.get(),
@@ -626,8 +647,10 @@ internal class StaticCacheCompilation(
private val partialLinkageConfig: UsedPartialLinkageConfig = settings.get()
private val useHeaders: Boolean = settings.get<CacheMode>().useHeaders
override fun applySpecificArgs(argsBuilder: ArgsBuilder): Unit = with(argsBuilder) {
add("-produce", "static_cache")
add("-produce", if (createHeaderCache) "header_cache" else "static_cache")
pipelineType.compilerFlags.forEach { compilerFlag -> add(compilerFlag) }
when (options) {
@@ -653,9 +676,18 @@ internal class StaticCacheCompilation(
override fun applyDependencies(argsBuilder: ArgsBuilder): Unit = with(argsBuilder) {
dependencies.friends.takeIf(Collection<*>::isNotEmpty)?.let { friends ->
add("-friend-modules", friends.joinToString(File.pathSeparator) { friend -> friend.path })
add(
"-friend-modules",
friends.joinToString(File.pathSeparator) { friend ->
friend.headerKlib.takeIf { useHeaders && it.exists() }?.path ?: friend.path
})
}
addFlattened(dependencies.cachedLibraries) { lib ->
listOf(
"-l",
lib.klib.headerKlib.takeIf { useHeaders && it.exists() }?.path ?: lib.klib.path
)
}
addFlattened(dependencies.cachedLibraries) { (_, library) -> listOf("-l", library.path) }
super.applyDependencies(argsBuilder)
}
@@ -693,7 +725,7 @@ class CategorizedDependencies(uncategorizedDependencies: Iterable<TestCompilatio
}
val uniqueCacheDirs: Set<File> by lazy {
cachedLibraries.mapToSet { (libraryCacheDir, _) -> libraryCacheDir } // Avoid repeating the same directory more than once.
cachedLibraries.mapToSet { it.cacheDir } // Avoid repeating the same directory more than once.
}
private inline fun <reified A : TestCompilationArtifact, reified T : TestCompilationDependencyType<A>> Iterable<TestCompilationDependency<*>>.collectArtifacts(): List<A> {
@@ -12,10 +12,15 @@ sealed interface TestCompilationArtifact {
data class KLIB(val klibFile: File) : TestCompilationArtifact {
val path: String get() = klibFile.path
val headerKlib: File get() = klibFile.resolveSibling(klibFile.name.replaceAfterLast(".", "header.klib"))
override val logFile: File get() = klibFile.resolveSibling("${klibFile.name}.log")
}
data class KLIBStaticCache(val cacheDir: File, val klib: KLIB, val fileCheckStage: String? = null) : TestCompilationArtifact {
interface KLIBStaticCache : TestCompilationArtifact {
val cacheDir: File
val klib: KLIB
val fileCheckStage: String?
override val logFile: File get() = cacheDir.resolve("${klib.klibFile.nameWithoutExtension}-cache.log")
val fileCheckDump: File?
get() = fileCheckStage?.let {
@@ -23,6 +28,12 @@ sealed interface TestCompilationArtifact {
}
}
data class KLIBStaticCacheImpl(override val cacheDir: File, override val klib: KLIB, override val fileCheckStage: String? = null) :
KLIBStaticCache
data class KLIBStaticCacheHeader(override val cacheDir: File, override val klib: KLIB, override val fileCheckStage: String? = null) :
KLIBStaticCache
data class Executable(val executableFile: File, val fileCheckStage: String? = null) : TestCompilationArtifact {
val path: String get() = executableFile.path
override val logFile: File get() = executableFile.resolveSibling("${executableFile.name}.log")
@@ -30,24 +30,25 @@ internal class TestCompilationFactory {
private val cachedObjCFrameworkCompilations = ThreadSafeCache<ObjCFrameworkCacheKey, ObjCFrameworkCompilation>()
private val cachedBinaryLibraryCompilations = ThreadSafeCache<BinaryLibraryCacheKey, BinaryLibraryCompilation>()
private data class KlibCacheKey(val sourceModules: Set<TestModule>, val freeCompilerArgs: TestCompilerArgs)
private data class KlibCacheKey(val sourceModules: Set<TestModule>, val freeCompilerArgs: TestCompilerArgs, val useHeaders: Boolean)
private data class ExecutableCacheKey(val sourceModules: Set<TestModule>)
private data class ObjCFrameworkCacheKey(val sourceModules: Set<TestModule>)
private data class BinaryLibraryCacheKey(val sourceModules: Set<TestModule>, val kind: BinaryLibraryKind)
// A pair of compilations for a KLIB itself and for its static cache that are created together.
private data class KlibCompilations(val klib: TestCompilation<KLIB>, val staticCache: TestCompilation<KLIBStaticCache>?)
private data class KlibCompilations(val klib: TestCompilation<KLIB>, val staticCache: TestCompilation<KLIBStaticCache>?, val headerCache: TestCompilation<KLIBStaticCache>?)
private data class CompilationDependencies(
private val klibDependencies: List<CompiledDependency<KLIB>>,
private val staticCacheDependencies: List<CompiledDependency<KLIBStaticCache>>
private val staticCacheDependencies: List<CompiledDependency<KLIBStaticCache>>,
private val staticCacheHeaderDependencies: List<CompiledDependency<KLIBStaticCache>>
) {
/** Dependencies needed to compile KLIB. */
fun forKlib(): Iterable<CompiledDependency<KLIB>> = klibDependencies
/** Dependencies needed to compile KLIB static cache. */
fun forStaticCache(klib: CompiledDependency<KLIB>): Iterable<CompiledDependency<*>> =
(klibDependencies.asSequence().filter { it.type == FriendLibrary } + klib + staticCacheDependencies).asIterable()
fun forStaticCache(klib: CompiledDependency<KLIB>, useHeaders: Boolean): Iterable<CompiledDependency<*>> =
(klibDependencies.asSequence().filter { it.type == FriendLibrary } + klib + if (useHeaders) staticCacheHeaderDependencies else staticCacheDependencies).asIterable()
/** Dependencies needed to compile one-stage executable. */
fun forOneStageExecutable(): Iterable<CompiledDependency<*>> =
@@ -210,7 +211,8 @@ internal class TestCompilationFactory {
produceStaticCache: ProduceStaticCache,
settings: Settings
): KlibCompilations {
val cacheKey = KlibCacheKey(sourceModules, freeCompilerArgs)
val useHeaders: Boolean = settings.get<CacheMode>().useHeaders
val cacheKey = KlibCacheKey(sourceModules, freeCompilerArgs, useHeaders)
// Fast pass.
cachedKlibCompilations[cacheKey]?.let { return it }
@@ -223,12 +225,20 @@ internal class TestCompilationFactory {
val staticCacheArtifactAndOptions: Pair<KLIBStaticCache, StaticCacheCompilation.Options>? = when (produceStaticCache) {
is ProduceStaticCache.No -> null // No artifact means no static cache should be compiled.
is ProduceStaticCache.Yes -> KLIBStaticCache(
is ProduceStaticCache.Yes -> KLIBStaticCacheImpl(
cacheDir = settings.cacheDirForStaticCache(klibArtifact, isGivenKlibArtifact),
klib = klibArtifact
) to produceStaticCache.options
}
val headerCacheArtifactAndOptions = staticCacheArtifactAndOptions?.let {
if (!useHeaders) return@let null
KLIBStaticCacheHeader(
cacheDir = settings.cacheDirForStaticCache(klibArtifact, isGivenKlibArtifact, header = true),
klib = klibArtifact
) to it.second
}
return cachedKlibCompilations.computeIfAbsent(cacheKey) {
val (klibCompilation, makePerFileCacheOverride) = if (isGivenKlibArtifact)
GivenLibraryCompilation(klibArtifact) to false // Don't make per-file-cache from given dependencies(usually, cinterop)
@@ -274,13 +284,32 @@ internal class TestCompilationFactory {
freeCompilerArgs = freeCompilerArgs,
options = staticCacheOptions,
pipelineType = settings.get(),
dependencies = dependencies.forStaticCache(klibCompilation.asKlibDependency(type = /* does not matter in fact*/ Library)),
dependencies = dependencies.forStaticCache(
klibCompilation.asKlibDependency(type = /* does not matter in fact*/ Library),
settings.get<CacheMode>().useHeaders
),
expectedArtifact = staticCacheArtifact,
makePerFileCacheOverride = makePerFileCacheOverride,
)
}
KlibCompilations(klibCompilation, staticCacheCompilation)
val headerCacheCompilation: StaticCacheCompilation? =
headerCacheArtifactAndOptions?.let { (staticCacheArtifact, staticCacheOptions) ->
StaticCacheCompilation(
settings = settings,
freeCompilerArgs = freeCompilerArgs,
options = staticCacheOptions,
createHeaderCache = true,
pipelineType = settings.get(),
dependencies = dependencies.forStaticCache(
klibCompilation.asKlibDependency(type = /* does not matter in fact*/ Library),
settings.get<CacheMode>().useHeaders
),
expectedArtifact = staticCacheArtifact
)
}
KlibCompilations(klibCompilation, staticCacheCompilation, headerCacheCompilation)
}
}
@@ -291,6 +320,7 @@ internal class TestCompilationFactory {
): CompilationDependencies {
val klibDependencies = mutableListOf<CompiledDependency<KLIB>>()
val staticCacheDependencies = mutableListOf<CompiledDependency<KLIBStaticCache>>()
val staticCacheHeaderDependencies = mutableListOf<CompiledDependency<KLIBStaticCache>>()
val produceStaticCache = ProduceStaticCache.decideForRegularKlib(settings)
@@ -299,14 +329,16 @@ internal class TestCompilationFactory {
val klibCompilations = modulesToKlib(setOf(dependencyModule), freeCompilerArgs, produceStaticCache, settings)
klibDependencies += klibCompilations.klib.asKlibDependency(type)
if (type == Library || type == IncludedLibrary)
if (type == Library || type == IncludedLibrary) {
staticCacheDependencies.addIfNotNull(klibCompilations.staticCache?.asStaticCacheDependency())
staticCacheHeaderDependencies.addIfNotNull((klibCompilations.headerCache ?: klibCompilations.staticCache)?.asStaticCacheDependency())
}
}
sourceModules.allDependencies().collectDependencies(Library)
sourceModules.allFriends().collectDependencies(FriendLibrary)
return CompilationDependencies(klibDependencies, staticCacheDependencies)
return CompilationDependencies(klibDependencies, staticCacheDependencies, staticCacheHeaderDependencies)
}
private fun sortDependsOnTopologically(module: TestModule): List<TestModule> {
@@ -362,7 +394,7 @@ internal class TestCompilationFactory {
}
}
private fun Settings.cacheDirForStaticCache(klibArtifact: KLIB, isGivenKlibArtifact: Boolean): File {
private fun Settings.cacheDirForStaticCache(klibArtifact: KLIB, isGivenKlibArtifact: Boolean, header: Boolean = false): File {
val artifactBaseDir = if (isGivenKlibArtifact) {
// Special case for the given (external) KLIB artifacts.
get<Binaries>().givenBinariesDir
@@ -371,7 +403,7 @@ internal class TestCompilationFactory {
klibArtifact.klibFile.parentFile
}
return artifactBaseDir.resolve(STATIC_CACHE_DIR_NAME).apply { mkdirs() }
return artifactBaseDir.resolve(if (header) HEADER_CACHE_DIR_NAME else STATIC_CACHE_DIR_NAME).apply { mkdirs() }
}
private fun Settings.singleModuleArtifactFile(module: TestModule.Exclusive, extension: String): File {
@@ -210,6 +210,7 @@ sealed class CacheMode {
abstract val staticCacheForDistributionLibrariesRootDir: File?
abstract val useStaticCacheForUserLibraries: Boolean
abstract val makePerFileCaches: Boolean
abstract val useHeaders: Boolean
abstract val alias: Alias
val useStaticCacheForDistributionLibraries: Boolean get() = staticCacheForDistributionLibrariesRootDir != null
@@ -218,6 +219,7 @@ sealed class CacheMode {
override val staticCacheForDistributionLibrariesRootDir: File? get() = null
override val useStaticCacheForUserLibraries: Boolean get() = false
override val makePerFileCaches: Boolean = false
override val useHeaders = false
override val alias = Alias.NO
}
@@ -227,6 +229,7 @@ sealed class CacheMode {
optimizationMode: OptimizationMode,
override val useStaticCacheForUserLibraries: Boolean,
override val makePerFileCaches: Boolean,
override val useHeaders: Boolean,
override val alias: Alias,
) : CacheMode() {
init {
@@ -255,7 +258,7 @@ sealed class CacheMode {
}
}
enum class Alias { NO, STATIC_ONLY_DIST, STATIC_EVERYWHERE, STATIC_PER_FILE_EVERYWHERE }
enum class Alias { NO, STATIC_ONLY_DIST, STATIC_EVERYWHERE, STATIC_PER_FILE_EVERYWHERE, STATIC_USE_HEADERS_EVERYWHERE }
companion object {
fun defaultForTestTarget(distribution: Distribution, kotlinNativeTargets: KotlinNativeTargets): Alias {
@@ -33,6 +33,7 @@ internal const val SHARED_MODULES_DIR_NAME = "__shared_modules__"
internal const val GIVEN_MODULES_DIR_NAME = "__given_modules__"
internal const val STATIC_CACHE_DIR_NAME = "__static_cache__"
internal const val HEADER_CACHE_DIR_NAME = "__header_cache__"
internal fun prettyHash(hash: Int): String = hash.toUInt().toString(16).padStart(8, '0')