From 6d4f0881659a766268aac3bb75f6ac241cfc9370 Mon Sep 17 00:00:00 2001 From: Wavesonics Date: Fri, 25 Sep 2026 00:34:44 -0700 Subject: [PATCH 1/2] runtime: meter fuel in metered stores An embedder running untrusted modules had no way to stop one that loops forever: chasm has no instruction budget. The workaround is rewriting each module to decrement a global at every function entry and loop header, which adds eight instructions to every iteration. A store created with StoreConfig(meterFuel = true) compiles one FuelCheck instruction at each function entry and at each loop's branch target, taking a unit from the store's Fuel. A metered store starts with no fuel; addFuel tops it up, saturating at Long.MAX_VALUE, resetFuel empties it, and remainingFuel reads it. Each returns an error for an unmetered store, and addFuel rejects negative amounts. Running out traps with FuelExhausted and leaves the balance at zero, including during a module's start function. Unmetered stores compile no checks, so existing embedders are unaffected. On a grammar-checking plugin (Harper, compiled to Wasm) checking 40 paragraphs on the JVM, warmed up: 0.83 to 0.88 s unmetered, 1.12 to 1.16 s with the rewriting workaround, and 0.94 to 0.97 s with native fuel. --- chasm/api/chasm.klib.api | 4 + chasm/api/jvm/chasm.api | 7 + .../github/charlietap/chasm/embedding/Fuel.kt | 46 ++ .../charlietap/chasm/embedding/Store.kt | 4 + .../charlietap/chasm/integration/FuelTest.kt | 200 +++++++++ .../resources/integration/fuel.wasm | Bin 0 -> 142 bytes .../commonTest/resources/integration/fuel.wat | 26 ++ .../resources/integration/fuel_parallel.wasm | Bin 0 -> 3635 bytes .../resources/integration/fuel_parallel.wat | 401 ++++++++++++++++++ .../resources/integration/fuel_start.wasm | Bin 0 -> 32 bytes .../resources/integration/fuel_start.wat | 6 + .../compiler/ControlInstructionCompiler.kt | 7 +- .../chasm/compiler/FunctionCompiler.kt | 2 + .../chasm/compiler/context/CompilerContext.kt | 4 + .../instruction/AdminInstructionEmitter.kt | 8 + config/api/config.klib.api | 13 + config/api/jvm/config.api | 13 + .../charlietap/chasm/config/StoreConfig.kt | 12 + .../dispatch/admin/FuelCheckDispatcher.kt | 19 + .../chasm/runtime/error/InvocationError.kt | 2 + .../runtime/instruction/AdminInstruction.kt | 3 + .../charlietap/chasm/runtime/store/Fuel.kt | 12 + .../charlietap/chasm/runtime/store/Store.kt | 1 + .../CompilerInstructionTagTranslator.kt | 1 + 24 files changed, 790 insertions(+), 1 deletion(-) create mode 100644 chasm/src/commonMain/kotlin/io/github/charlietap/chasm/embedding/Fuel.kt create mode 100644 chasm/src/commonTest/kotlin/io/github/charlietap/chasm/integration/FuelTest.kt create mode 100644 chasm/src/commonTest/resources/integration/fuel.wasm create mode 100644 chasm/src/commonTest/resources/integration/fuel.wat create mode 100644 chasm/src/commonTest/resources/integration/fuel_parallel.wasm create mode 100644 chasm/src/commonTest/resources/integration/fuel_parallel.wat create mode 100644 chasm/src/commonTest/resources/integration/fuel_start.wasm create mode 100644 chasm/src/commonTest/resources/integration/fuel_start.wat create mode 100644 config/src/commonMain/kotlin/io/github/charlietap/chasm/config/StoreConfig.kt create mode 100644 executor/invoker/src/commonMain/kotlin/io/github/charlietap/chasm/executor/invoker/dispatch/admin/FuelCheckDispatcher.kt create mode 100644 runtime/core/src/commonMain/kotlin/io/github/charlietap/chasm/runtime/store/Fuel.kt 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..2f578a0c6 --- /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, an unmetered one 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 0000000000000000000000000000000000000000..af461ab7592de713cfb3e5603a9eed15bcff5543 GIT binary patch literal 142 zcmWm5Ne+TQ6h+bdtD(fNiLwHmJ97iF6AThlF_eU6%+@U?_a!HJyyigwDNPHlY_i!< zuoKS7h=jX$^B5EO?zg5T%hY%4H1`;n{ewNk^o|)9d>MoW6@Ppb0sB0mTIH>2jf1%oIA*f|+lauSP682XvHnj|MMvokO|@Gx+5 nkJ6*zFq#fV^TB92Fj@|bmII^Zz-T!zS`Lhs1Eb{tt;+!b49l-4 literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..90063ab8e8f74125440f61d40cf1698d94bf582a GIT binary patch literal 32 ncmZQbEY4+QU|?WmVN76PU}j=u;9z9n;$&oJV0PeP;N}JZG06fr literal 0 HcmV?d00001 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" From a10b700a947e27ac0efb97a719807d5ab0645857 Mon Sep 17 00:00:00 2001 From: CharlieTap Date: Sat, 26 Sep 2026 11:18:23 +0100 Subject: [PATCH 2/2] test: remove comma from fuel test name --- .../kotlin/io/github/charlietap/chasm/integration/FuelTest.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 index 2f578a0c6..d0aaa9725 100644 --- a/chasm/src/commonTest/kotlin/io/github/charlietap/chasm/integration/FuelTest.kt +++ b/chasm/src/commonTest/kotlin/io/github/charlietap/chasm/integration/FuelTest.kt @@ -161,7 +161,7 @@ class FuelTest { } @Test - fun `a metered store compiles a check per function and loop, an unmetered one none`() { + 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)