diff --git a/compiler/ir/backend.wasm/src/org/jetbrains/kotlin/backend/wasm/compiler.kt b/compiler/ir/backend.wasm/src/org/jetbrains/kotlin/backend/wasm/compiler.kt index f85e0b83fc9..0ac53569de7 100644 --- a/compiler/ir/backend.wasm/src/org/jetbrains/kotlin/backend/wasm/compiler.kt +++ b/compiler/ir/backend.wasm/src/org/jetbrains/kotlin/backend/wasm/compiler.kt @@ -118,7 +118,11 @@ fun WasmCompiledModuleFragment.generateJs(): String { println(valueAddr) { console.log(">>> " + importStringFromWasm(valueAddr)); - } + }, + + printError(valueAddr) { + console.error(">>> " + importStringFromWasm(valueAddr)); + }, }; function importStringFromWasm(addr) { diff --git a/libraries/stdlib/wasm/builtins/kotlin/Throwable.kt b/libraries/stdlib/wasm/builtins/kotlin/Throwable.kt new file mode 100644 index 00000000000..6f5ad876aaf --- /dev/null +++ b/libraries/stdlib/wasm/builtins/kotlin/Throwable.kt @@ -0,0 +1,34 @@ +/* + * Copyright 2010-2022 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 kotlin + +import kotlin.wasm.internal.ExternalInterfaceType +import kotlin.wasm.internal.jsToKotlinStringAdapter + +/** + * The base class for all errors and exceptions. Only instances of this class can be thrown or caught. + * + * @param message the detail message string. + * @param cause the cause of this throwable. + */ +public open class Throwable(open val message: String?, open val cause: kotlin.Throwable?) { + constructor(message: String?) : this(message, null) + + constructor(cause: Throwable?) : this(cause?.toString(), cause) + + constructor() : this(null, null) + + private val jsStack: ExternalInterfaceType = captureStackTrace() + + internal val stack: String by lazy { + jsToKotlinStringAdapter(jsStack).removePrefix("Error\n") + } + + internal var suppressedExceptionsList: MutableList? = null +} + +@JsFun("() => new Error().stack") +private external fun captureStackTrace(): ExternalInterfaceType \ No newline at end of file diff --git a/libraries/stdlib/wasm/src/kotlin/io.kt b/libraries/stdlib/wasm/src/kotlin/io.kt index df7ff268dd7..368fd523234 100644 --- a/libraries/stdlib/wasm/src/kotlin/io.kt +++ b/libraries/stdlib/wasm/src/kotlin/io.kt @@ -35,3 +35,11 @@ internal actual interface Serializable @WasmImport("runtime", "println") private fun printlnImpl(messageAddr: Int): Unit = implementedAsIntrinsic + +internal fun printError(error: Any?) { + printErrorImpl(exportString(error.toString())) +} + +@WasmImport("runtime", "printError") +private fun printErrorImpl(errorAddr: Int): Unit = + implementedAsIntrinsic diff --git a/libraries/stdlib/wasm/src/kotlin/throwableExtensions.kt b/libraries/stdlib/wasm/src/kotlin/throwableExtensions.kt index fc2ef231f66..835a2e549f4 100644 --- a/libraries/stdlib/wasm/src/kotlin/throwableExtensions.kt +++ b/libraries/stdlib/wasm/src/kotlin/throwableExtensions.kt @@ -15,15 +15,14 @@ package kotlin * - the detailed description of each throwable in the [Throwable.cause] chain. */ @SinceKotlin("1.4") -public actual fun Throwable.stackTraceToString(): String = - TODO("Implement stackTraceToString") +public actual fun Throwable.stackTraceToString(): String = ExceptionTraceBuilder().buildFor(this) /** * Prints the [detailed description][Throwable.stackTraceToString] of this throwable to console error output. */ @SinceKotlin("1.4") public actual fun Throwable.printStackTrace() { - TODO("Implement printStackTrace") + printError(this.stackTraceToString()) } /** @@ -32,14 +31,108 @@ public actual fun Throwable.printStackTrace() { */ @SinceKotlin("1.4") public actual fun Throwable.addSuppressed(exception: Throwable) { - TODO("Implement Throwable.addSuppressed") + if (this !== exception) { + val supressed = suppressedExceptionsList + if (supressed == null) { + suppressedExceptionsList = mutableListOf(exception) + } else { + supressed.add(exception) + } + } } /** * Returns a list of all exceptions that were suppressed in order to deliver this exception. */ @SinceKotlin("1.4") -public actual val Throwable.suppressedExceptions: List - get() { - TODO("Implement Throwable.suppressedExceptions") - } \ No newline at end of file +public actual val Throwable.suppressedExceptions: List get() { + return this.suppressedExceptionsList ?: emptyList() +} + +private class ExceptionTraceBuilder { + private val target = StringBuilder() + private val visited = mutableListOf() + private var topStack: String = "" + private var topStackStart: Int = 0 + + fun buildFor(exception: Throwable): String { + exception.dumpFullTrace("", "") + return target.toString() + } + + private fun hasSeen(exception: Throwable): Boolean = visited.any { it === exception } + + private fun Throwable.dumpFullTrace(indent: String, qualifier: String) { + this.dumpSelfTrace(indent, qualifier) || return + + var cause = this.cause + while (cause != null) { + cause.dumpSelfTrace(indent, "Caused by: ") || return + cause = cause.cause + } + } + + private fun Throwable.dumpSelfTrace(indent: String, qualifier: String): Boolean { + target.append(indent).append(qualifier) + val shortInfo = this.toString() + if (hasSeen(this)) { + target.append("[CIRCULAR REFERENCE, SEE ABOVE: ").append(shortInfo).append("]\n") + return false + } + visited.add(this) + + var stack = this.stack as String? + if (stack != null) { + val stackStart = stack.indexOf(shortInfo).let { if (it < 0) 0 else it + shortInfo.length } + if (stackStart == 0) target.append(shortInfo).append("\n") + if (topStack.isEmpty()) { + topStack = stack + topStackStart = stackStart + } else { + stack = dropCommonFrames(stack, stackStart) + } + if (indent.isNotEmpty()) { + // indent stack, but avoid indenting exception message lines + val messageLines = if (stackStart == 0) 0 else 1 + shortInfo.count { c -> c == '\n' } + stack.lineSequence().forEachIndexed { index: Int, line: String -> + if (index >= messageLines) target.append(indent) + target.append(line).append("\n") + } + } else { + target.append(stack).append("\n") + } + } else { + target.append(shortInfo).append("\n") + } + + val suppressed = suppressedExceptions + if (suppressed.isNotEmpty()) { + val suppressedIndent = indent + " " + for (s in suppressed) { + s.dumpFullTrace(suppressedIndent, "Suppressed: ") + } + } + return true + } + + private fun dropCommonFrames(stack: String, stackStart: Int): String { + var commonFrames: Int = 0 + var lastBreak: Int = 0 + var preLastBreak: Int = 0 + for (pos in 0 until minOf(topStack.length - topStackStart, stack.length - stackStart)) { + val c = stack[stack.lastIndex - pos] + if (c != topStack[topStack.lastIndex - pos]) break + if (c == '\n') { + commonFrames += 1 + preLastBreak = lastBreak + lastBreak = pos + } + } + if (commonFrames <= 1) return stack + while (preLastBreak > 0 && stack[stack.lastIndex - (preLastBreak - 1)] == ' ') + preLastBreak -= 1 + + // leave 1 common frame to ease matching with the top exception stack + return stack.dropLast(preLastBreak) + "... and ${commonFrames - 1} more common stack frames skipped" + } +} \ No newline at end of file diff --git a/libraries/stdlib/wasm/test/testUtils.kt b/libraries/stdlib/wasm/test/testUtils.kt index 5c787894183..a346b575744 100644 --- a/libraries/stdlib/wasm/test/testUtils.kt +++ b/libraries/stdlib/wasm/test/testUtils.kt @@ -26,8 +26,7 @@ public actual fun testOnJs(action: () -> Unit) { } // TODO: See KT-24975 public actual val isFloat32RangeEnforced: Boolean = false -// TODO: We need to implement this on wasm -actual val supportsSuppressedExceptions: Boolean get() = false +actual val supportsSuppressedExceptions: Boolean get() = true // TODO: implement named group reference in replacement expression public actual val supportsNamedCapturingGroup: Boolean get() = false