Skip to content
Open
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
1 change: 1 addition & 0 deletions design/mvp/Binary.md
Original file line number Diff line number Diff line change
Expand Up @@ -206,6 +206,7 @@ defvaltype ::= pvt:<primvaltype> => pvt
| 0x68 i:<typeidx> => (borrow i)
| 0x66 t?:<valtype>? => (stream t?) 🔀
| 0x65 t?:<valtype>? => (future t?) 🔀
| 0x63 k:<valtype> v:<valtype> => (map k v) (if k is in <keytype>) 🗺️
labelvaltype ::= l:<label'> t:<valtype> => l t
case ::= l:<label'> t?:<valtype>? 0x00 => (case l t?)
label' ::= len:<u32> l:<label> => l (if len = |l|)
Expand Down
1 change: 1 addition & 0 deletions design/mvp/CanonicalABI.md
Original file line number Diff line number Diff line change
Expand Up @@ -1661,6 +1661,7 @@ def despecialize(t):
case EnumType(labels) : return VariantType([ CaseType(l, None) for l in labels ])
case OptionType(t) : return VariantType([ CaseType("none", None), CaseType("some", t) ])
case ResultType(ok, err) : return VariantType([ CaseType("ok", ok), CaseType("error", err) ])
case MapType(k, v) : return ListType(despecialize(TupleType([k, v])))
case _ : return t
```
The specialized value types `string` and `flags` are missing from this list
Expand Down
13 changes: 13 additions & 0 deletions design/mvp/Explainer.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ implemented, considered stable and included in a future milestone:
* 🔧: fixed-length lists
* 📝: the `error-context` type
* 🔗: canonical interface names
* 🗺️: `map` type

(Based on the previous [scoping and layering] proposal to the WebAssembly CG,
this repo merges and supersedes the [module-linking] and [interface-types]
Expand Down Expand Up @@ -560,12 +561,14 @@ defvaltype ::= bool
| (enum "<label>"+)
| (option <valtype>)
| (result <valtype>? (error <valtype>)?)
| (map <keytype> <valtype>) 🗺️
| (own <typeidx>)
| (borrow <typeidx>)
| (stream <typeidx>?) 🔀
| (future <typeidx>?) 🔀
valtype ::= <typeidx>
| <defvaltype>
keytype ::= bool | s8 | u8 | s16 | u16 | s32 | u32 | s64 | u64 | char | string 🗺️
resourcetype ::= (resource (rep i32) (dtor async? <funcidx> (callback <funcidx>)?)?)
functype ::= (func (param "<label>" <valtype>)* (result <valtype>)?)
componenttype ::= (component <componentdecl>*)
Expand Down Expand Up @@ -765,6 +768,7 @@ defined by the following mapping:
(option <valtype>) ↦ (variant (case "none") (case "some" <valtype>))
(result <valtype>? (error <valtype>)?) ↦ (variant (case "ok" <valtype>?) (case "error" <valtype>?))
string ↦ (list char)
(map <keytype> <valtype>) ↦ (list (tuple <keytype> <valtype>))
```

Specialized value types have the same set of semantic values as their
Expand All @@ -780,6 +784,14 @@ this can sometimes allow values to be represented differently. For example,
`flags` in the Canonical ABI uses a bit-vector while an equivalent record
of boolean fields uses a sequence of boolean-valued bytes.

Since a `map` is a specialization of a list of (key, value) pairs without any
additional semantic guarantee of key uniqueness, the Component Model does not
forcibly prevent duplicate keys from appearing in the list. In the case of
duplicate keys, the expectation for bindings generators is that for any given
key, the *last* (key, value) pair in the list defines the value of the key in
the map. To simplify bindings generation, `<keytype>`s is a conservative subset
Comment on lines +789 to +792
Copy link
Contributor

@lann lann Aug 20, 2025

Choose a reason for hiding this comment

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

the expectation for bindings generators

Can we find a way to make this language stronger? Imagine some "authorization middleware" component sits on an interface and inspects a call with a map param. If the value {"action": "view", "action": "delete"} comes through it is going to be very important that the middleware treats the map exactly the same as the next component.

