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 logo -

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))