diff --git a/chasm/api/chasm.klib.api b/chasm/api/chasm.klib.api index 445fdbbb2..3dfde3555 100644 --- a/chasm/api/chasm.klib.api +++ b/chasm/api/chasm.klib.api @@ -471,6 +471,7 @@ final fun io.github.charlietap.chasm.embedding.reference/writeArrayElement(io.gi final fun io.github.charlietap.chasm.embedding.reference/writeStructField(io.github.charlietap.chasm.embedding.shapes/Store, io.github.charlietap.chasm.runtime.value/ReferenceValue.Struct, kotlin/Int, io.github.charlietap.chasm.runtime.value/FieldValue): io.github.charlietap.chasm.embedding.shapes/ChasmResult // io.github.charlietap.chasm.embedding.reference/writeStructField|writeStructField(io.github.charlietap.chasm.embedding.shapes.Store;io.github.charlietap.chasm.runtime.value.ReferenceValue.Struct;kotlin.Int;io.github.charlietap.chasm.runtime.value.FieldValue){}[0] final fun io.github.charlietap.chasm.embedding.table/readTable(io.github.charlietap.chasm.embedding.shapes/Store, io.github.charlietap.chasm.embedding.shapes/Table, kotlin/Int): io.github.charlietap.chasm.embedding.shapes/ChasmResult // io.github.charlietap.chasm.embedding.table/readTable|readTable(io.github.charlietap.chasm.embedding.shapes.Store;io.github.charlietap.chasm.embedding.shapes.Table;kotlin.Int){}[0] final fun io.github.charlietap.chasm.embedding.table/writeTable(io.github.charlietap.chasm.embedding.shapes/Store, io.github.charlietap.chasm.embedding.shapes/Table, kotlin/Int, io.github.charlietap.chasm.runtime.value/ReferenceValue): io.github.charlietap.chasm.embedding.shapes/ChasmResult // io.github.charlietap.chasm.embedding.table/writeTable|writeTable(io.github.charlietap.chasm.embedding.shapes.Store;io.github.charlietap.chasm.embedding.shapes.Table;kotlin.Int;io.github.charlietap.chasm.runtime.value.ReferenceValue){}[0] +final fun io.github.charlietap.chasm.embedding/addFuel(io.github.charlietap.chasm.embedding.shapes/Store, kotlin/Long): io.github.charlietap.chasm.embedding.shapes/ChasmResult // io.github.charlietap.chasm.embedding/addFuel|addFuel(io.github.charlietap.chasm.embedding.shapes.Store;kotlin.Long){}[0] final fun io.github.charlietap.chasm.embedding/component(io.github.charlietap.chasm.stream/SourceReader, io.github.charlietap.chasm.config/ComponentConfig = ...): io.github.charlietap.chasm.embedding.shapes/ChasmResult // io.github.charlietap.chasm.embedding/component|component(io.github.charlietap.chasm.stream.SourceReader;io.github.charlietap.chasm.config.ComponentConfig){}[0] final fun io.github.charlietap.chasm.embedding/component(kotlin/ByteArray, io.github.charlietap.chasm.config/ComponentConfig = ...): io.github.charlietap.chasm.embedding.shapes/ChasmResult // io.github.charlietap.chasm.embedding/component|component(kotlin.ByteArray;io.github.charlietap.chasm.config.ComponentConfig){}[0] final fun io.github.charlietap.chasm.embedding/dropInstance(io.github.charlietap.chasm.embedding.shapes/Store, io.github.charlietap.chasm.embedding.shapes/Instance): io.github.charlietap.chasm.embedding.shapes/ChasmResult // io.github.charlietap.chasm.embedding/dropInstance|dropInstance(io.github.charlietap.chasm.embedding.shapes.Store;io.github.charlietap.chasm.embedding.shapes.Instance){}[0] @@ -488,7 +489,10 @@ final fun io.github.charlietap.chasm.embedding/module(io.github.charlietap.chasm final fun io.github.charlietap.chasm.embedding/module(kotlin/ByteArray, io.github.charlietap.chasm.config/ModuleConfig = ...): io.github.charlietap.chasm.embedding.shapes/ChasmResult // io.github.charlietap.chasm.embedding/module|module(kotlin.ByteArray;io.github.charlietap.chasm.config.ModuleConfig){}[0] final fun io.github.charlietap.chasm.embedding/moduleInfo(io.github.charlietap.chasm.embedding.shapes/Module): io.github.charlietap.chasm.embedding.shapes/ModuleInfo // io.github.charlietap.chasm.embedding/moduleInfo|moduleInfo(io.github.charlietap.chasm.embedding.shapes.Module){}[0] final fun io.github.charlietap.chasm.embedding/prepareFunction(io.github.charlietap.chasm.embedding.shapes/Store, io.github.charlietap.chasm.embedding.shapes/Instance, kotlin/String): io.github.charlietap.chasm.embedding.shapes/ChasmResult // io.github.charlietap.chasm.embedding/prepareFunction|prepareFunction(io.github.charlietap.chasm.embedding.shapes.Store;io.github.charlietap.chasm.embedding.shapes.Instance;kotlin.String){}[0] +final fun io.github.charlietap.chasm.embedding/remainingFuel(io.github.charlietap.chasm.embedding.shapes/Store): io.github.charlietap.chasm.embedding.shapes/ChasmResult // io.github.charlietap.chasm.embedding/remainingFuel|remainingFuel(io.github.charlietap.chasm.embedding.shapes.Store){}[0] +final fun io.github.charlietap.chasm.embedding/resetFuel(io.github.charlietap.chasm.embedding.shapes/Store): io.github.charlietap.chasm.embedding.shapes/ChasmResult // io.github.charlietap.chasm.embedding/resetFuel|resetFuel(io.github.charlietap.chasm.embedding.shapes.Store){}[0] final fun io.github.charlietap.chasm.embedding/store(): io.github.charlietap.chasm.embedding.shapes/Store // io.github.charlietap.chasm.embedding/store|store(){}[0] +final fun io.github.charlietap.chasm.embedding/store(io.github.charlietap.chasm.config/StoreConfig): io.github.charlietap.chasm.embedding.shapes/Store // io.github.charlietap.chasm.embedding/store|store(io.github.charlietap.chasm.config.StoreConfig){}[0] final fun io.github.charlietap.chasm.embedding/table(io.github.charlietap.chasm.embedding.shapes/Store, io.github.charlietap.chasm.type/TableType, io.github.charlietap.chasm.runtime.value/ReferenceValue): io.github.charlietap.chasm.embedding.shapes/Table // io.github.charlietap.chasm.embedding/table|table(io.github.charlietap.chasm.embedding.shapes.Store;io.github.charlietap.chasm.type.TableType;io.github.charlietap.chasm.runtime.value.ReferenceValue){}[0] final fun io.github.charlietap.chasm.embedding/tag(io.github.charlietap.chasm.embedding.shapes/Store, io.github.charlietap.chasm.type/TagType): io.github.charlietap.chasm.embedding.shapes/Tag // io.github.charlietap.chasm.embedding/tag|tag(io.github.charlietap.chasm.embedding.shapes.Store;io.github.charlietap.chasm.type.TagType){}[0] final fun io.github.charlietap.chasm.embedding/validate(io.github.charlietap.chasm.embedding.shapes/Component): io.github.charlietap.chasm.embedding.shapes/ChasmResult // io.github.charlietap.chasm.embedding/validate|validate(io.github.charlietap.chasm.embedding.shapes.Component){}[0] diff --git a/chasm/api/jvm/chasm.api b/chasm/api/jvm/chasm.api index ec27f4fec..eda447bb5 100644 --- a/chasm/api/jvm/chasm.api +++ b/chasm/api/jvm/chasm.api @@ -20,6 +20,12 @@ public final class io/github/charlietap/chasm/embedding/ExportsKt { public static final fun exports (Lio/github/charlietap/chasm/embedding/shapes/Instance;)Ljava/util/List; } +public final class io/github/charlietap/chasm/embedding/FuelKt { + public static final fun addFuel (Lio/github/charlietap/chasm/embedding/shapes/Store;J)Lio/github/charlietap/chasm/embedding/shapes/ChasmResult; + public static final fun remainingFuel (Lio/github/charlietap/chasm/embedding/shapes/Store;)Lio/github/charlietap/chasm/embedding/shapes/ChasmResult; + public static final fun resetFuel (Lio/github/charlietap/chasm/embedding/shapes/Store;)Lio/github/charlietap/chasm/embedding/shapes/ChasmResult; +} + public final class io/github/charlietap/chasm/embedding/FunctionKt { public static final fun function (Lio/github/charlietap/chasm/embedding/shapes/Store;Lio/github/charlietap/chasm/type/FunctionType;Lio/github/charlietap/chasm/host/HostFunction;)Lio/github/charlietap/chasm/embedding/shapes/Function; } @@ -66,6 +72,7 @@ public final class io/github/charlietap/chasm/embedding/PrepareFunctionKt { public final class io/github/charlietap/chasm/embedding/StoreKt { public static final fun store ()Lio/github/charlietap/chasm/embedding/shapes/Store; + public static final fun store (Lio/github/charlietap/chasm/config/StoreConfig;)Lio/github/charlietap/chasm/embedding/shapes/Store; } public final class io/github/charlietap/chasm/embedding/TableKt { diff --git a/chasm/src/commonMain/kotlin/io/github/charlietap/chasm/embedding/Fuel.kt b/chasm/src/commonMain/kotlin/io/github/charlietap/chasm/embedding/Fuel.kt new file mode 100644 index 000000000..9c2d58034 --- /dev/null +++ b/chasm/src/commonMain/kotlin/io/github/charlietap/chasm/embedding/Fuel.kt @@ -0,0 +1,46 @@ +package io.github.charlietap.chasm.embedding + +import io.github.charlietap.chasm.embedding.error.ChasmError +import io.github.charlietap.chasm.embedding.shapes.ChasmResult +import io.github.charlietap.chasm.embedding.shapes.Store + +/** + * Adds [amount] units to a store created with `StoreConfig(meterFuel = true)`, saturating at + * [Long.MAX_VALUE]. Its code takes one at every function entry and loop iteration, and traps with + * `FuelExhausted` once none are left. Instantiating a module runs its start function on the same fuel. + * Call it only while none of the store's calls are running. + */ +fun addFuel( + store: Store, + amount: Long, +): ChasmResult { + val fuel = store.store.fuel + if (!fuel.metered) return unmeteredError() + if (amount < 0L) { + return ChasmResult.Error(ChasmError.ExecutionError("Fuel added must be non-negative")) + } + + fuel.remaining += amount.coerceAtMost(Long.MAX_VALUE - fuel.remaining) + return ChasmResult.Success(Unit) +} + +/** Empties a metered store's fuel; call it only while none of its calls run. */ +fun resetFuel(store: Store): ChasmResult { + val fuel = store.store.fuel + if (!fuel.metered) return unmeteredError() + + fuel.remaining = 0L + return ChasmResult.Success(Unit) +} + +/** The fuel a metered store has left; call it only while none of its calls run. */ +fun remainingFuel(store: Store): ChasmResult { + val fuel = store.store.fuel + if (!fuel.metered) return unmeteredError() + + return ChasmResult.Success(fuel.remaining) +} + +private fun unmeteredError() = ChasmResult.Error( + ChasmError.ExecutionError("Fuel metering is not enabled for this store"), +) diff --git a/chasm/src/commonMain/kotlin/io/github/charlietap/chasm/embedding/Store.kt b/chasm/src/commonMain/kotlin/io/github/charlietap/chasm/embedding/Store.kt index a1e5c71bd..03fb2900d 100644 --- a/chasm/src/commonMain/kotlin/io/github/charlietap/chasm/embedding/Store.kt +++ b/chasm/src/commonMain/kotlin/io/github/charlietap/chasm/embedding/Store.kt @@ -1,6 +1,10 @@ package io.github.charlietap.chasm.embedding +import io.github.charlietap.chasm.config.StoreConfig import io.github.charlietap.chasm.embedding.shapes.Store +import io.github.charlietap.chasm.runtime.store.Fuel import io.github.charlietap.chasm.runtime.store.Store as InternalStore fun store(): Store = Store(InternalStore()) + +fun store(config: StoreConfig): Store = Store(InternalStore(fuel = Fuel(metered = config.meterFuel))) diff --git a/chasm/src/commonTest/kotlin/io/github/charlietap/chasm/integration/FuelTest.kt b/chasm/src/commonTest/kotlin/io/github/charlietap/chasm/integration/FuelTest.kt new file mode 100644 index 000000000..d0aaa9725 --- /dev/null +++ b/chasm/src/commonTest/kotlin/io/github/charlietap/chasm/integration/FuelTest.kt @@ -0,0 +1,200 @@ +package io.github.charlietap.chasm.integration + +import com.goncalossilva.resources.Resource +import io.github.charlietap.chasm.config.StoreConfig +import io.github.charlietap.chasm.embedding.addFuel +import io.github.charlietap.chasm.embedding.error.ChasmError +import io.github.charlietap.chasm.embedding.instance +import io.github.charlietap.chasm.embedding.invoke +import io.github.charlietap.chasm.embedding.module +import io.github.charlietap.chasm.embedding.remainingFuel +import io.github.charlietap.chasm.embedding.resetFuel +import io.github.charlietap.chasm.embedding.shapes.ChasmResult +import io.github.charlietap.chasm.embedding.shapes.Instance +import io.github.charlietap.chasm.embedding.shapes.Store +import io.github.charlietap.chasm.embedding.shapes.expect +import io.github.charlietap.chasm.embedding.store +import io.github.charlietap.chasm.runtime.error.InvocationError +import io.github.charlietap.chasm.runtime.value.NumberValue +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertIs + +class FuelTest { + + @Test + fun `an unmetered store runs without fuel`() { + val (store, instance) = instantiate(meterFuel = false) + + assertEquals(ChasmResult.Success(listOf(NumberValue.I32(0))), count(store, instance, 10)) + } + + @Test + fun `the fuel api returns an error for an unmetered store and leaves its balance unchanged`() { + val (store, _) = instantiate(meterFuel = false) + + assertIs>(addFuel(store, 10)) + assertIs>(resetFuel(store)) + assertIs>(remainingFuel(store)) + assertEquals(0, store.store.fuel.remaining) + } + + @Test + fun `a fresh metered store has no fuel and traps on invocation`() { + val (store, instance) = instantiate(meterFuel = true) + + assertEquals(ChasmResult.Success(0L), remainingFuel(store)) + assertEquals(trap(InvocationError.FuelExhausted), count(store, instance, 10)) + } + + @Test + fun `a function entry and each loop iteration spend a unit`() { + val (store, instance) = instantiate(meterFuel = true) + addFuel(store, 100) + + assertEquals(ChasmResult.Success(listOf(NumberValue.I32(0))), count(store, instance, 10)) + assertEquals(ChasmResult.Success(89L), remainingFuel(store)) + } + + @Test + fun `an exactly sufficient allowance succeeds and leaves zero`() { + val (store, instance) = instantiate(meterFuel = true) + addFuel(store, 11) + + assertEquals(ChasmResult.Success(listOf(NumberValue.I32(0))), count(store, instance, 10)) + assertEquals(ChasmResult.Success(0L), remainingFuel(store)) + + addFuel(store, 10) + assertEquals(trap(InvocationError.FuelExhausted), count(store, instance, 10)) + assertEquals(ChasmResult.Success(0L), remainingFuel(store)) + } + + @Test + fun `adding fuel preserves the existing balance`() { + val (store, _) = instantiate(meterFuel = true) + + assertEquals(ChasmResult.Success(Unit), addFuel(store, 40)) + assertEquals(ChasmResult.Success(Unit), addFuel(store, 2)) + assertEquals(ChasmResult.Success(42L), remainingFuel(store)) + + assertEquals(ChasmResult.Success(Unit), addFuel(store, 0)) + assertEquals(ChasmResult.Success(42L), remainingFuel(store)) + } + + @Test + fun `adding fuel saturates at the maximum`() { + val (store, _) = instantiate(meterFuel = true) + + addFuel(store, 42) + assertEquals(ChasmResult.Success(Unit), addFuel(store, Long.MAX_VALUE - 42)) + assertEquals(ChasmResult.Success(Long.MAX_VALUE), remainingFuel(store)) + + assertEquals(ChasmResult.Success(Unit), addFuel(store, Long.MAX_VALUE)) + assertEquals(ChasmResult.Success(Long.MAX_VALUE), remainingFuel(store)) + } + + @Test + fun `adding negative fuel returns an error and leaves the balance unchanged`() { + val (store, _) = instantiate(meterFuel = true) + addFuel(store, 42) + + assertIs>(addFuel(store, -1)) + assertEquals(ChasmResult.Success(42L), remainingFuel(store)) + } + + @Test + fun `resetting fuel empties the balance`() { + val (store, instance) = instantiate(meterFuel = true) + addFuel(store, 42) + + assertEquals(ChasmResult.Success(Unit), resetFuel(store)) + assertEquals(ChasmResult.Success(0L), remainingFuel(store)) + assertEquals(trap(InvocationError.FuelExhausted), count(store, instance, 1)) + } + + @Test + fun `a loop traps once the fuel runs out`() { + val (store, instance) = instantiate(meterFuel = true) + addFuel(store, 1_000) + + assertEquals(trap(InvocationError.FuelExhausted), invoke(store, instance, "spin")) + assertEquals(ChasmResult.Success(0L), remainingFuel(store)) + } + + @Test + fun `adding fuel after exhaustion lets a finite function complete`() { + val (store, instance) = instantiate(meterFuel = true) + addFuel(store, 5) + assertEquals(trap(InvocationError.FuelExhausted), count(store, instance, 10)) + + addFuel(store, 11) + assertEquals(ChasmResult.Success(listOf(NumberValue.I32(0))), count(store, instance, 10)) + assertEquals(ChasmResult.Success(0L), remainingFuel(store)) + } + + @Test + fun `recursion traps once the fuel runs out`() { + val (store, instance) = instantiate(meterFuel = true) + addFuel(store, 100) + + assertEquals(trap(InvocationError.FuelExhausted), invoke(store, instance, "recurse")) + } + + @Test + fun `tail calls and indirect calls trap once the fuel runs out`() { + val (store, instance) = instantiate(meterFuel = true) + addFuel(store, 100) + assertEquals(trap(InvocationError.FuelExhausted), invoke(store, instance, "ping")) + + addFuel(store, 100) + assertEquals(trap(InvocationError.FuelExhausted), invoke(store, instance, "indirect")) + } + + @Test + fun `a looping start function exhausts fuel during instantiation`() { + val store = store(StoreConfig(meterFuel = true)) + addFuel(store, 1_000) + val module = module(Resource(FILE_DIR + START_FIXTURE).readBytes()).expect("module decodes") + + assertEquals(trap(InvocationError.FuelExhausted), instance(store, module, emptyList())) + assertEquals(ChasmResult.Success(0L), remainingFuel(store)) + } + + @Test + fun `a metered store compiles a check per function and loop and an unmetered store compiles none`() { + val (metered, _) = instantiate(meterFuel = true) + val (unmetered, _) = instantiate(meterFuel = false) + + // Six functions and two loops. + assertEquals(8, metered.store.program.size - unmetered.store.program.size) + } + + @Test + fun `a module large enough to compile in parallel is metered throughout`() { + val functions = 400 + val (metered, instance) = instantiate(meterFuel = true, fileName = PARALLEL_FIXTURE) + val (unmetered, _) = instantiate(meterFuel = false, fileName = PARALLEL_FIXTURE) + + assertEquals(2 * functions, metered.store.program.size - unmetered.store.program.size) + addFuel(metered, 1_000) + assertEquals(trap(InvocationError.FuelExhausted), invoke(metered, instance, "last")) + } + + private fun count(store: Store, instance: Instance, times: Int) = + invoke(store, instance, "count", listOf(NumberValue.I32(times))) + + private fun instantiate(meterFuel: Boolean, fileName: String = FUEL_FIXTURE): Pair { + val store = store(StoreConfig(meterFuel = meterFuel)) + val module = module(Resource(FILE_DIR + fileName).readBytes()).expect("module decodes") + return store to instance(store, module, emptyList()).expect("module instantiates") + } + + private fun trap(error: InvocationError) = ChasmResult.Error(ChasmError.ExecutionError(error.toString())) + + companion object { + private const val FILE_DIR = "integration/" + private const val FUEL_FIXTURE = "fuel.wasm" + private const val START_FIXTURE = "fuel_start.wasm" + private const val PARALLEL_FIXTURE = "fuel_parallel.wasm" + } +} diff --git a/chasm/src/commonTest/resources/integration/fuel.wasm b/chasm/src/commonTest/resources/integration/fuel.wasm new file mode 100644 index 000000000..af461ab75 Binary files /dev/null and b/chasm/src/commonTest/resources/integration/fuel.wasm differ diff --git a/chasm/src/commonTest/resources/integration/fuel.wat b/chasm/src/commonTest/resources/integration/fuel.wat new file mode 100644 index 000000000..7d9f1577f --- /dev/null +++ b/chasm/src/commonTest/resources/integration/fuel.wat @@ -0,0 +1,26 @@ +(module + (type $void (func)) + (table 1 funcref) + (elem (i32.const 0) $indirect) + + (func $spin (export "spin") + (loop $continue + (br $continue))) + + (func $count (export "count") (param $n i32) (result i32) + (loop $continue + (local.set $n (i32.sub (local.get $n) (i32.const 1))) + (br_if $continue (local.get $n))) + (local.get $n)) + + (func $recurse (export "recurse") + (call $recurse)) + + (func $ping (export "ping") + (return_call $pong)) + + (func $pong + (return_call $ping)) + + (func $indirect (export "indirect") + (call_indirect (type $void) (i32.const 0)))) diff --git a/chasm/src/commonTest/resources/integration/fuel_parallel.wasm b/chasm/src/commonTest/resources/integration/fuel_parallel.wasm new file mode 100644 index 000000000..fc2e69a52 Binary files /dev/null and b/chasm/src/commonTest/resources/integration/fuel_parallel.wasm differ diff --git a/chasm/src/commonTest/resources/integration/fuel_parallel.wat b/chasm/src/commonTest/resources/integration/fuel_parallel.wat new file mode 100644 index 000000000..82e92f245 --- /dev/null +++ b/chasm/src/commonTest/resources/integration/fuel_parallel.wat @@ -0,0 +1,401 @@ +(module + (func (loop $continue (br $continue))) + (func (loop $continue (br $continue))) + (func (loop $continue (br $continue))) + (func (loop $continue (br $continue))) + (func (loop $continue (br $continue))) + (func (loop $continue (br $continue))) + (func (loop $continue (br $continue))) + (func (loop $continue (br $continue))) + (func (loop $continue (br $continue))) + (func (loop $continue (br $continue))) + (func (loop $continue (br $continue))) + (func (loop $continue (br $continue))) + (func (loop $continue (br $continue))) + (func (loop $continue (br $continue))) + (func (loop $continue (br $continue))) + (func (loop $continue (br $continue))) + (func (loop $continue (br $continue))) + (func (loop $continue (br $continue))) + (func (loop $continue (br $continue))) + (func (loop $continue (br $continue))) + (func (loop $continue (br $continue))) + (func (loop $continue (br $continue))) + (func (loop $continue (br $continue))) + (func (loop $continue (br $continue))) + (func (loop $continue (br $continue))) + (func (loop $continue (br $continue))) + (func (loop $continue (br $continue))) + (func (loop $continue (br $continue))) + (func (loop $continue (br $continue))) + (func (loop $continue (br $continue))) + (func (loop $continue (br $continue))) + (func (loop $continue (br $continue))) + (func (loop $continue (br $continue))) + (func (loop $continue (br $continue))) + (func (loop $continue (br $continue))) + (func (loop $continue (br $continue))) + (func (loop $continue (br $continue))) + (func (loop $continue (br $continue))) + (func (loop $continue (br $continue))) + (func (loop $continue (br $continue))) + (func (loop $continue (br $continue))) + (func (loop $continue (br $continue))) + (func (loop $continue (br $continue))) + (func (loop $continue (br $continue))) + (func (loop $continue (br $continue))) + (func (loop $continue (br $continue))) + (func (loop $continue (br $continue))) + (func (loop $continue (br $continue))) + (func (loop $continue (br $continue))) + (func (loop $continue (br $continue))) + (func (loop $continue (br $continue))) + (func (loop $continue (br $continue))) + (func (loop $continue (br $continue))) + (func (loop $continue (br $continue))) + (func (loop $continue (br $continue))) + (func (loop $continue (br $continue))) + (func (loop $continue (br $continue))) + (func (loop $continue (br $continue))) + (func (loop $continue (br $continue))) + (func (loop $continue (br $continue))) + (func (loop $continue (br $continue))) + (func (loop $continue (br $continue))) + (func (loop $continue (br $continue))) + (func (loop $continue (br $continue))) + (func (loop $continue (br $continue))) + (func (loop $continue (br $continue))) + (func (loop $continue (br $continue))) + (func (loop $continue (br $continue))) + (func (loop $continue (br $continue))) + (func (loop $continue (br $continue))) + (func (loop $continue (br $continue))) + (func (loop $continue (br $continue))) + (func (loop $continue (br $continue))) + (func (loop $continue (br $continue))) + (func (loop $continue (br $continue))) + (func (loop $continue (br $continue))) + (func (loop $continue (br $continue))) + (func (loop $continue (br $continue))) + (func (loop $continue (br $continue))) + (func (loop $continue (br $continue))) + (func (loop $continue (br $continue))) + (func (loop $continue (br $continue))) + (func (loop $continue (br $continue))) + (func (loop $continue (br $continue))) + (func (loop $continue (br $continue))) + (func (loop $continue (br $continue))) + (func (loop $continue (br $continue))) + (func (loop $continue (br $continue))) + (func (loop $continue (br $continue))) + (func (loop $continue (br $continue))) + (func (loop $continue (br $continue))) + (func (loop $continue (br $continue))) + (func (loop $continue (br $continue))) + (func (loop $continue (br $continue))) + (func (loop $continue (br $continue))) + (func (loop $continue (br $continue))) + (func (loop $continue (br $continue))) + (func (loop $continue (br $continue))) + (func (loop $continue (br $continue))) + (func (loop $continue (br $continue))) + (func (loop $continue (br $continue))) + (func (loop $continue (br $continue))) + (func (loop $continue (br $continue))) + (func (loop $continue (br $continue))) + (func (loop $continue (br $continue))) + (func (loop $continue (br $continue))) + (func (loop $continue (br $continue))) + (func (loop $continue (br $continue))) + (func (loop $continue (br $continue))) + (func (loop $continue (br $continue))) + (func (loop $continue (br $continue))) + (func (loop $continue (br $continue))) + (func (loop $continue (br $continue))) + (func (loop $continue (br $continue))) + (func (loop $continue (br $continue))) + (func (loop $continue (br $continue))) + (func (loop $continue (br $continue))) + (func (loop $continue (br $continue))) + (func (loop $continue (br $continue))) + (func (loop $continue (br $continue))) + (func (loop $continue (br $continue))) + (func (loop $continue (br $continue))) + (func (loop $continue (br $continue))) + (func (loop $continue (br $continue))) + (func (loop $continue (br $continue))) + (func (loop $continue (br $continue))) + (func (loop $continue (br $continue))) + (func (loop $continue (br $continue))) + (func (loop $continue (br $continue))) + (func (loop $continue (br $continue))) + (func (loop $continue (br $continue))) + (func (loop $continue (br $continue))) + (func (loop $continue (br $continue))) + (func (loop $continue (br $continue))) + (func (loop $continue (br $continue))) + (func (loop $continue (br $continue))) + (func (loop $continue (br $continue))) + (func (loop $continue (br $continue))) + (func (loop $continue (br $continue))) + (func (loop $continue (br $continue))) + (func (loop $continue (br $continue))) + (func (loop $continue (br $continue))) + (func (loop $continue (br $continue))) + (func (loop $continue (br $continue))) + (func (loop $continue (br $continue))) + (func (loop $continue (br $continue))) + (func (loop $continue (br $continue))) + (func (loop $continue (br $continue))) + (func (loop $continue (br $continue))) + (func (loop $continue (br $continue))) + (func (loop $continue (br $continue))) + (func (loop $continue (br $continue))) + (func (loop $continue (br $continue))) + (func (loop $continue (br $continue))) + (func (loop $continue (br $continue))) + (func (loop $continue (br $continue))) + (func (loop $continue (br $continue))) + (func (loop $continue (br $continue))) + (func (loop $continue (br $continue))) + (func (loop $continue (br $continue))) + (func (loop $continue (br $continue))) + (func (loop $continue (br $continue))) + (func (loop $continue (br $continue))) + (func (loop $continue (br $continue))) + (func (loop $continue (br $continue))) + (func (loop $continue (br $continue))) + (func (loop $continue (br $continue))) + (func (loop $continue (br $continue))) + (func (loop $continue (br $continue))) + (func (loop $continue (br $continue))) + (func (loop $continue (br $continue))) + (func (loop $continue (br $continue))) + (func (loop $continue (br $continue))) + (func (loop $continue (br $continue))) + (func (loop $continue (br $continue))) + (func (loop $continue (br $continue))) + (func (loop $continue (br $continue))) + (func (loop $continue (br $continue))) + (func (loop $continue (br $continue))) + (func (loop $continue (br $continue))) + (func (loop $continue (br $continue))) + (func (loop $continue (br $continue))) + (func (loop $continue (br $continue))) + (func (loop $continue (br $continue))) + (func (loop $continue (br $continue))) + (func (loop $continue (br $continue))) + (func (loop $continue (br $continue))) + (func (loop $continue (br $continue))) + (func (loop $continue (br $continue))) + (func (loop $continue (br $continue))) + (func (loop $continue (br $continue))) + (func (loop $continue (br $continue))) + (func (loop $continue (br $continue))) + (func (loop $continue (br $continue))) + (func (loop $continue (br $continue))) + (func (loop $continue (br $continue))) + (func (loop $continue (br $continue))) + (func (loop $continue (br $continue))) + (func (loop $continue (br $continue))) + (func (loop $continue (br $continue))) + (func (loop $continue (br $continue))) + (func (loop $continue (br $continue))) + (func (loop $continue (br $continue))) + (func (loop $continue (br $continue))) + (func (loop $continue (br $continue))) + (func (loop $continue (br $continue))) + (func (loop $continue (br $continue))) + (func (loop $continue (br $continue))) + (func (loop $continue (br $continue))) + (func (loop $continue (br $continue))) + (func (loop $continue (br $continue))) + (func (loop $continue (br $continue))) + (func (loop $continue (br $continue))) + (func (loop $continue (br $continue))) + (func (loop $continue (br $continue))) + (func (loop $continue (br $continue))) + (func (loop $continue (br $continue))) + (func (loop $continue (br $continue))) + (func (loop $continue (br $continue))) + (func (loop $continue (br $continue))) + (func (loop $continue (br $continue))) + (func (loop $continue (br $continue))) + (func (loop $continue (br $continue))) + (func (loop $continue (br $continue))) + (func (loop $continue (br $continue))) + (func (loop $continue (br $continue))) + (func (loop $continue (br $continue))) + (func (loop $continue (br $continue))) + (func (loop $continue (br $continue))) + (func (loop $continue (br $continue))) + (func (loop $continue (br $continue))) + (func (loop $continue (br $continue))) + (func (loop $continue (br $continue))) + (func (loop $continue (br $continue))) + (func (loop $continue (br $continue))) + (func (loop $continue (br $continue))) + (func (loop $continue (br $continue))) + (func (loop $continue (br $continue))) + (func (loop $continue (br $continue))) + (func (loop $continue (br $continue))) + (func (loop $continue (br $continue))) + (func (loop $continue (br $continue))) + (func (loop $continue (br $continue))) + (func (loop $continue (br $continue))) + (func (loop $continue (br $continue))) + (func (loop $continue (br $continue))) + (func (loop $continue (br $continue))) + (func (loop $continue (br $continue))) + (func (loop $continue (br $continue))) + (func (loop $continue (br $continue))) + (func (loop $continue (br $continue))) + (func (loop $continue (br $continue))) + (func (loop $continue (br $continue))) + (func (loop $continue (br $continue))) + (func (loop $continue (br $continue))) + (func (loop $continue (br $continue))) + (func (loop $continue (br $continue))) + (func (loop $continue (br $continue))) + (func (loop $continue (br $continue))) + (func (loop $continue (br $continue))) + (func (loop $continue (br $continue))) + (func (loop $continue (br $continue))) + (func (loop $continue (br $continue))) + (func (loop $continue (br $continue))) + (func (loop $continue (br $continue))) + (func (loop $continue (br $continue))) + (func (loop $continue (br $continue))) + (func (loop $continue (br $continue))) + (func (loop $continue (br $continue))) + (func (loop $continue (br $continue))) + (func (loop $continue (br $continue))) + (func (loop $continue (br $continue))) + (func (loop $continue (br $continue))) + (func (loop $continue (br $continue))) + (func (loop $continue (br $continue))) + (func (loop $continue (br $continue))) + (func (loop $continue (br $continue))) + (func (loop $continue (br $continue))) + (func (loop $continue (br $continue))) + (func (loop $continue (br $continue))) + (func (loop $continue (br $continue))) + (func (loop $continue (br $continue))) + (func (loop $continue (br $continue))) + (func (loop $continue (br $continue))) + (func (loop $continue (br $continue))) + (func (loop $continue (br $continue))) + (func (loop $continue (br $continue))) + (func (loop $continue (br $continue))) + (func (loop $continue (br $continue))) + (func (loop $continue (br $continue))) + (func (loop $continue (br $continue))) + (func (loop $continue (br $continue))) + (func (loop $continue (br $continue))) + (func (loop $continue (br $continue))) + (func (loop $continue (br $continue))) + (func (loop $continue (br $continue))) + (func (loop $continue (br $continue))) + (func (loop $continue (br $continue))) + (func (loop $continue (br $continue))) + (func (loop $continue (br $continue))) + (func (loop $continue (br $continue))) + (func (loop $continue (br $continue))) + (func (loop $continue (br $continue))) + (func (loop $continue (br $continue))) + (func (loop $continue (br $continue))) + (func (loop $continue (br $continue))) + (func (loop $continue (br $continue))) + (func (loop $continue (br $continue))) + (func (loop $continue (br $continue))) + (func (loop $continue (br $continue))) + (func (loop $continue (br $continue))) + (func (loop $continue (br $continue))) + (func (loop $continue (br $continue))) + (func (loop $continue (br $continue))) + (func (loop $continue (br $continue))) + (func (loop $continue (br $continue))) + (func (loop $continue (br $continue))) + (func (loop $continue (br $continue))) + (func (loop $continue (br $continue))) + (func (loop $continue (br $continue))) + (func (loop $continue (br $continue))) + (func (loop $continue (br $continue))) + (func (loop $continue (br $continue))) + (func (loop $continue (br $continue))) + (func (loop $continue (br $continue))) + (func (loop $continue (br $continue))) + (func (loop $continue (br $continue))) + (func (loop $continue (br $continue))) + (func (loop $continue (br $continue))) + (func (loop $continue (br $continue))) + (func (loop $continue (br $continue))) + (func (loop $continue (br $continue))) + (func (loop $continue (br $continue))) + (func (loop $continue (br $continue))) + (func (loop $continue (br $continue))) + (func (loop $continue (br $continue))) + (func (loop $continue (br $continue))) + (func (loop $continue (br $continue))) + (func (loop $continue (br $continue))) + (func (loop $continue (br $continue))) + (func (loop $continue (br $continue))) + (func (loop $continue (br $continue))) + (func (loop $continue (br $continue))) + (func (loop $continue (br $continue))) + (func (loop $continue (br $continue))) + (func (loop $continue (br $continue))) + (func (loop $continue (br $continue))) + (func (loop $continue (br $continue))) + (func (loop $continue (br $continue))) + (func (loop $continue (br $continue))) + (func (loop $continue (br $continue))) + (func (loop $continue (br $continue))) + (func (loop $continue (br $continue))) + (func (loop $continue (br $continue))) + (func (loop $continue (br $continue))) + (func (loop $continue (br $continue))) + (func (loop $continue (br $continue))) + (func (loop $continue (br $continue))) + (func (loop $continue (br $continue))) + (func (loop $continue (br $continue))) + (func (loop $continue (br $continue))) + (func (loop $continue (br $continue))) + (func (loop $continue (br $continue))) + (func (loop $continue (br $continue))) + (func (loop $continue (br $continue))) + (func (loop $continue (br $continue))) + (func (loop $continue (br $continue))) + (func (loop $continue (br $continue))) + (func (loop $continue (br $continue))) + (func (loop $continue (br $continue))) + (func (loop $continue (br $continue))) + (func (loop $continue (br $continue))) + (func (loop $continue (br $continue))) + (func (loop $continue (br $continue))) + (func (loop $continue (br $continue))) + (func (loop $continue (br $continue))) + (func (loop $continue (br $continue))) + (func (loop $continue (br $continue))) + (func (loop $continue (br $continue))) + (func (loop $continue (br $continue))) + (func (loop $continue (br $continue))) + (func (loop $continue (br $continue))) + (func (loop $continue (br $continue))) + (func (loop $continue (br $continue))) + (func (loop $continue (br $continue))) + (func (loop $continue (br $continue))) + (func (loop $continue (br $continue))) + (func (loop $continue (br $continue))) + (func (loop $continue (br $continue))) + (func (loop $continue (br $continue))) + (func (loop $continue (br $continue))) + (func (loop $continue (br $continue))) + (func (loop $continue (br $continue))) + (func (loop $continue (br $continue))) + (func (loop $continue (br $continue))) + (func (loop $continue (br $continue))) + (func (loop $continue (br $continue))) + (func (loop $continue (br $continue))) + (func (loop $continue (br $continue))) + (func (export "last") (loop $continue (br $continue)))) diff --git a/chasm/src/commonTest/resources/integration/fuel_start.wasm b/chasm/src/commonTest/resources/integration/fuel_start.wasm new file mode 100644 index 000000000..90063ab8e Binary files /dev/null and b/chasm/src/commonTest/resources/integration/fuel_start.wasm differ diff --git a/chasm/src/commonTest/resources/integration/fuel_start.wat b/chasm/src/commonTest/resources/integration/fuel_start.wat new file mode 100644 index 000000000..b23346b94 --- /dev/null +++ b/chasm/src/commonTest/resources/integration/fuel_start.wat @@ -0,0 +1,6 @@ +(module + (func $spin + (loop $continue + (br $continue))) + + (start $spin)) diff --git a/compiler/src/commonMain/kotlin/io/github/charlietap/chasm/compiler/ControlInstructionCompiler.kt b/compiler/src/commonMain/kotlin/io/github/charlietap/chasm/compiler/ControlInstructionCompiler.kt index 4ccaa9004..e3164391a 100644 --- a/compiler/src/commonMain/kotlin/io/github/charlietap/chasm/compiler/ControlInstructionCompiler.kt +++ b/compiler/src/commonMain/kotlin/io/github/charlietap/chasm/compiler/ControlInstructionCompiler.kt @@ -23,6 +23,7 @@ import io.github.charlietap.chasm.compiler.instruction.emitCall import io.github.charlietap.chasm.compiler.instruction.emitCallIndirect import io.github.charlietap.chasm.compiler.instruction.emitCallRef import io.github.charlietap.chasm.compiler.instruction.emitCopies +import io.github.charlietap.chasm.compiler.instruction.emitFuelCheck import io.github.charlietap.chasm.compiler.instruction.emitFunctionReturn import io.github.charlietap.chasm.compiler.instruction.emitJump import io.github.charlietap.chasm.compiler.instruction.emitReturnCallIndirect @@ -201,7 +202,11 @@ private fun enterBlock( } else { resultSlots } - if (kind == BlockKind.Loop) state.bind(branchTarget) + if (kind == BlockKind.Loop) { + state.bind(branchTarget) + // At the loop's branch target, so every iteration spends fuel. + state.emitFuelCheck() + } state.controls.push( kind = kind, diff --git a/compiler/src/commonMain/kotlin/io/github/charlietap/chasm/compiler/FunctionCompiler.kt b/compiler/src/commonMain/kotlin/io/github/charlietap/chasm/compiler/FunctionCompiler.kt index e1c694265..90f29fc02 100644 --- a/compiler/src/commonMain/kotlin/io/github/charlietap/chasm/compiler/FunctionCompiler.kt +++ b/compiler/src/commonMain/kotlin/io/github/charlietap/chasm/compiler/FunctionCompiler.kt @@ -25,6 +25,7 @@ import io.github.charlietap.chasm.compiler.instruction.emitCopy import io.github.charlietap.chasm.compiler.instruction.emitDeferredBranchPaths import io.github.charlietap.chasm.compiler.instruction.emitF32Constant import io.github.charlietap.chasm.compiler.instruction.emitF64Constant +import io.github.charlietap.chasm.compiler.instruction.emitFuelCheck import io.github.charlietap.chasm.compiler.instruction.emitGlobalSet import io.github.charlietap.chasm.compiler.instruction.emitI32Constant import io.github.charlietap.chasm.compiler.instruction.emitI64Constant @@ -71,6 +72,7 @@ internal fun FunctionCompiler( program = programBuilder, ) beginFunctionControl(state) + state.emitFuelCheck() var index = 0 while (index < function.body.instructions.size) { diff --git a/compiler/src/commonMain/kotlin/io/github/charlietap/chasm/compiler/context/CompilerContext.kt b/compiler/src/commonMain/kotlin/io/github/charlietap/chasm/compiler/context/CompilerContext.kt index acbd9993a..f2da504bc 100644 --- a/compiler/src/commonMain/kotlin/io/github/charlietap/chasm/compiler/context/CompilerContext.kt +++ b/compiler/src/commonMain/kotlin/io/github/charlietap/chasm/compiler/context/CompilerContext.kt @@ -16,6 +16,7 @@ import io.github.charlietap.chasm.runtime.instance.MemoryInstance import io.github.charlietap.chasm.runtime.instance.ModuleInstance import io.github.charlietap.chasm.runtime.instance.TableInstance import io.github.charlietap.chasm.runtime.instance.TagInstance +import io.github.charlietap.chasm.runtime.store.Fuel import io.github.charlietap.chasm.runtime.store.Store import io.github.charlietap.chasm.runtime.type.ModuleTypeResolver import io.github.charlietap.chasm.runtime.type.RuntimeTypeMap @@ -36,6 +37,8 @@ internal class CompilerContext( val globals: Array = emptyArray(), val elements: Array = emptyArray(), val data: Array = emptyArray(), + /** A metered store's fuel, which the checks emitted at function entries and loop headers take from. */ + val fuel: Fuel? = null, ) { val emptyBlockType: FunctionType = types.blockType(BlockType.Empty) val instructionObserver = diagnostics?.instructionObserver @@ -66,4 +69,5 @@ internal fun createCompilerContext( globals = Array(instance.globalAddresses.size) { index -> store.global(instance.globalAddresses[index]) }, elements = Array(instance.elemAddresses.size) { index -> store.element(instance.elemAddresses[index]) }, data = Array(instance.dataAddresses.size) { index -> store.data(instance.dataAddresses[index]) }, + fuel = store.fuel.takeIf { it.metered }, ) diff --git a/compiler/src/commonMain/kotlin/io/github/charlietap/chasm/compiler/instruction/AdminInstructionEmitter.kt b/compiler/src/commonMain/kotlin/io/github/charlietap/chasm/compiler/instruction/AdminInstructionEmitter.kt index 39baba0d0..c737a9a29 100644 --- a/compiler/src/commonMain/kotlin/io/github/charlietap/chasm/compiler/instruction/AdminInstructionEmitter.kt +++ b/compiler/src/commonMain/kotlin/io/github/charlietap/chasm/compiler/instruction/AdminInstructionEmitter.kt @@ -3,6 +3,7 @@ package io.github.charlietap.chasm.compiler.instruction import io.github.charlietap.chasm.compiler.context.FunctionCompilationContext import io.github.charlietap.chasm.executor.invoker.dispatch.admin.CopySlotDispatcher import io.github.charlietap.chasm.executor.invoker.dispatch.admin.CopySlotsDispatcher +import io.github.charlietap.chasm.executor.invoker.dispatch.admin.FuelCheckDispatcher import io.github.charlietap.chasm.executor.invoker.dispatch.numeric.F32ConstDispatcher import io.github.charlietap.chasm.executor.invoker.dispatch.numeric.F64ConstDispatcher import io.github.charlietap.chasm.executor.invoker.dispatch.numeric.I32ConstDispatcher @@ -63,3 +64,10 @@ internal fun FunctionCompilationContext.emitF64Constant(bits: Long, destinationS val instruction = NumericInstruction.F64ConstS(bits, destinationSlot) emit(instruction, ::F64ConstDispatcher) } + +internal fun FunctionCompilationContext.emitFuelCheck() { + val fuel = compiler.fuel ?: return + emit(AdminInstruction.FuelCheck) { + FuelCheckDispatcher(it, fuel) + } +} diff --git a/config/api/config.klib.api b/config/api/config.klib.api index 87492a086..9cfb5af22 100644 --- a/config/api/config.klib.api +++ b/config/api/config.klib.api @@ -180,3 +180,16 @@ final class io.github.charlietap.chasm.config/RuntimeConfig { // io.github.charl final fun hashCode(): kotlin/Int // io.github.charlietap.chasm.config/RuntimeConfig.hashCode|hashCode(){}[0] final fun toString(): kotlin/String // io.github.charlietap.chasm.config/RuntimeConfig.toString|toString(){}[0] } + +final class io.github.charlietap.chasm.config/StoreConfig { // io.github.charlietap.chasm.config/StoreConfig|null[0] + constructor (kotlin/Boolean = ...) // io.github.charlietap.chasm.config/StoreConfig.|(kotlin.Boolean){}[0] + + final val meterFuel // io.github.charlietap.chasm.config/StoreConfig.meterFuel|{}meterFuel[0] + final fun (): kotlin/Boolean // io.github.charlietap.chasm.config/StoreConfig.meterFuel.|(){}[0] + + final fun component1(): kotlin/Boolean // io.github.charlietap.chasm.config/StoreConfig.component1|component1(){}[0] + final fun copy(kotlin/Boolean = ...): io.github.charlietap.chasm.config/StoreConfig // io.github.charlietap.chasm.config/StoreConfig.copy|copy(kotlin.Boolean){}[0] + final fun equals(kotlin/Any?): kotlin/Boolean // io.github.charlietap.chasm.config/StoreConfig.equals|equals(kotlin.Any?){}[0] + final fun hashCode(): kotlin/Int // io.github.charlietap.chasm.config/StoreConfig.hashCode|hashCode(){}[0] + final fun toString(): kotlin/String // io.github.charlietap.chasm.config/StoreConfig.toString|toString(){}[0] +} diff --git a/config/api/jvm/config.api b/config/api/jvm/config.api index 31be6070f..443e812f2 100644 --- a/config/api/jvm/config.api +++ b/config/api/jvm/config.api @@ -158,3 +158,16 @@ public final class io/github/charlietap/chasm/config/RuntimeConfig { public fun toString ()Ljava/lang/String; } +public final class io/github/charlietap/chasm/config/StoreConfig { + public fun ()V + public fun (Z)V + public synthetic fun (ZILkotlin/jvm/internal/DefaultConstructorMarker;)V + public final fun component1 ()Z + public final fun copy (Z)Lio/github/charlietap/chasm/config/StoreConfig; + public static synthetic fun copy$default (Lio/github/charlietap/chasm/config/StoreConfig;ZILjava/lang/Object;)Lio/github/charlietap/chasm/config/StoreConfig; + public fun equals (Ljava/lang/Object;)Z + public final fun getMeterFuel ()Z + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + diff --git a/config/src/commonMain/kotlin/io/github/charlietap/chasm/config/StoreConfig.kt b/config/src/commonMain/kotlin/io/github/charlietap/chasm/config/StoreConfig.kt new file mode 100644 index 000000000..761ac60b1 --- /dev/null +++ b/config/src/commonMain/kotlin/io/github/charlietap/chasm/config/StoreConfig.kt @@ -0,0 +1,12 @@ +package io.github.charlietap.chasm.config + +/** + * Configures a store. + * + * @property meterFuel compiles a fuel check at every function entry and loop iteration of the + * store's code. A metered store starts with no fuel, so add some before running any of its code, + * a module's start function included. + */ +data class StoreConfig( + val meterFuel: Boolean = false, +) diff --git a/executor/invoker/src/commonMain/kotlin/io/github/charlietap/chasm/executor/invoker/dispatch/admin/FuelCheckDispatcher.kt b/executor/invoker/src/commonMain/kotlin/io/github/charlietap/chasm/executor/invoker/dispatch/admin/FuelCheckDispatcher.kt new file mode 100644 index 000000000..f45918b38 --- /dev/null +++ b/executor/invoker/src/commonMain/kotlin/io/github/charlietap/chasm/executor/invoker/dispatch/admin/FuelCheckDispatcher.kt @@ -0,0 +1,19 @@ +package io.github.charlietap.chasm.executor.invoker.dispatch.admin + +import io.github.charlietap.chasm.runtime.dispatch.DispatchableInstruction +import io.github.charlietap.chasm.runtime.error.InvocationError +import io.github.charlietap.chasm.runtime.exception.InvocationException +import io.github.charlietap.chasm.runtime.instruction.AdminInstruction +import io.github.charlietap.chasm.runtime.store.Fuel + +/** [fuel] is the compiling store's, captured so a check does not look it up on every loop iteration. */ +fun FuelCheckDispatcher( + @Suppress("UNUSED_PARAMETER") instruction: AdminInstruction.FuelCheck, + fuel: Fuel, +): DispatchableInstruction = DispatchableInstruction { _, _, nextIp -> + if (fuel.remaining <= 0L) { + throw InvocationException(InvocationError.FuelExhausted) + } + fuel.remaining-- + nextIp +} diff --git a/runtime/core/src/commonMain/kotlin/io/github/charlietap/chasm/runtime/error/InvocationError.kt b/runtime/core/src/commonMain/kotlin/io/github/charlietap/chasm/runtime/error/InvocationError.kt index 00834fe67..c96bda0a4 100644 --- a/runtime/core/src/commonMain/kotlin/io/github/charlietap/chasm/runtime/error/InvocationError.kt +++ b/runtime/core/src/commonMain/kotlin/io/github/charlietap/chasm/runtime/error/InvocationError.kt @@ -170,6 +170,8 @@ sealed interface InvocationError : ModuleTrapError { data object GuestHeapOutOfMemory : InvocationError + data object FuelExhausted : InvocationError + @JvmInline value class UnimplementedInstruction(val instruction: LinkedInstruction) : InvocationError diff --git a/runtime/core/src/commonMain/kotlin/io/github/charlietap/chasm/runtime/instruction/AdminInstruction.kt b/runtime/core/src/commonMain/kotlin/io/github/charlietap/chasm/runtime/instruction/AdminInstruction.kt index f021f6521..79ffed925 100644 --- a/runtime/core/src/commonMain/kotlin/io/github/charlietap/chasm/runtime/instruction/AdminInstruction.kt +++ b/runtime/core/src/commonMain/kotlin/io/github/charlietap/chasm/runtime/instruction/AdminInstruction.kt @@ -21,6 +21,9 @@ sealed interface AdminInstruction : LinkedInstruction { data class Jump(val targetIp: Int) : AdminInstruction + /** Takes a unit of the store's [io.github.charlietap.chasm.runtime.store.Fuel]; emitted only for metered stores. */ + data object FuelCheck : AdminInstruction + data class JumpCopies( val operands: OperandTransfer, val destinationSlotBase: Int, diff --git a/runtime/core/src/commonMain/kotlin/io/github/charlietap/chasm/runtime/store/Fuel.kt b/runtime/core/src/commonMain/kotlin/io/github/charlietap/chasm/runtime/store/Fuel.kt new file mode 100644 index 000000000..27842e7bd --- /dev/null +++ b/runtime/core/src/commonMain/kotlin/io/github/charlietap/chasm/runtime/store/Fuel.kt @@ -0,0 +1,12 @@ +package io.github.charlietap.chasm.runtime.store + +/** + * A budget for the code a store runs. When [metered], code compiled into the store takes a unit from + * [remaining] at every function entry and loop iteration: [remaining] of them pass, and the next traps + * with [io.github.charlietap.chasm.runtime.error.InvocationError.FuelExhausted]. Unmetered stores + * compile no checks and pay nothing. + */ +class Fuel(val metered: Boolean = false) { + /** Read and written only by the thread running the store's calls. */ + var remaining: Long = 0L +} diff --git a/runtime/core/src/commonMain/kotlin/io/github/charlietap/chasm/runtime/store/Store.kt b/runtime/core/src/commonMain/kotlin/io/github/charlietap/chasm/runtime/store/Store.kt index 2cd5cb029..22b0bd5e0 100644 --- a/runtime/core/src/commonMain/kotlin/io/github/charlietap/chasm/runtime/store/Store.kt +++ b/runtime/core/src/commonMain/kotlin/io/github/charlietap/chasm/runtime/store/Store.kt @@ -18,4 +18,5 @@ class Store( val data: MutableList = [], val program: Program = Program(), val heap: WasmHeap = WasmHeap(), + val fuel: Fuel = Fuel(), ) diff --git a/tools/compiler-baseline/src/main/kotlin/io/github/charlietap/chasm/tools/compilerbaseline/CompilerInstructionTagTranslator.kt b/tools/compiler-baseline/src/main/kotlin/io/github/charlietap/chasm/tools/compilerbaseline/CompilerInstructionTagTranslator.kt index 62db7b41f..1931d64fd 100644 --- a/tools/compiler-baseline/src/main/kotlin/io/github/charlietap/chasm/tools/compilerbaseline/CompilerInstructionTagTranslator.kt +++ b/tools/compiler-baseline/src/main/kotlin/io/github/charlietap/chasm/tools/compilerbaseline/CompilerInstructionTagTranslator.kt @@ -53,6 +53,7 @@ class CompilerInstructionTagTranslator { is AdminInstruction.EndFunction -> "admin.end_function" is AdminInstruction.CopySlot -> "admin.copy_slot" is AdminInstruction.CopySlots -> "admin.copy_slots" + AdminInstruction.FuelCheck -> "admin.fuel_check" is AdminInstruction.Jump -> "admin.jump" is AdminInstruction.JumpCopies -> "admin.jump_copies" is AdminInstruction.JumpIfI -> "admin.jump_if.i"