Copy link
Member Author

Choose a reason for hiding this comment

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

Yeah, I suppose "expectation" is too weak; would it make sense to say "Although the Component Model cannot enforce this property, bindings generators MUST ..."?

Copy link

Choose a reason for hiding this comment

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

I see two things about the current map specification that can lead to problems.

First, allowing duplicate keys and expect bindings to just ignore the last occurrence. I think uniqueness of keys should be mandated by the spec and enforced at the boundary.

The second is that the order of entries is propagated, but some languages’ (standard) Map type(s) will not preserve this. In my opinion, it should be expected that bindings will map map to a type that retains order. It would then also be a good idea to rename to something else (e.g. dict, ordered-map) so there is less chance of confusion with a conventional (non order preserving) Map type.

Copy link
Member Author

Choose a reason for hiding this comment

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

Yeah, it's a tradeoff to be sure. However, if map forces the runtime to build a temporary hash set (to enforce uniqueness) that is pure overhead (and potentially a pretty non-trivial runtime-internal memory allocation, which we otherwise avoid in the CABI), interface designers will have to ask whether they can "afford" to use map or whether they should use list<tuple<K,V>> instead for performance reasons, which seems net worse.

Copy link

Choose a reason for hiding this comment

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

  1. I have no issue with mandating uniqueness in the spec. It's always easier to relax constraints than to tighten them, anyway, so might as well be more constrained up front. However, when that constraint is violated by a misbehaving lifting component, I think the behavior should be the same for the lowering component; just ignore all but the final value. I don't think we should do the C thing and call it undefined behavior (unless that's normal for the component model?) and I don't think the lowering should trap. So this is just a question of semantics rather than behavior.

  2. I don't think the basic map type should be ordered. Most languages do not use an ordered basic map type because 9 times out of 10 you don't need an ordering for your maps. In the cases where you do need an ordered map, you could always fall back in list<tuple<key, value>>. I don't think it's super important to create a specialization for an ordered map as well, but that could be revisited later.

Copy link
Contributor

Choose a reason for hiding this comment

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

deterministic profile subsets this by sorting

Could you say more about why this would be helpful? It isn't immediately clear to me that it would be worth the runtime cost.

Copy link
Contributor

Choose a reason for hiding this comment

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

Does that sound right?

Pretty much, yeah. 👍

Copy link
Member Author

Choose a reason for hiding this comment

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

@lann Since the deterministic profile can't randomly permute, if we don't normalize order in the deterministic profile, then that effectively makes order an observable part of the semantics of map values. But yeah, I suppose in some cases the performance might be a problem, even for the deterministic profile, so it's a tradeoff worth discussing.

Copy link
Contributor

Choose a reason for hiding this comment

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

Ah I see, you're talking about baking unorderedness (and presumably dedupedness?) into the map lowering semantics while I was only thinking of making the bindings generation guidelines language stronger.

I see the benefit of formalizing it but yeah, requiring sorting on every map lowering seems quite a different order of tradeoff than NaN canonicalization. 🙂

Do you have any references on high-level motivations / use-cases for deterministic profiles? It seems difficult to evaluate this kind of tradeoff without that context.

Copy link
Member Author

Choose a reason for hiding this comment

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

The use case for the deterministic profile (introduced in the 3.0 draft here as part of the relaxed SIMD instructions) is just to define, if you want to run wasm deterministically, here's how to do it. If we don't specify sort+dedupe, that means that, even when running deterministically, a component can produce different outputs for the input {a:1, b:2} vs {b:2, a:1} which means that these two values must be considered unequal if you're, e.g., caching outputs keyed by inputs. That's a corollary, but I don't know how much of a problem it is.

of `<valtype>`, but this subset could be expanded over time based on use cases.

Note that, at least initially, variants are required to have a non-empty list of
cases. This could be relaxed in the future to allow an empty list of cases, with
the empty `(variant)` effectively serving as an [empty type] and indicating
Expand Down Expand Up @@ -2793,6 +2805,7 @@ At a high level, the additional coercions would be:
| `enum` | same as [`enum`] | same as [`enum`] |
| `option` | same as [`T?`] | same as [`T?`] |
| `result` | same as `variant`, but coerce a top-level `error` return value to a thrown exception | same as `variant`, but coerce uncaught exceptions to top-level `error` return values |
| `map` | `new Map(_)` | `Map`s directly or other objects via `Object.entries(_)` |
Copy link

Choose a reason for hiding this comment

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

ToJSValue could a plain JS object as well, not sure if matters here

Copy link
Member Author

Choose a reason for hiding this comment

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

That's worth discussing with JS folks, but the hazard I was remembering with plain JS objects is that, when used for arbitrary keys, there can be conflicts with built-in properties (e.g., __proto__), which was one of the original motivations for adding Map. (Also, objects only work with the keytype is string.)

| `own`, `borrow` | see below | see below |
| `future` | to a `Promise` | from a `Promise` |
| `stream` | to a `ReadableStream` | from a `ReadableStream` |
Expand Down
12 changes: 12 additions & 0 deletions design/mvp/WIT.md
Original file line number Diff line number Diff line change
Expand Up @@ -1021,6 +1021,7 @@ keyword ::= 'as'
| 'include'
| 'interface'
| 'list'
| 'map'
| 'option'
| 'own'
| 'package'
Expand Down Expand Up @@ -1712,6 +1713,7 @@ ty ::= 'u8' | 'u16' | 'u32' | 'u64'
| list
| option
| result
| map
| handle
| future
| stream
Expand All @@ -1733,6 +1735,11 @@ result ::= 'result' '<' ty ',' ty '>'
| 'result' '<' ty '>'
| 'result'

map ::= 'map' '<' kt ',' ty '>'
kt ::= 'u8' | 'u16' | 'u32' | 'u64'
| 's8' | 's16' | 's32' | 's64'
| 'char' | 'bool' | 'string'

future ::= 'future' '<' ty '>'
| 'future'

Expand Down Expand Up @@ -1772,6 +1779,11 @@ variant result {
These types are so frequently used and frequently have language-specific
meanings though so they're also provided as first-class types.

🗺️ The `map` type is semantically equivalent to a list of pairs of keys and
values but is meant to be represented by bindings generators in the source
language as a mapping from keys to values (e.g., as an associative array or or
hash table) where, in the case of duplicate keys, the last key's value is used.

The `future` and `stream` types are described as part of the [async
explainer](Async.md#streams-and-futures).

Expand Down
6 changes: 6 additions & 0 deletions design/mvp/canonical-abi/definitions.py
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,11 @@ class ResultType(ValType):
ok: Optional[ValType]
error: Optional[ValType]

@dataclass
class MapType(ValType):
k: ValType # <keytype>
v: ValType

@dataclass
class FlagsType(ValType):
labels: list[str]
Expand Down Expand Up @@ -974,6 +979,7 @@ def despecialize(t):
case EnumType(labels) : return VariantType([ CaseType(l, None) for l in labels ])
case OptionType(t) : return VariantType([ CaseType("none", None), CaseType("some", t) ])
case ResultType(ok, err) : return VariantType([ CaseType("ok", ok), CaseType("error", err) ])
case MapType(k, v) : return ListType(despecialize(TupleType([k, v])))
case _ : return t

### Type Predicates
Expand Down
4 changes: 4 additions & 0 deletions design/mvp/canonical-abi/run_tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -343,6 +343,10 @@ def test_heap(t, expect, args, byte_array):
test_heap(t, v, [0,2],
[0xff,0xff,0xff,0xff, 0,0,0,0])

t = MapType(U8Type(), U16Type())
test_heap(t, [{'0':42, '1':83}, {'0':43, '1':84}], [0, 2],
[42,0xff,83,0, 43,0xff,84,0])

def test_flatten(t, params, results):
expect = CoreFuncType(params, results)

Expand Down