Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions chasm/api/chasm.klib.api
Original file line number Diff line number Diff line change
Expand Up @@ -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<kotlin/Unit, io.github.charlietap.chasm.embedding.error/ChasmError.ExecutionError> // 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.runtime.value/ReferenceValue, io.github.charlietap.chasm.embedding.error/ChasmError.ExecutionError> // 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<kotlin/Unit, io.github.charlietap.chasm.embedding.error/ChasmError.ExecutionError> // 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<kotlin/Unit, io.github.charlietap.chasm.embedding.error/ChasmError.ExecutionError> // 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.shapes/Component, io.github.charlietap.chasm.embedding.error/ChasmError.DecodeError> // 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.shapes/Component, io.github.charlietap.chasm.embedding.error/ChasmError.DecodeError> // 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<kotlin/Unit, io.github.charlietap.chasm.embedding.error/ChasmError.ExecutionError> // io.github.charlietap.chasm.embedding/dropInstance|dropInstance(io.github.charlietap.chasm.embedding.shapes.Store;io.github.charlietap.chasm.embedding.shapes.Instance){}[0]
Expand All @@ -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.shapes/Module, io.github.charlietap.chasm.embedding.error/ChasmError.DecodeError> // 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.shapes/PreparedFunction, io.github.charlietap.chasm.embedding.error/ChasmError.ExecutionError> // 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<kotlin/Long, io.github.charlietap.chasm.embedding.error/ChasmError.ExecutionError> // 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<kotlin/Unit, io.github.charlietap.chasm.embedding.error/ChasmError.ExecutionError> // 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.shapes/Component, io.github.charlietap.chasm.embedding.error/ChasmError.ValidationError> // io.github.charlietap.chasm.embedding/validate|validate(io.github.charlietap.chasm.embedding.shapes.Component){}[0]
Expand Down
7 changes: 7 additions & 0 deletions chasm/api/jvm/chasm.api
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand Down Expand Up @@ -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 {
Expand Down
Original file line number Diff line number Diff line change
@@ -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<Unit, ChasmError.ExecutionError> {
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<Unit, ChasmError.ExecutionError> {
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<Long, ChasmError.ExecutionError> {
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"),
)
Original file line number Diff line number Diff line change
@@ -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)))
Original file line number Diff line number Diff line change
@@ -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 {

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Given we move to addFuel we could do with a few extra cases:

  • A fresh metered store has zero fuel and traps on invocation; an exactly sufficient allowance succeeds and leaves zero.
  • addFuel preserves the existing balance, adding zero changes nothing, and additions reaching or exceeding Long.MAX_VALUE saturate correctly.
  • Negative additions and additions to unmetered stores return an error without changing the balance.
  • After exhaustion, adding fuel allows a finite function to complete successfully. The current refill test runs another infinite loop, so it doesn’t quite establish this.
  • A looping start function exhausts fuel during instantiation.


@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<ChasmResult.Error<*>>(addFuel(store, 10))
assertIs<ChasmResult.Error<*>>(resetFuel(store))
assertIs<ChasmResult.Error<*>>(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<ChasmResult.Error<*>>(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<Store, Instance> {
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"
}
}
Binary file added chasm/src/commonTest/resources/integration/fuel.wasm
Binary file not shown.
26 changes: 26 additions & 0 deletions chasm/src/commonTest/resources/integration/fuel.wat
Original file line number Diff line number Diff line change
@@ -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))))
Binary file not shown.
Loading
Loading