diff --git a/chasm/build.gradle.kts b/chasm/build.gradle.kts index ddfa7ecee..ac1712f20 100644 --- a/chasm/build.gradle.kts +++ b/chasm/build.gradle.kts @@ -40,6 +40,7 @@ kotlin { sourceSets { commonMain { dependencies { + api(projects.host) api(projects.stream) implementation(projects.ast) diff --git a/chasm/src/commonTest/kotlin/io/github/charlietap/chasm/integration/HostFunctionExceptionTest.kt b/chasm/src/commonTest/kotlin/io/github/charlietap/chasm/integration/HostFunctionExceptionTest.kt new file mode 100644 index 000000000..4a9bb0c7c --- /dev/null +++ b/chasm/src/commonTest/kotlin/io/github/charlietap/chasm/integration/HostFunctionExceptionTest.kt @@ -0,0 +1,59 @@ +package io.github.charlietap.chasm.integration + +import io.github.charlietap.chasm.embedding.error.ChasmError +import io.github.charlietap.chasm.embedding.fixture.publicFunctionType +import io.github.charlietap.chasm.embedding.fixture.publicImport +import io.github.charlietap.chasm.embedding.fixture.publicStore +import io.github.charlietap.chasm.embedding.function +import io.github.charlietap.chasm.embedding.shapes.ChasmResult +import io.github.charlietap.chasm.embedding.shapes.HostFunction +import io.github.charlietap.chasm.embedding.shapes.ValueType +import io.github.charlietap.chasm.executor.runtime.error.InvocationError +import io.github.charlietap.chasm.fixture.executor.runtime.store +import io.github.charlietap.chasm.host.HostFunctionException +import kotlin.test.Test +import kotlin.test.assertEquals + +class HostFunctionExceptionTest { + + @Test + fun `can run a host function that throws an exception and return a chasm error`() { + + val store = publicStore(store()) + + val functionType = publicFunctionType( + emptyList(), + listOf(ValueType.Number.I32), + ) + val reason = "Fail gracefully" + val exception = HostFunctionException(reason) + val hostFunction: HostFunction = { + throw exception + } + val functionExternal = function(store, functionType, hostFunction) + val functionImport = publicImport( + "env", + "host_function", + functionExternal, + ) + val result = testRunner( + fileName = "host_function.wasm", + fileDirectory = FILE_DIR, + functionName = "call_host_function", + store = store, + imports = listOf( + functionImport, + ), + ) + + val expected = ChasmError.ExecutionError( + InvocationError.HostFunctionError(reason).toString(), + ) + + assertEquals(ChasmResult.Error(expected), result) + } + + companion object { + private const val FILE_DIR = "src/commonTest/resources/integration/" + } +} diff --git a/chasm/src/commonTest/resources/integration/host_function.wasm b/chasm/src/commonTest/resources/integration/host_function.wasm new file mode 100644 index 000000000..075851890 Binary files /dev/null and b/chasm/src/commonTest/resources/integration/host_function.wasm differ diff --git a/chasm/src/commonTest/resources/integration/host_function.wat b/chasm/src/commonTest/resources/integration/host_function.wat new file mode 100644 index 000000000..ae51a67d3 --- /dev/null +++ b/chasm/src/commonTest/resources/integration/host_function.wat @@ -0,0 +1,5 @@ +(module + (import "env" "host_function" (func $host_function (result i32))) + (func $call_host_function (result i32) call $host_function) + (export "call_host_function" (func $call_host_function)) +) diff --git a/executor/invoker/build.gradle.kts b/executor/invoker/build.gradle.kts index dea2d6623..f2b9b6050 100644 --- a/executor/invoker/build.gradle.kts +++ b/executor/invoker/build.gradle.kts @@ -22,6 +22,7 @@ kotlin { api(libs.result) implementation(projects.executor.memory) + implementation(projects.host) implementation(projects.libs.sse2) } } diff --git a/executor/invoker/src/commonMain/kotlin/io/github/charlietap/chasm/executor/invoker/function/HostFunctionCall.kt b/executor/invoker/src/commonMain/kotlin/io/github/charlietap/chasm/executor/invoker/function/HostFunctionCall.kt index 6059b169f..7296c08a2 100644 --- a/executor/invoker/src/commonMain/kotlin/io/github/charlietap/chasm/executor/invoker/function/HostFunctionCall.kt +++ b/executor/invoker/src/commonMain/kotlin/io/github/charlietap/chasm/executor/invoker/function/HostFunctionCall.kt @@ -20,6 +20,7 @@ import io.github.charlietap.chasm.executor.runtime.value.ExecutionValue import io.github.charlietap.chasm.executor.runtime.value.NumberValue import io.github.charlietap.chasm.executor.runtime.value.ReferenceValue import io.github.charlietap.chasm.executor.runtime.value.VectorValue +import io.github.charlietap.chasm.host.HostFunctionException internal typealias HostFunctionCall = (ExecutionContext, FunctionInstance.HostFunction) -> Result @@ -39,7 +40,11 @@ internal fun HostFunctionCall( store, frame.instance, ) - val results = function.function.invoke(functionContext, params) + val results = try { + function.function.invoke(functionContext, params) + } catch (e: HostFunctionException) { + Err(InvocationError.HostFunctionError(e.reason)).bind() + } type.results.types.forEachIndexed { index, valueType -> val result = results.getOrNull(index) diff --git a/executor/runtime/src/commonMain/kotlin/io/github/charlietap/chasm/executor/runtime/error/InvocationError.kt b/executor/runtime/src/commonMain/kotlin/io/github/charlietap/chasm/executor/runtime/error/InvocationError.kt index 30e7e4fb7..fbc7b3b4d 100644 --- a/executor/runtime/src/commonMain/kotlin/io/github/charlietap/chasm/executor/runtime/error/InvocationError.kt +++ b/executor/runtime/src/commonMain/kotlin/io/github/charlietap/chasm/executor/runtime/error/InvocationError.kt @@ -117,6 +117,9 @@ sealed interface InvocationError : ModuleTrapError { val actualValue: ExecutionValue?, ) : InvocationError + @JvmInline + value class HostFunctionError(val error: String) : InvocationError + data object ProgramFinishedInconsistentState : InvocationError sealed interface Trap : InvocationError { diff --git a/host/build.gradle.kts b/host/build.gradle.kts new file mode 100644 index 000000000..f3787ff36 --- /dev/null +++ b/host/build.gradle.kts @@ -0,0 +1,21 @@ +import org.jetbrains.kotlin.gradle.dsl.JvmTarget +import org.jetbrains.kotlin.gradle.tasks.KotlinCompile + +plugins { + alias(libs.plugins.kotlin.multiplatform) + + alias(libs.plugins.conventions.kmp) + alias(libs.plugins.conventions.linting) + alias(libs.plugins.conventions.publishing) +} + +configure { + name = "host" + description = "host system interface" +} + +tasks.withType().configureEach { + compilerOptions { + jvmTarget = JvmTarget.fromTarget(libs.versions.java.bytecode.version.get()) + } +} diff --git a/host/src/commonMain/kotlin/io/github/charlietap/chasm/host/HostFunctionException.kt b/host/src/commonMain/kotlin/io/github/charlietap/chasm/host/HostFunctionException.kt new file mode 100644 index 000000000..0b6ce9686 --- /dev/null +++ b/host/src/commonMain/kotlin/io/github/charlietap/chasm/host/HostFunctionException.kt @@ -0,0 +1,3 @@ +package io.github.charlietap.chasm.host + +class HostFunctionException(val reason: String) : Exception() diff --git a/settings.gradle.kts b/settings.gradle.kts index e962699d9..05b733dc8 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -46,6 +46,8 @@ include(":executor:invoker") include(":executor:memory") include(":executor:runtime") +include(":host") + include(":libs:sse2") include(":libs:stack")