diff --git a/Cargo.lock b/Cargo.lock
index c7594f00..901d0702 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -2673,6 +2673,7 @@ dependencies = [
"libipld",
"rust_decimal",
"serde",
+ "serde_json",
"stacker",
"thiserror",
"tokio",
diff --git a/README.md b/README.md
index cd3ed092..35292415 100644
--- a/README.md
+++ b/README.md
@@ -129,9 +129,12 @@ represents the `Homestar` runtime. We recommend diving into each package's own
- [homestar-wasm](./homestar-wasm)
This *wasm* library manages the [wasmtime][wasmtime] runtime, provides the
- [Ipld][ipld] to/from [Wit][wit] interpreter/translation-layer, and implements
+ [IPLD][ipld] to/from [WIT][wit] interpreter/translation-layer, and implements
the input interface for working with Ipvm's standard Wasm tasks.
+ You can find the spec for translating between IPLD and WIT runtime values
+ based on WIT types [here](./homestar-wasm/README.md##interpreting-between-ipld-and-wit).
+
- [homestar-workflow](./homestar-workflow)
The *workflow* library implements workflow-centric [Ipvm features][ipvm-spec]
diff --git a/homestar-wasm/Cargo.toml b/homestar-wasm/Cargo.toml
index d6509a27..3539f06c 100644
--- a/homestar-wasm/Cargo.toml
+++ b/homestar-wasm/Cargo.toml
@@ -54,6 +54,7 @@ wit-component = "0.200"
[dev-dependencies]
criterion = "0.5"
+serde_json = { workspace = true }
tokio = { workspace = true }
[features]
diff --git a/homestar-wasm/README.md b/homestar-wasm/README.md
index cdc7d9b6..ed6c2448 100644
--- a/homestar-wasm/README.md
+++ b/homestar-wasm/README.md
@@ -3,7 +3,7 @@
-
Homestar
+ homestar-wasm
@@ -23,15 +23,1055 @@
##
+## Outline
+
+- [Description](#description)
+- [Interpreting between IPLD and WIT](#interpreting-between-ipld-and-wit)
+
## Description
This *wasm* library manages the [wasmtime][wasmtime] runtime, provides the
-[Ipld][ipld] to/from [Wit][wit] interpreter/translation-layer, and implements
-the input interface for working with Ipvm's standard Wasm tasks.
+[IPLD][ipld] to/from Wasm Interace Types ([WIT][wit])
+interpreter/translation-layer, and implements the input interface for working
+with Ipvm's standard Wasm tasks.
For more information, please go to our [Homestar Readme][homestar-readme].
+## Interpreting between IPLD and WIT
+
+Our recursive interpreter is able to bidirectionally translate between
+the runtime [IPLD data model][ipld-data-model] and [WIT][wit] values, based on
+known [WIT][wit] interface types.
+
+### Primitive Types
+
+We'll start by covering WIT [primitive types][wit-primitive].
+
+#### Booleans
+
+This section outlines the translation process between IPLD boolean values
+(`Ipld::Bool`) and [WIT `bool` runtime values][wit-val].
+
+- **IPLD to WIT Translation**:
+
+ When a WIT function expects a `bool` input, an `Ipld::Bool` value (either
+ `true` or `false`) is mapped to a `bool` WIT runtime
+ value.
+
+ **Example**: Consider a WIT function defined as follows:
+
+ ```wit
+ export fn: func(a: bool) -> bool;
+ ```
+
+ Given a JSON input for this function:
+
+ ```json
+ {
+ "args": [true]
+ }
+ ```
+
+ `true` is converted into an `Ipld::Bool`, which is then translated and
+ passed into `fn` as a boolean argument (`bool`).
+
+- **WIT to IPLD Translation**:
+
+ Conversely, when a boolean value is returned from a WIT function, it can be
+ translated back into an `Ipld::Bool`.
+
+**IPLD Schema Definition**:
+
+```ipldsch
+type IPLDBooleanAsWit bool
+```
+
+#### Integers
+
+This section outlines the translation process between IPLD integer values
+(`Ipld::Integer`) and [WIT `integer` rutime values][wit-val].
+
+The [Component Model][wit] supports these [integer][wit-integer] types:
+
+```ebnf
+ty ::= 'u8' | 'u16' | 'u32' | 'u64'
+ | 's8' | 's16' | 's32' | 's64'
+```
+
+- **IPLD to WIT Translation**:
+
+ Typically, when a WIT function expects an integer input, an `Ipld::Integer`
+ value is mapped to an integer WIT runtime value.
+
+ **Example**: Consider a WIT function defined as follows:
+
+ ```wit
+ export fn: func(a: s32) -> s32;
+ ```
+
+ Given a JSON input for this function:
+
+ ```json
+ {
+ "args": [1]
+ }
+ ```
+
+ `1` is converted into an `Ipld::Integer`, which is then translated and
+ passed into `fn` as an integer argument (`s32`).
+
+ **Note**: However, if the input argument to the WIT interface is a `float`
+ type, but the incoming value is an `Ipld::Integer`, then the IPLD value will
+ be cast to a `float`, and remain as one for the rest of the computation. The cast is
+ to provide affordances for JavaScript where, for example, the number `1.0` is converted to `1`.
+
+- **WIT to IPLD Translation**:
+
+ Conversely, when an integer value (not a float) is returned from a WIT
+ function, it can be translated back into an `Ipld::Integer`.
+
+**IPLD Schema Definitions**:
+
+```ipldschme
+type IPLDIntegerAsWit union {
+ | U8 int
+ | U16 int
+ | U32 int
+ | U64 int
+ | S8 int
+ | S16 int
+ | S32 int
+ | S64 int
+ | Float32In int
+ | Float64In int
+} representation kinded
+
+type WitAsIpldInteger union {
+ | U8 int
+ | U16 int
+ | U32 int
+ | U64 int
+ | S8 int
+ | S16 int
+ | S32 int
+ | S64 int
+ | Float32Out float
+ | Float64Out float
+} representation kinded
+```
+
+#### Floats
+
+This section outlines the translation process between IPLD float values
+(`Ipld::Float`) and [WIT `float` runtime values][wit-val].
+
+The [Component Model][wit] supports these Float types:
+
+```ebnf
+ty ::= 'float32' | 'float64'
+```
+
+- **IPLD to WIT Translation**:
+
+ When a WIT function expects a float input, an `Ipld::Float` value is
+ mapped to a float WIT runtime value. Casting is done to convert from `f32` to
+ `f64` if necessary.
+
+ **Example**: Consider a WIT function defined as follows:
+
+ ```wit
+ export fn: func(a: f64) -> f64;
+ ```
+
+ Given a JSON input for this function:
+
+ ```json
+ {
+ "args": [1.0]
+ }
+ ```
+
+ `1.0` is converted into an `Ipld::Float`, which is then translated and
+ passed into `fn` as a float argument (`f64`).
+
+- **WIT to IPLD Translation**:
+
+ Conversely, when a `float32` or `float64` value is returned from a WIT
+ function, it can be translated back into an `Ipld::Float`.
+
+ **Note**: In converting from `float32` to `float64`, the latter of which is
+ the default precision for [IPLD][ipld-float], precision will be lost.
+ **The interpreter will use decimal precision in this conversion**.
+
+**IPLD Schema Definitions**:
+
+```ipldsch
+type IPLDFloatAsWit union {
+ | Float32 float
+ | Float64 float
+} representation kinded
+
+type WitAsIpldFloat union {
+ | Float32 float
+ | Float64 float
+} representation kinded
+```
+
+#### Strings
+
+This section outlines the translation process between IPLD string values
+(`Ipld::String`) and various [WIT runtime values][wit-val]. A `Ipld::String` value can be
+interpreted as one of a `string`, `char`, `list`, or an `enum` discriminant
+(which has no payload).
+
+- `string`
+
+ * **IPLD to WIT Translation**
+
+ When a WIT function expects a `string` input, an `Ipld::String` value is
+ mapped to a `string` WIT runtime value.
+
+ **Example**:
+
+ ```wit
+ export fn: func(a: string) -> string;
+ ```
+
+ Given a JSON input for this function:
+
+ ```json
+ {
+ "args": ["Saspirilla"]
+ }
+ ```
+
+ `"Saspirilla"` is converted into an `Ipld::String`, which is then translated
+ and passed into `fn` as a string argument (`string`).
+
+ * **WIT to IPLD Translation**:
+
+ Conversely, when a `string` value is returned from a WIT function, it is
+ translated back into an `Ipld::String`.
+
+- `char`
+
+ * **IPLD to WIT Translation**
+
+ When a WIT function expects a `char` input, an `Ipld::String` value is
+ mapped to a `char` WIT runtime value.
+
+ **Example**:
+
+ ```wit
+ export fn: func(a: char) -> char;
+ ```
+
+ Given a JSON input for this function:
+
+ ```json
+ {
+ "args": ["S"]
+ }
+ ```
+
+ `"S"`is converted into an `Ipld::String`, which is then translated and
+ passed into `fn` as a char argument (`char`).
+
+ * **WIT to IPLD Translation**:
+
+ Conversely, when a char value is returned from a WIT function, it is
+ translated back into an `Ipld::String`.
+
+- `list`
+
+ * **IPLD to WIT Translation**
+
+ When a WIT function expects a `list` input, an `Ipld::String` value is
+ mapped to a `list` WIT runtime value.
+
+ **Example**:
+
+ ```wit
+ export fn: func(a: list) -> list;
+ ```
+
+ Given a JSON input for this function:
+
+ ```json
+ {
+ "args": ["aGVsbDA"]
+ }
+ ```
+
+ `"aGVsbDA"` is converted into an `Ipld::String`, which is then translated
+ into bytes and passed into `fn` as a `list` argument.
+
+ * **WIT to IPLD Translation**:
+
+ **Here, when a `list` value is returned from a WIT function, it is
+ translated into an `Ipld::Bytes` value, which is the proper type**.
+
+- [`enum`][wit-enum]:
+
+ An enum statement defines a new type which is semantically equivalent to a
+ variant where none of the cases have a payload type.
+
+ * **IPLD to WIT Translation**
+
+ When a WIT function expects an `enum` input, an `Ipld::String` value is
+ mapped to a `enum` WIT runtime value.
+
+ **Example**:
+
+ ```wit
+ enum color {
+ Red,
+ Green,
+ Blue
+ }
+
+ export fn: func(a: color) -> string;
+ ```
+
+ Given a JSON input for this function:
+
+ ```json
+ {
+ "args": ["Green"]
+ }
+ ```
+
+ `"Green"` is converted into an `Ipld::String`, which is then translated and
+ passed into `fn` as a enum argument (`color`). **You'll have to provide a
+ string that matches on one of the discriminants**.
+
+ * **WIT to IPLD Translation**:
+
+ Conversely, when an enum value is returned from a WIT function, it can be
+ translated back into an `Ipld::String` value.
+
+**IPLD Schema Definitions**:
+
+``` ipldsch
+type Enum enum {
+ | Red
+ | Green
+ | Blue
+}
+
+type IPLDStringAsWit union {
+ | Enum Enum
+ | String string
+ | Char string
+ | Listu8In string
+} representation kinded
+
+type WitAsIpldString union {
+ | Enum Enum
+ | String string
+ | Char string
+ | Listu8Out bytes
+} representation kinded
+```
+
+#### Bytes
+
+This section outlines the translation process between IPLD bytes values
+(`Ipld::Bytes`) and various [WIT runtime values][wit-val]. A `Ipld::Bytes` value
+can be interpreted either as a `list` or `string`.
+
+- [`list`][wit-list]:
+
+ * **IPLD to WIT Translation**
+
+ When a WIT function expects a `list` input, an `Ipld::Bytes` value is
+ mapped to a `list` WIT runtime value.
+
+ **Example**:
+
+ ```wit
+ export fn: func(a: list) -> list;
+ ```
+
+ Given a JSON input for this function:
+
+ ```json
+ {
+ "args": [{"/": {"bytes": "aGVsbDA"}}]
+ }
+ ```
+
+ `"aGVsbDA"` is converted into an `Ipld::Bytes`, which is then translated
+ into bytes and passed into `fn` as a `list` argument.
+
+ * **WIT to IPLD Translation**:
+
+ Conversely, when a `list` value is returned from a WIT function, it is
+ translated back into an `Ipld::Bytes` value if the list contains valid
+ `u8` values.
+
+- `string`
+
+ * **IPLD to WIT Translation**
+
+ When a WIT function expects a `string` input, an `Ipld::Bytes` value is
+ mapped to a `string` WIT runtime value.
+
+ **Example**:
+
+ ```wit
+ export fn: func(a: string) -> string;
+ ```
+
+ Given a JSON input for this function:
+
+ ```json
+ {
+ "args": [{"/": {"bytes": "aGVsbDA"}}]
+ }
+ ```
+
+ `"aGVsbDA"` is converted into an `Ipld::Bytes`, which is then translated
+ into a `string` and passed into `fn` as a `string` argument.
+
+ * **WIT to IPLD Translation**:
+
+ **Here, when a string value is returned from a WIT function, it is
+ translated into an `Ipld::String` value, because we can't determine if it
+ was originally `bytes`**.
+
+**IPLD Schema Definitions**:
+
+``` ipldsch
+type IPLDBytesAsWit union {
+ | ListU8 bytes
+ | StringIn bytes
+} representation kinded
+
+type WitAsIpldBytes union {
+ | ListU8 bytes
+ | StringOut string
+} representation kinded
+```
+
+
+#### Nulls
+
+This section outlines the translation process between IPLD null values
+(`Ipld::Null`) and various [WIT runtime values][wit-val]. A `Ipld::Null` value
+can be interpreted either as a `string` or `option`.
+
+**We'll cover only the `string` case here** and return to the `option` case
+below.
+
+* **IPLD to WIT Translation**
+
+ When a WIT function expects a `string` input, an `Ipld::Null` value is
+ mapped as a `"null"` `string` WIT runtime value.
+
+ **Example**:
+
+ ```wit
+ export fn: func(a: string) -> string;
+ ```
+
+ Given a JSON input for this function:
+
+ ```json
+ {
+ "args": [null]
+ }
+ ```
+
+ `null` is converted into an `Ipld::Null`, which is then translated and
+ passed into `fn` as a `string` argument with the value of `"null"`.
+
+* **WIT to IPLD Translation**:
+
+ Conversely, when a `string` value of `"null"` is returned from a WIT function,
+ it can be translated into an `Ipld::Null` value.
+
+**IPLD Schema Definitions**:
+
+``` ipldsch
+type None unit representation null
+
+type IPLDNullAsWit union {
+ | None
+ | String string
+} representation kinded
+
+type WitAsIpldNull union {
+ | None
+ | String string
+} representation kinded
+```
+
+#### Links
+
+This section outlines the translation process between IPLD link values
+(`Ipld::Link`) and [WIT `string` runtime values][wit-val]. A `Ipld::Link` is always
+interpreted as a `string` in WIT, and vice versa.
+
+* **IPLD to WIT Translation**
+
+ When a WIT function expects a `string` input, an `Ipld::Link` value is
+ mapped to a `string` WIT runtime value, translated accordingly based
+ on the link being [Cidv0][cidv0] or [Cidv1][cidv1].
+
+ **Example**:
+
+ ```wit
+ export fn: func(a: string) -> string;
+ ```
+
+ Given a JSON input for this function:
+
+ ```json
+ {
+ "args": ["bafybeia32q3oy6u47x624rmsmgrrlpn7ulruissmz5z2ap6alv7goe7h3q"]
+ }
+ ```
+
+ `"bafybeia32q3oy6u47x624rmsmgrrlpn7ulruissmz5z2ap6alv7goe7h3q"` is converted
+ into an `Ipld::Link`, which is then translated and passed into `fn` as a
+ `string` argument.
+
+* **WIT to IPLD Translation**:
+
+ Conversely, when a `string` value is returned from a WIT function, and if it
+ can be converted to a Cid, it can then be translated into an `Ipld::Link`
+ value.
+
+**IPLD Schema Definitions**:
+
+``` ipldsch
+type IPLDLinkAsWit &String link
+
+type WitAsIpldLink &String link
+```
+
+### Non-primitive Types
+
+Next, we'll cover the more interesting, WIT non-primitive types.
+
+#### List Values
+
+This section outlines the translation process between IPLD list values
+(`Ipld::List`) and various [WIT runtime values][wit-val]. A `Ipld::List`
+value can be interpreted as one of a `list`, `tuple`, set of `flags`,
+or a `result`.
+
+**We'll return to the `result` case below, and cover the rest of the
+possibilities here**.
+
+- [`list`][wit-list]
+
+ * **IPLD to WIT Translation**
+
+ When a WIT function expects a `list` input, an `Ipld::List` value is
+ mapped to a `list` WIT runtime value.
+
+ **Example**:
+
+ ```wit
+ export fn: func(a: list, b: s32) -> list;
+ ```
+
+ Given a JSON input for this function:
+
+ ```json
+ {
+ "args": [[1, 2, 3], 44]
+ }
+ ```
+
+ `[1, 2, 3]` is converted into an `Ipld::List`, which is then translated
+ and passed into `fn` as a `list` argument.
+
+ * **WIT to IPLD Translation**:
+
+ Conversely, when a `list` value is returned from a WIT function, it is
+ translated back into an `Ipld::List` value.
+
+- [`tuple`][wit-tuple]:
+
+ * **IPLD to WIT Translation**
+
+ When a WIT function expects a `tuple` input, an `Ipld::List` value is
+ mapped to a `tuple` WIT runtime value.
+
+ **Example**:
+
+ ```wit
+ type ipv6-socket-address = tuple;
+
+ export fn: func(a: ipv6-socket-address) -> tuple;
+ ```
+
+ Given a JSON input for this function:
+
+ ```json
+ {
+ "args": [[8193, 3512, 34211, 0, 0, 35374, 880, 29492]]
+ }
+ ```
+
+ `[8193, 3512, 34211, 0, 0, 35374, 880, 29492]` is converted into an
+ `Ipld::List`, which is then translated and passed into `fn` as a
+ `tuple` argument.
+
+ **If the length of list does not match not match the number of fields in the
+ tuple interface type, then an error will be thrown in the interpreter.**
+
+ * **WIT to IPLD Translation**:
+
+ Conversely, when a `tuple` value is returned from a WIT function, it is
+ translated back into an `Ipld::List` value.
+
+- [`flags`][wit-flags]:
+
+ `flags` represent a bitset structure with a name for each bit. The type
+ represents a set of named booleans. In an instance of the named type, each flag will
+ be either true or false.
+
+ * **IPLD to WIT Translation**
+
+ When a WIT function expects a `flags` input, an `Ipld::List` value is
+ mapped to a `flags` WIT runtime value.
+
+ When used as an input, you can set the flags you want turned on/true as an
+ inclusive subset of strings. When used as an output, you will get a list of
+ strings representing the flags that are set to true.
+
+
+ **Example**:
+
+ ```wit
+ flags permissions {
+ read,
+ write,
+ exec,
+ }
+
+ export fn: func(perm: permissions) -> bool;
+ ```
+
+ Given a JSON input for this function:
+
+ ```json
+ {
+ "args": [["read", "write"]]
+ }
+ ```
+
+ `[read, write]` is converted into an `Ipld::List`, which is then translated
+ and passed into `fn` as a `permissions` argument.
+
+ * **WIT to IPLD Translation**:
+
+ Conversely, when a `flags` value is returned from a WIT function, it is
+ translated back into an `Ipld::List` value.
+
+
+**IPLD Schema Definitions**:
+
+``` ipldsch
+type IPLDListAsWit union {
+ | List [any]
+ | Tuple [any]
+ | Flags [string]
+} representation kinded
+
+type WitAsIpldList union {
+ | List [any]
+ | Tuple [any]
+ | Flags [string]
+} representation kinded
+```
+
+#### Maps
+
+This section outlines the translation process between IPLD map values
+(`Ipld::Map`) and various [WIT runtime values][wit-val]. A `Ipld::Map`
+value can be interpreted as one of a `record`, `variant`, or
+a `list` of two-element `tuples`.
+
+- [`record`][wit-record]:
+
+ * **IPLD to WIT Translation**
+
+ When a WIT function expects a `record` input, an `Ipld::Map` value is
+ mapped to a `record` WIT runtime value.
+
+ **Example**:
+
+ ```wit
+ record pair {
+ x: u32,
+ y: u32,
+ }
+
+ export fn: func(a: pair) -> u32;
+ ```
+
+ Given a JSON input for this function:
+
+ ```json
+ {
+ "args": [{"x": 1, "y": 2}]
+ }
+ ```
+
+ `{"x": 1, "y": 2}` is converted into an `Ipld::Map`, which is then
+ translated and passed into `fn` as a `pair` argument.
+
+ **The keys in the map must match the field names in the record type**.
+
+ * **WIT to IPLD Translation**:
+
+ Conversely, when a `record` value is returned from a WIT function, it is
+ translated back into an `Ipld::Map` value.
+
+- [`variant`][wit-variant]:
+
+ A variant statement defines a new type where instances of the type match
+ exactly one of the variants listed for the type. This is similar to a
+ "sum" type in algebraic datatypes (or an enum in Rust if you're familiar
+ with it). Variants can be thought of as tagged unions as well.
+
+ Each case of a variant can have an optional type / payload associated with it
+ which is present when values have that particular case's tag.
+
+ * **IPLD to WIT Translation**
+
+ When a WIT function expects a `variant` input, an `Ipld::Map` value is
+ mapped to a `variant` WIT runtime value.
+
+ **Example**:
+
+ ```wit
+
+ variant filter {
+ all,
+ none,
+ some(list),
+ }
+
+ export fn: func(a: filter);
+ ```
+
+ Given a JSON input for this function:
+
+ ```json
+ {
+ "args": [{"some" : ["a", "b", "c"]}]
+ }
+ ```
+
+ `{"some" : ["a", "b", "c"]}` is converted into an `Ipld::Map`, which is
+ then translated and passed into `fn` as a `filter` argument, where the key
+ is the variant name and the value is the payload.
+
+ **The keys in the map must match the variant names in the variant type**.
+
+ * **WIT to IPLD Translation**:
+
+ Conversely, when a `variant` value is returned from a WIT function, it is
+ translated back into an `Ipld::Map` value where the tag is the key and
+ payload is the value.
+
+- [`list`][wit-list]:
+
+ * **IPLD to WIT Translation**
+
+ When a WIT function expects a nested `list` of two-element `tuples` as input,
+ an `Ipld::Map` value is mapped to that specific WIT runtime value.
+
+ **Example**:
+
+ ```wit
+ export fn: func(a: list>) -> list;
+ ```
+
+ Given a JSON input for this function:
+
+ ```json
+ {
+ "args": [{"a": 1, "b": 2}]
+ }
+ ```
+
+ `{"a": 1, "b": 2}` is converted into an `Ipld::Map`, which is then
+ translated and passed into `fn` as a `list>` argument.
+
+ * **WIT to IPLD Translation**:
+
+ Conversely, when a `list` of two-element `tuples` is returned from a WIT
+ function, it can be translated back into an `Ipld::Map` value.
+
+**IPLD Schema Definitions**:
+
+``` ipldsch
+type TupleAsMap {string:any} representation listpairs
+
+type IPLDMapAsWit union {
+ | Record {string:any}
+ | Variant {string:any}
+ | List TupleAsMap
+} representation kinded
+
+type WitAsIpldMap union {
+ | Record {string:any}
+ | Variant {string:any}
+ | List TupleAsMap
+} representation kinded
+```
+
+#### WIT Options
+
+This section outlines the translation process between [WIT option runtime values][wit-val]
+(of type `option`) and various IPLD values. An [`option`][wit-option] can be interpreted
+as either a `Ipld::Null` or of any other IPLD value.
+
+* **IPLD to WIT Translation**
+
+ When a WIT function expects an `option` as input, an `Ipld::Null` value is
+ mapped to the `None`/`Unit` case for a WIT option. Otherwise, any other IPLD
+ value will be mapped to its matching WIT runtime value directly.
+
+ **Example**:
+
+ ```wit
+ export fn: func(a: option) -> option;
+ ```
+
+ * `Some` case:
+
+ - **Json Input**:
+
+ ```json
+ {
+ "args": [1]
+ }
+ ```
+
+ * `None` case:
+
+ - **Json Input**:
+
+ ```json
+ {
+ "args": [null]
+ }
+ ```
+
+ `1` is converted into an `Ipld::Integer`, which is then translated and
+ passed into `fn` as an integer argument (`s32`), as the `Some` case of the
+ option.
+
+ `null` is converted into an `Ipld::Null`, which is then translated and
+ passed into `fn` as a `None`/`Unit` case of the option (i.e. no value in WIT).
+
+ Essentially, you can view this as `Ipld::Any` being the `Some` case and
+ `Ipld::Null` being the `None` case.
+
+* **WIT to IPLD Translation**:
+
+ Conversely, when an `option` value is returned from a WIT function, it can be
+ translated back into an `Ipld::Null` value if it's the `None`/`Unit` case, or
+ any other IPLD value if it's the `Some` case.
+
+**IPLD Schema Definitions**:
+
+``` ipldsch
+type IpldAsWitOption union {
+ | Some any
+ | None
+} representation kinded
+
+type WitAsIpldOption union {
+ | Some any
+ | None
+} representation kinded
+```
+
+#### WIT Results
+
+This section outlines the translation process between [WIT result runtime values][wit-val]
+(of type `result`) and various IPLD values. We treat result as Left/Right
+[either][either] types over an `Ipld::List` of two elements.
+
+A [`result`][wit-result] can be interpreted as one of these patterns:
+
+- `Ok` (with a payload)
+
+ * **IPLD to WIT Translation**
+
+ When a WIT function expects a `result` as input, an `Ipld::List` value can
+ be mapped to the `Ok` case of the `result` WIT runtime value, including
+ a payload.
+
+ **Example**:
+
+ ```wit
+ export fn: func(a: result) -> result;
+ ```
+
+ Given a JSON input for this function:
+
+ ```json
+ {
+ "args": [[47, null]]
+ }
+ ```
+
+
+ `[47, null]` is converted into an `Ipld::List`, which is then translated
+ and passed into `fn` as an `Ok` case of the `result` argument with a
+ payload of `47` matching the `s32` type on the left.
+
+ * **WIT to IPLD Translation**:
+
+ Conversely, when a `result` value is returned from a WIT function, it can
+ be translated back into an `Ipld::List` of this specific structure.
+
+- `Err` (with a payload)
+
+ * **IPLD to WIT Translation**
+
+ **Example**:
+
+ ```wit
+ export fn: func(a: result) -> result;
+ ```
+
+ Given a JSON input for this function:
+
+ ```json
+ {
+ "args": [[null, "error message"]]
+ }
+ ```
+
+ `[null, "error message"]` is converted into an `Ipld::List`, which is
+ then translated and passed into `fn` as an `Err` case of the `result`
+ argument with a payload of `"error message"` matching the `string` type
+ on the right.
+
+ * **WIT to IPLD Translation**:
+
+ Conversely, when a `result` value is returned from a WIT function, it can
+ be translated back into an `Ipld::List` of this specific structure.
+
+- `Ok` case (without a payload)
+
+ * **IPLD to WIT Translation**
+
+ **Example**:
+
+ ```wit
+ export fn: func(a: result<_, string>) -> result<_, string>;
+ ```
+
+ Given a JSON input for this function:
+
+ ```json
+ {
+ "args": [[47, null]]
+ }
+ ```
+
+ `[47, null]` is converted into an `Ipld::List`, which is then translated
+ and passed into `fn` as an `Ok` case of the `result` argument. The payload
+ is ignored as it's not needed (expressed in the type as `_` above), so
+ `47` is not used.
+
+ * **WIT to IPLD Translation**:
+
+ **Here, when this specific `Ok` case is returned from a WIT function, it can
+ be translated back into an `Ipld::List`, but one structured as
+ `[1, null]` internally, which signifies the `Ok` (not error) case, with
+ the `1` payload discarded.**
+
+- `Err` case (without a payload)
+
+ * **IPLD to WIT Translation**
+
+ **Example**:
+
+ ```wit
+ export fn: func(a: result) -> result;
+ ```
+
+ Given a JSON input for this function:
+
+ ```json
+ {
+ "args": [[null, "error message"]]
+ }
+ ```
+
+ `[null, "error message"]` is converted into an `Ipld::List`, which is
+ then translated and passed into `fn` as an `Err` case of the `result`
+ argument. The payload is ignored as it's not needed (expressed in the type
+ as `_` above), so `"error message"` is not used.
+
+ * **WIT to IPLD Translation**:
+
+ **Here, when this specific `Err` case is returned from a WIT function, it
+ can be translated back into an `Ipld::List`, but one structured as
+ `[null, 1]` internally, which signifies the `Err` (error) case, with
+ the `1` payload discarded.**
+
+**IPLD Schema Definitions**:
+
+``` ipldsch
+type Null unit representation null
+
+type IpldAsWitResult union {
+ | Ok [any, Null]
+ | Err [Null, any]
+} representation kinded
+
+type WitAsIpldResult union {
+ | Ok [any, Null]
+ | OkNone [1, Null]
+ | Err [Null, any]
+ | ErrNone [Null, 1]
+} representation kinded
+```
+
+**Note**: `any` is used here to represent any type that's not `Null`. So,
+given an input with a `result` type, the JSON value of
+
+```json
+{
+ "args": [null, null]
+}
+```
+
+will fail to be translated into a Wit `result`runtime value, as it's ambiguous
+which case it should be mapped to.
+
+[cidv0]: https://github.com/multiformats/cid?tab=readme-ov-file#cidv0
+[cidv1]: https://github.com/multiformats/cid?tab=readme-ov-file#cidv1
+[either]: https://www.scala-lang.org/api/2.13.6/scala/util/Either.html
[homestar-readme]: https://github.com/ipvm-wg/homestar/blob/main/README.md
[ipld]: https://ipld.io/
+[ipld-data-model]: https://ipld.io/docs/data-model/
+[ipld-float]: https://ipld.io/design/tricky-choices/numeric-domain/#floating-point
+[ipld-type]: https://docs.rs/libipld/latest/libipld/ipld/enum.Ipld.html
[wasmtime]: https://github.com/bytecodealliance/wasmtime
[wit]: https://github.com/WebAssembly/component-model/blob/main/design/mvp/WIT.md
+[wit-primitive]: https://component-model.bytecodealliance.org/design/wit.html#primitive-types
+[wit-enum]: https://component-model.bytecodealliance.org/design/wit.html#enums
+[wit-flags]: https://component-model.bytecodealliance.org/design/wit.html#flags
+[wit-integer]: https://component-model.bytecodealliance.org/design/wit.html#built-in-types
+[wit-list]: https://component-model.bytecodealliance.org/design/wit.html#lists
+[wit-option]: https://component-model.bytecodealliance.org/design/wit.html#options
+[wit-record]: https://component-model.bytecodealliance.org/design/wit.html#records
+[wit-result]: https://component-model.bytecodealliance.org/design/wit.html#results
+[wit-tuple]: https://component-model.bytecodealliance.org/design/wit.html#tuples
+[wit-val]: https://docs.wasmtime.dev/api/wasmtime/component/enum.Val.html
+[wit-variant]: https://component-model.bytecodealliance.org/design/wit.html#variants
diff --git a/homestar-wasm/src/wasmtime/ipld.rs b/homestar-wasm/src/wasmtime/ipld.rs
index d3ccafd6..7d5e636e 100644
--- a/homestar-wasm/src/wasmtime/ipld.rs
+++ b/homestar-wasm/src/wasmtime/ipld.rs
@@ -71,6 +71,7 @@ impl<'a> From<&'a Type> for InterfaceType<'a> {
| Type::Result(_)
| Type::Flags(_)
| Type::Enum(_)
+ | Type::Char
| Type::String
| Type::S8
| Type::S16
@@ -244,6 +245,12 @@ impl RuntimeVal {
)?;
RuntimeVal::new(res_inst.new_val(Err(Some(inner_v.value())))?)
}
+ ([_ipld, Ipld::Null], _, _) => {
+ RuntimeVal::new(res_inst.new_val(Ok(None))?)
+ }
+ ([Ipld::Null, _ipld], _, _) => {
+ RuntimeVal::new(res_inst.new_val(Err(None))?)
+ }
_ => Err(InterpreterError::IpldToWit(
"IPLD (as WIT result) has specific structure does does not match"
.to_string(),
@@ -307,6 +314,32 @@ impl RuntimeVal {
.ok_or(InterpreterError::IpldToWit(
"IPLD string not an enum discriminant".to_string(),
))?,
+ Some(Type::Char) => {
+ let mut chars = v.chars();
+ let c = match chars.next() {
+ // Attempt to get the first character
+ Some(c) => {
+ // Check if there's no second character
+ if chars.next().is_none() {
+ Ok(c)
+ } else {
+ Err(InterpreterError::IpldToWit(
+ "IPLD string not a valid char".to_string(),
+ ))
+ }
+ }
+ None => Err(InterpreterError::IpldToWit(
+ "IPLD string is empty".to_string(),
+ )),
+ }?;
+ RuntimeVal::new(Val::Char(c))
+ }
+ Some(Type::List(list_inst)) => {
+ let bytes = v.as_bytes();
+ let val_bytes = bytes.iter().map(|elem| Val::U8(*elem)).collect();
+
+ RuntimeVal::new(list_inst.new_val(val_bytes)?)
+ }
_ => RuntimeVal::new(Val::String(Box::from(v))),
},
Ipld::Bytes(v) => match interface_ty.inner() {
@@ -355,13 +388,15 @@ impl RuntimeVal {
}
Some(Type::Flags(flags_inst)) => {
let flags = v.iter().try_fold(vec![], |mut acc, elem| {
- if let Ipld::String(flag) = elem {
- acc.push(flag.as_ref());
- Ok::<_, InterpreterError>(acc)
- } else {
- Err(InterpreterError::IpldToWit(
- "IPLD (as flags) must contain only strings".to_string(),
- ))
+ let mut names = flags_inst.names();
+ match elem {
+ Ipld::String(flag) if names.any(|name| name == flag) => {
+ acc.push(flag.as_ref());
+ Ok::<_, InterpreterError>(acc)
+ }
+ _ => Err(InterpreterError::IpldToWit(
+ "IPLD (as flags) must contain only strings as part of the interface type".to_string(),
+ )),
}
})?;
@@ -615,10 +650,10 @@ impl TryFrom for Ipld {
Ipld::List(inner)
}
RuntimeVal(Val::Flags(v), _) => {
- let inner = v.flags().try_fold(vec![], |mut acc, flag| {
+ let inner = v.flags().fold(vec![], |mut acc, flag| {
acc.push(Ipld::String(flag.to_string()));
- Ok::<_, Self::Error>(acc)
- })?;
+ acc
+ });
Ipld::List(inner)
}
RuntimeVal(Val::Enum(v), _) => Ipld::String(v.discriminant().to_string()),
@@ -635,7 +670,12 @@ impl TryFrom for Ipld {
mod test {
use super::*;
use crate::test_utils;
- use libipld::multihash::{Code, MultihashDigest};
+ use libipld::{
+ json::DagJsonCodec,
+ multihash::{Code, MultihashDigest},
+ prelude::Codec,
+ };
+ use serde_json::json;
const RAW: u64 = 0x55;
@@ -821,7 +861,7 @@ mod test {
}
#[test]
- fn try_float_type_roundtrip() {
+ fn try_float32_type_roundtrip() {
let ipld = Ipld::Float(3883.20);
let runtime_float = RuntimeVal::new(Val::Float32(3883.20));
@@ -841,6 +881,27 @@ mod test {
assert_eq!(Ipld::try_from(runtime_float).unwrap(), ipld);
}
+ #[test]
+ fn try_float64_type_roundtrip() {
+ let ipld = Ipld::Float(3883.20);
+ let runtime_float = RuntimeVal::new(Val::Float64(3883.20));
+
+ let ty = test_utils::component::setup_component_with_param(
+ "float64".to_string(),
+ &[test_utils::component::Param(
+ test_utils::component::Type::F64,
+ Some(0),
+ )],
+ );
+
+ assert_eq!(
+ RuntimeVal::try_from(ipld.clone(), &InterfaceType::Type(ty)).unwrap(),
+ runtime_float
+ );
+
+ assert_eq!(Ipld::try_from(runtime_float).unwrap(), ipld);
+ }
+
#[test]
fn try_integer_to_float() {
let ipld_in = Ipld::Integer(5);
@@ -865,22 +926,28 @@ mod test {
#[test]
fn try_string_roundtrip() {
- let ipld = Ipld::String("Hello!".into());
- let runtime = RuntimeVal::new(Val::String(Box::from("Hello!")));
+ let ipld1 = Ipld::String("Hello!".into());
+ let ipld2 = Ipld::String("!".into());
+ let runtime1 = RuntimeVal::new(Val::String(Box::from("Hello!")));
+ let runtime2 = RuntimeVal::new(Val::Char('!'));
assert_eq!(
- RuntimeVal::try_from(ipld.clone(), &InterfaceType::Any).unwrap(),
- runtime
+ RuntimeVal::try_from(ipld1.clone(), &InterfaceType::Any).unwrap(),
+ runtime1
);
+ assert_eq!(Ipld::try_from(runtime1).unwrap(), ipld1);
- assert_eq!(Ipld::try_from(runtime).unwrap(), ipld);
+ // assert char case
+ assert_eq!(
+ RuntimeVal::try_from(ipld2.clone(), &InterfaceType::TypeRef(&Type::Char)).unwrap(),
+ runtime2
+ );
+ assert_eq!(Ipld::try_from(runtime2).unwrap(), ipld2);
}
#[test]
- fn try_bytes_roundtrip() {
- let bytes = b"hell0".to_vec();
- let ipld = Ipld::Bytes(bytes.clone());
-
+ fn try_string_to_listu8_to_string_roundtrip() {
+ let ipld_bytes_as_string = Ipld::String(String::from_utf8_lossy(b"hell0").to_string());
let ty = test_utils::component::setup_component("(list u8)".to_string(), 8);
let val_list = ty
.unwrap_list()
@@ -892,14 +959,68 @@ mod test {
Val::U8(48),
]))
.unwrap();
- let runtime = RuntimeVal::new(val_list);
+ let runtime = RuntimeVal::new(val_list);
assert_eq!(
- RuntimeVal::try_from(ipld.clone(), &InterfaceType::Type(ty)).unwrap(),
+ RuntimeVal::try_from(ipld_bytes_as_string.clone(), &InterfaceType::Type(ty)).unwrap(),
runtime
);
+ assert_eq!(
+ Ipld::try_from(runtime).unwrap(),
+ Ipld::Bytes(b"hell0".to_vec())
+ );
+ }
- assert_eq!(Ipld::try_from(runtime).unwrap(), ipld);
+ #[test]
+ fn try_bytes_roundtrip() {
+ let bytes1 = b"hell0".to_vec();
+ let bytes2 = Base::Base64.encode(b"hell0");
+
+ let ipld1 = Ipld::Bytes(bytes1.clone());
+ let ipld2 = Ipld::String("aGVsbDA".to_string());
+ let json = json!({
+ "/": {"bytes": format!("{}", bytes2)}
+ });
+
+ let ipld3: Ipld = DagJsonCodec.decode(json.to_string().as_bytes()).unwrap();
+ let Ipld::Bytes(_bytes) = ipld3.clone() else {
+ panic!("IPLD is not bytes");
+ };
+
+ let ty1 = test_utils::component::setup_component("(list u8)".to_string(), 8);
+ let val_list1 = ty1
+ .unwrap_list()
+ .new_val(Box::new([
+ Val::U8(104),
+ Val::U8(101),
+ Val::U8(108),
+ Val::U8(108),
+ Val::U8(48),
+ ]))
+ .unwrap();
+ let runtime1 = RuntimeVal::new(val_list1);
+
+ let ty2 = test_utils::component::setup_component("string".to_string(), 8);
+ let runtime2 = RuntimeVal::new(Val::String(Box::from("aGVsbDA")));
+
+ assert_eq!(
+ RuntimeVal::try_from(ipld1.clone(), &InterfaceType::Type(ty1)).unwrap(),
+ runtime1
+ );
+ assert_eq!(Ipld::try_from(runtime1).unwrap(), ipld1);
+
+ assert_eq!(
+ RuntimeVal::try_from(ipld1.clone(), &InterfaceType::Type(ty2.clone())).unwrap(),
+ runtime2
+ );
+ assert_eq!(Ipld::try_from(runtime2).unwrap(), ipld2);
+
+ let runtime3 = RuntimeVal::new(Val::String(Box::from("aGVsbDA")));
+ assert_eq!(
+ RuntimeVal::try_from(ipld3.clone(), &InterfaceType::Type(ty2)).unwrap(),
+ runtime3
+ );
+ assert_eq!(Ipld::try_from(runtime3).unwrap(), ipld2);
}
#[test]
@@ -1266,6 +1387,9 @@ mod test {
let ty3 = test_utils::component::setup_component("(result)".to_string(), 4);
let interface_ty3 = InterfaceType::Type(ty3.clone());
+ let ty4 = test_utils::component::setup_component("(result (error string))".to_string(), 12);
+ let interface_ty4 = InterfaceType::Type(ty4.clone());
+
let val1 = ty1
.unwrap_result()
.new_val(Ok(Some(Val::String(Box::from("Hello!")))))
@@ -1287,6 +1411,12 @@ mod test {
let val5 = ty1.unwrap_result().new_val(Err(None)).unwrap();
let runtime5 = RuntimeVal::new(val5);
+ let val6 = ty4.unwrap_result().new_val(Ok(None)).unwrap();
+ let runtime6 = RuntimeVal::new(val6);
+
+ let val7 = ty1.unwrap_result().new_val(Err(None)).unwrap();
+ let runtime7 = RuntimeVal::new(val7);
+
assert_eq!(
RuntimeVal::try_from(ok_ipld.clone(), &interface_ty1).unwrap(),
runtime1
@@ -1316,6 +1446,20 @@ mod test {
runtime5
);
assert_eq!(Ipld::try_from(runtime5).unwrap(), err_res_ipld);
+
+ // result with `_` any ok payload:
+ assert_eq!(
+ RuntimeVal::try_from(ok_ipld.clone(), &interface_ty4).unwrap(),
+ runtime6
+ );
+ assert_eq!(Ipld::try_from(runtime6).unwrap(), ok_res_ipld);
+
+ // result with `_` any err payload:
+ assert_eq!(
+ RuntimeVal::try_from(err_ipld.clone(), &interface_ty1).unwrap(),
+ runtime7
+ );
+ assert_eq!(Ipld::try_from(runtime7).unwrap(), err_res_ipld);
}
#[test]
@@ -1378,11 +1522,12 @@ mod test {
#[test]
fn try_flags_roundtrip() {
- let ipld = Ipld::List(vec![
+ let ipld1 = Ipld::List(vec![
Ipld::String("foo-bar-baz".into()),
Ipld::String("B".into()),
Ipld::String("C".into()),
]);
+ let ipld2 = Ipld::List(vec![Ipld::String("foo-bar-baz".into())]);
let ty = test_utils::component::setup_component(
r#"(flags "foo-bar-baz" "B" "C")"#.to_string(),
@@ -1390,18 +1535,24 @@ mod test {
);
let interface_ty = InterfaceType::Type(ty.clone());
- let val = ty
+ let val1 = ty
.unwrap_flags()
.new_val(&["foo-bar-baz", "B", "C"])
.unwrap();
+ let val2 = ty.unwrap_flags().new_val(&["foo-bar-baz"]).unwrap();
- let runtime = RuntimeVal::new(val);
+ let runtime1 = RuntimeVal::new(val1);
+ let runtime2 = RuntimeVal::new(val2);
assert_eq!(
- RuntimeVal::try_from(ipld.clone(), &interface_ty).unwrap(),
- runtime
+ RuntimeVal::try_from(ipld1.clone(), &interface_ty).unwrap(),
+ runtime1
);
-
- assert_eq!(Ipld::try_from(runtime).unwrap(), ipld);
+ assert_eq!(Ipld::try_from(runtime1).unwrap(), ipld1);
+ assert_eq!(
+ RuntimeVal::try_from(ipld2.clone(), &interface_ty).unwrap(),
+ runtime2
+ );
+ assert_eq!(Ipld::try_from(runtime2).unwrap(), ipld2);
}
}
diff --git a/homestar-wasm/src/wasmtime/world.rs b/homestar-wasm/src/wasmtime/world.rs
index c30c43a2..41474d66 100644
--- a/homestar-wasm/src/wasmtime/world.rs
+++ b/homestar-wasm/src/wasmtime/world.rs
@@ -13,7 +13,7 @@ use crate::{
Error,
},
};
-use heck::{ToKebabCase, ToSnakeCase};
+use heck::{ToKebabCase, ToLowerCamelCase, ToPascalCase, ToSnakeCase};
use homestar_invocation::{
bail,
error::ResolveError,
@@ -384,6 +384,11 @@ impl World {
.func(fun_name)
.or_else(|| __exports.func(&fun_name.to_kebab_case()))
.or_else(|| __exports.func(&fun_name.to_snake_case()))
+ .or_else(|| __exports.func(&fun_name.to_lower_camel_case()))
+ .or_else(|| __exports.func(&fun_name.to_pascal_case()))
+ // Support identifiers
+ // https://github.com/WebAssembly/component-model/blob/main/design/mvp/WIT.md#identifiers
+ .or_else(|| __exports.func(format!("%{}", fun_name).as_str()))
.ok_or_else(|| Error::WasmFunctionNotFound(fun_name.to_string()))?;
Ok(World(func))