Skip to main content

submilli_engine/runtime/prelude/map/
mod.rs

1//! The submilli `Map<K, V>` — the Rust port of the prelude's hand-written Wasm
2//! hashtable (`codegen/prelude/map.rs`).
3//!
4//! The host owns the map end-to-end: it builds the backing struct, drives the
5//! open-addressing probe sequence, the insertion-order ledger, tombstones, and
6//! resize. Storage stays in the GC heap (`$MapBacking { vtable, keys, values,
7//! size, order, order_len, hashes, order_positions, identity }`); host fns read and write it through the struct ABI
8//! rather than holding a Rust `HashMap`.
9//!
10//! Keys hash and compare through the object vtable — slot 3 (`hash`) and slot 2
11//! (`equals`) via [`dispatch_vtable_slot`] — so `get`/`set`/`has`/`delete` are
12//! async (the dispatch may re-enter the guest for user-class keys). For
13//! string/array/number keys the slots are host fns, so the dispatch resolves
14//! without truly suspending. Cached bucket hashes avoid rehashing keys on
15//! resize; reverse ledger positions make deletion independent of ledger length.
16//!
17//! Re-entrancy is closed by construction: `equals`/`hash` for user subtypes are
18//! compiler-generated structural functions that recurse only into other
19//! structural `equals`/`hash`, never back into `Map.set`/etc. So a mid-probe
20//! dispatch can't mutate the map; no in-flight-mutation guard is needed.
21
22use crate::runtime::host::{abi_arg, abi_result};
23mod install;
24
25pub(crate) use install::declare_types;
26pub use install::{declare, install};
27
28use wasmtime::{
29    ArrayRef, ArrayRefPre, ArrayType, Caller, FieldType, Finality, Global, GlobalType, HeapType,
30    Mutability, RefType, Rooted, StorageType, Store, StructRef, StructRefPre, StructType, Val,
31    ValType,
32};
33
34use crate::runtime::StoreData;
35use crate::runtime::fuel;
36use crate::runtime::gc_singleton::{singleton_array, singleton_struct};
37use crate::runtime::host::{host_map_tombstone, host_object_vtable, write_submilli_array_struct};
38use crate::runtime::intrinsic_types::{IntrinsicTypes, intrinsic_types};
39use crate::runtime::prelude::closure::{self, Closure};
40use crate::runtime::prelude::collection::{
41    both_nan, decode_key, encode_key, is_null_key, probe_capacity, rehash_capacity,
42};
43use crate::runtime::prelude::collection::{is_a, object_field, read_array_vals, unbox_bool};
44use crate::runtime::prelude::iterator::{
45    IterKind, IteratorSource, build_iterator, iter_done, iter_yield, next_closure_type, shared_next,
46};
47use crate::runtime::prelude::keep::KeptValue;
48use crate::runtime::prelude::ledger::{
49    LedgerCursor, LedgerFields, forward_cleared, forward_rehashed,
50};
51use crate::runtime::prelude::vtable::dispatch_vtable_slot;
52
53/// The map's initial bucket capacity (must stay a power of two for the
54/// `& (cap - 1)` probe mask).
55const INITIAL_CAPACITY: i32 = 8;
56
57// ---------------------------------------------------------------------------
58// Backing types (shared with codegen via WasmGC canonicalization)
59// ---------------------------------------------------------------------------
60
61/// `$rawIndexArray` — `(array (mut i32))`, the insertion-order ledger element
62/// type. A standalone final array, so it canonicalizes with codegen's type.
63pub(crate) fn raw_index_array_type(engine: &wasmtime::Engine) -> wasmtime::Result<ArrayType> {
64    singleton_array(
65        engine,
66        Finality::Final,
67        FieldType::new(Mutability::Var, StorageType::ValType(ValType::I32)),
68    )
69}
70
71/// `$MapBacking` — a non-final `$Object` subtype
72/// `{ vtable, keys, values, size, order, order_len, hashes, order_positions, identity }`, mirroring
73/// `declare_intrinsic_types` in codegen. Built as a singleton so it
74/// canonicalizes to the same engine type the guest's `ref.cast` targets.
75/// [`map_backing_matches_codegen`] pins the agreement.
76pub(crate) fn map_backing_struct(
77    engine: &wasmtime::Engine,
78    intr: &IntrinsicTypes,
79) -> wasmtime::Result<StructType> {
80    let imm = Mutability::Const;
81    let mutv = Mutability::Var;
82    let raw_index = raw_index_array_type(engine)?;
83    singleton_struct(
84        engine,
85        Finality::NonFinal,
86        Some(intr.object.clone()),
87        vec![
88            FieldType::new(
89                imm,
90                StorageType::ValType(ValType::Ref(RefType::new(
91                    false,
92                    intr.vtable.clone().into(),
93                ))),
94            ),
95            FieldType::new(
96                mutv,
97                StorageType::ValType(ValType::Ref(RefType::new(
98                    false,
99                    intr.raw_array.clone().into(),
100                ))),
101            ),
102            FieldType::new(
103                mutv,
104                StorageType::ValType(ValType::Ref(RefType::new(
105                    false,
106                    intr.raw_array.clone().into(),
107                ))),
108            ),
109            FieldType::new(mutv, StorageType::ValType(ValType::I32)),
110            FieldType::new(
111                mutv,
112                StorageType::ValType(ValType::Ref(RefType::new(false, raw_index.clone().into()))),
113            ),
114            FieldType::new(mutv, StorageType::ValType(ValType::I32)),
115            FieldType::new(
116                mutv,
117                StorageType::ValType(ValType::Ref(RefType::new(false, raw_index.clone().into()))),
118            ),
119            FieldType::new(
120                mutv,
121                StorageType::ValType(ValType::Ref(RefType::new(false, raw_index.into()))),
122            ),
123            FieldType::new(mutv, StorageType::ValType(ValType::I64)),
124        ],
125    )
126}
127
128/// Build the per-store tombstone sentinel — a bare `$Object { object_vtable }`
129/// that marks a deleted probe slot (distinct from `null` = empty and from any
130/// real key). Created once at install and cached in [`HostAbi`]; the probe logic
131/// compares slots against it by reference identity.
132pub(crate) fn build_tombstone(
133    store: &mut Store<StoreData>,
134    object_ty: StructType,
135    object_vtable: Global,
136) -> wasmtime::Result<Global> {
137    let vtable_val = object_vtable.get(&mut *store);
138    let pre = StructRefPre::new(&mut *store, object_ty);
139    let sentinel = StructRef::new(&mut *store, &pre, &[vtable_val])?;
140    let gty = GlobalType::new(
141        ValType::Ref(RefType::new(false, HeapType::Any)),
142        Mutability::Const,
143    );
144    Global::new(&mut *store, gty, Val::AnyRef(Some(sentinel.to_anyref())))
145}
146
147// Field indices into `$MapBacking`.
148const F_KEYS: usize = 1;
149const F_VALUES: usize = 2;
150const F_SIZE: usize = 3;
151const F_ORDER: usize = 4;
152const F_ORDER_LEN: usize = 5;
153const F_HASHES: usize = 6;
154const F_ORDER_POSITIONS: usize = 7;
155
156/// The map's `$MapBacking` receiver, cast from the erased `(ref $Object)`.
157fn backing(caller: &mut Caller<'_, StoreData>, recv: &Val) -> wasmtime::Result<Rooted<StructRef>> {
158    let Val::AnyRef(Some(any)) = recv else {
159        return Err(wasmtime::Error::msg("Map method: receiver is null"));
160    };
161    any.as_struct(&mut *caller)?
162        .ok_or_else(|| wasmtime::Error::msg("Map method: receiver is not a $MapBacking"))
163}
164
165/// Read a `$rawArray`/`$rawIndexArray` field of the backing.
166fn field_array(
167    caller: &mut Caller<'_, StoreData>,
168    b: &Rooted<StructRef>,
169    idx: usize,
170) -> wasmtime::Result<Rooted<ArrayRef>> {
171    match b.field(&mut *caller, idx)? {
172        Val::AnyRef(Some(a)) => a.unwrap_array(&mut *caller),
173        other => Err(wasmtime::Error::msg(format!(
174            "Map backing field {idx} is not an array: {other:?}"
175        ))),
176    }
177}
178
179/// Read an i32 field (`size` / `order_len`) of the backing.
180fn field_i32(
181    caller: &mut Caller<'_, StoreData>,
182    b: &Rooted<StructRef>,
183    idx: usize,
184) -> wasmtime::Result<i32> {
185    match b.field(&mut *caller, idx)? {
186        Val::I32(n) => Ok(n),
187        other => Err(wasmtime::Error::msg(format!(
188            "Map backing field {idx} is not an i32: {other:?}"
189        ))),
190    }
191}
192
193/// Whether `slot` is the tombstone sentinel.
194fn is_tombstone(caller: &mut Caller<'_, StoreData>, slot: &Val) -> wasmtime::Result<bool> {
195    let tomb = host_map_tombstone(caller)?;
196    match (slot, &tomb) {
197        (Val::AnyRef(Some(a)), Val::AnyRef(Some(b))) => Ok(Rooted::ref_eq(&caller, a, b)?),
198        _ => Ok(false),
199    }
200}
201
202fn is_null(v: &Val) -> bool {
203    matches!(v, Val::AnyRef(None))
204}
205
206/// `key.vtable.hash(key)` (slot 3).
207async fn hash(caller: &mut Caller<'_, StoreData>, key: &Val) -> wasmtime::Result<i32> {
208    if is_null_key(caller, key)? {
209        return Ok(0);
210    }
211    if super::undefined::is_undefined(caller, key)? {
212        return Ok(1);
213    }
214    match dispatch_vtable_slot(caller, key, 3, &[]).await? {
215        Val::I32(h) => Ok(h),
216        other => Err(wasmtime::Error::msg(format!(
217            "Map key hash returned {other:?}, expected i32"
218        ))),
219    }
220}
221
222/// `key.vtable.equals(key, slot)` (slot 2).
223async fn equals(
224    caller: &mut Caller<'_, StoreData>,
225    key: &Val,
226    slot: &Val,
227) -> wasmtime::Result<bool> {
228    let left_null = is_null_key(caller, key)?;
229    let right_null = is_null_key(caller, slot)?;
230    if left_null || right_null {
231        return Ok(left_null && right_null);
232    }
233    let left_undefined = super::undefined::is_undefined(caller, key)?;
234    let right_undefined = super::undefined::is_undefined(caller, slot)?;
235    if left_undefined || right_undefined {
236        return Ok(left_undefined && right_undefined);
237    }
238    if both_nan(caller, key, slot)? {
239        return Ok(true);
240    }
241    match dispatch_vtable_slot(caller, key, 2, &[*slot]).await? {
242        Val::I32(b) => Ok(b != 0),
243        other => Err(wasmtime::Error::msg(format!(
244            "Map key equals returned {other:?}, expected i32"
245        ))),
246    }
247}
248
249fn index_value(
250    caller: &mut Caller<'_, StoreData>,
251    array: &Rooted<ArrayRef>,
252    index: u32,
253) -> wasmtime::Result<i32> {
254    #[cfg(test)]
255    {
256        caller.data_mut().collection_index_reads += 1;
257    }
258    match array.get(&mut *caller, index)? {
259        Val::I32(value) => Ok(value),
260        _ => Err(crate::runtime::host::fatal_host_error(
261            "Invalid collection index/hash array",
262        )),
263    }
264}
265
266/// A fresh `$rawArray` of `n` null slots.
267fn new_raw_array(caller: &mut Caller<'_, StoreData>, n: i32) -> wasmtime::Result<Rooted<ArrayRef>> {
268    let raw = intrinsic_types(&mut *caller)?.raw_array.clone();
269    let pre = ArrayRefPre::new(&mut *caller, raw);
270    let capacity =
271        u32::try_from(n).map_err(|_| wasmtime::Error::msg("Negative collection capacity"))?;
272    ArrayRef::new(&mut *caller, &pre, &Val::null_any_ref(), capacity)
273}
274
275/// A fresh `$rawIndexArray` of `n` zero slots.
276fn new_index_array(
277    caller: &mut Caller<'_, StoreData>,
278    n: i32,
279) -> wasmtime::Result<Rooted<ArrayRef>> {
280    let ty = raw_index_array_type(caller.engine())?;
281    let pre = ArrayRefPre::new(&mut *caller, ty);
282    let capacity =
283        u32::try_from(n).map_err(|_| wasmtime::Error::msg("Negative collection capacity"))?;
284    ArrayRef::new(&mut *caller, &pre, &Val::I32(0), capacity)
285}
286
287// ---------------------------------------------------------------------------
288// Data methods
289// ---------------------------------------------------------------------------
290
291/// `Map#get(self, key) -> V | undefined`.
292pub(super) async fn get(
293    caller: &mut Caller<'_, StoreData>,
294    recv: &Val,
295    key: &Val,
296) -> wasmtime::Result<Val> {
297    let encoded_key = encode_key(caller, key)?;
298    let key = &encoded_key;
299    let b = backing(caller, recv)?;
300    let Some(index) = find_slot(caller, &b, key).await? else {
301        return super::undefined::value(caller);
302    };
303    field_array(caller, &b, F_VALUES)?.get(&mut *caller, index)
304}
305
306/// `Map#has(self, key) -> boolean`.
307pub(super) async fn has(
308    caller: &mut Caller<'_, StoreData>,
309    recv: &Val,
310    key: &Val,
311) -> wasmtime::Result<bool> {
312    let encoded_key = encode_key(caller, key)?;
313    let b = backing(caller, recv)?;
314    Ok(find_slot(caller, &b, &encoded_key).await?.is_some())
315}
316
317/// Search at most one full probe cycle, including tables with no empty bucket.
318async fn find_slot(
319    caller: &mut Caller<'_, StoreData>,
320    b: &Rooted<StructRef>,
321    key: &Val,
322) -> wasmtime::Result<Option<u32>> {
323    let key_hash = hash(caller, key).await?;
324    find_slot_hashed(caller, b, key, key_hash).await
325}
326
327async fn find_slot_hashed(
328    caller: &mut Caller<'_, StoreData>,
329    b: &Rooted<StructRef>,
330    key: &Val,
331    key_hash: i32,
332) -> wasmtime::Result<Option<u32>> {
333    let keys = field_array(caller, b, F_KEYS)?;
334    let cap = probe_capacity(keys.len(&mut *caller)?)?;
335    let hashes = field_array(caller, b, F_HASHES)?;
336    let mut i = key_hash & (cap - 1);
337    for _ in 0..cap {
338        fuel::charge(&mut *caller, fuel::ELEM, 1)?;
339        let slot = keys.get(&mut *caller, i as u32)?;
340        if is_null(&slot) {
341            return Ok(None);
342        }
343        if !is_tombstone(caller, &slot)?
344            && index_value(caller, &hashes, i as u32)? == key_hash
345            && equals(caller, key, &slot).await?
346        {
347            return Ok(Some(i as u32));
348        }
349        i = (i + 1) & (cap - 1);
350    }
351    Ok(None)
352}
353
354/// `Map#set(self, key, value) -> self`. Resizes/compacts to keep a free slot,
355/// then probes; an existing key overwrites in place (insertion order kept), a
356/// new key takes the first tombstone or empty slot and appends to the ledger.
357pub(super) async fn set(
358    caller: &mut Caller<'_, StoreData>,
359    recv: &Val,
360    key: &Val,
361    value: &Val,
362) -> wasmtime::Result<Val> {
363    let encoded_key = encode_key(caller, key)?;
364    let key = &encoded_key;
365    let b = backing(caller, recv)?;
366    let key_hash = hash(caller, key).await?;
367
368    let size = field_i32(caller, &b, F_SIZE)?;
369    let cap0 = field_array(caller, &b, F_KEYS)?.len(&mut *caller)?;
370    let order_len = field_i32(caller, &b, F_ORDER_LEN)?;
371    if let Some(capacity) = rehash_capacity(cap0, size, order_len)? {
372        if let Some(index) = find_slot_hashed(caller, &b, key, key_hash).await? {
373            let values = field_array(caller, &b, F_VALUES)?;
374            values.set(&mut *caller, index, *value)?;
375            return Ok(*recv);
376        }
377        rehash(caller, &b, capacity)?;
378    }
379
380    let keys = field_array(caller, &b, F_KEYS)?;
381    let values = field_array(caller, &b, F_VALUES)?;
382    let order = field_array(caller, &b, F_ORDER)?;
383    let order_len = field_i32(caller, &b, F_ORDER_LEN)?;
384    let cap = probe_capacity(keys.len(&mut *caller)?)?;
385
386    let hashes = field_array(caller, &b, F_HASHES)?;
387    let positions = field_array(caller, &b, F_ORDER_POSITIONS)?;
388    let mut i = key_hash & (cap - 1);
389    let mut first_tomb: i32 = -1;
390    for _ in 0..cap {
391        fuel::charge(&mut *caller, fuel::ELEM, 1)?;
392        let slot = keys.get(&mut *caller, i as u32)?;
393        if is_null(&slot) {
394            let ins = if first_tomb == -1 { i } else { first_tomb };
395            keys.set(&mut *caller, ins as u32, *key)?;
396            values.set(&mut *caller, ins as u32, *value)?;
397            order.set(&mut *caller, order_len as u32, Val::I32(ins))?;
398            hashes.set(&mut *caller, ins as u32, Val::I32(key_hash))?;
399            positions.set(&mut *caller, ins as u32, Val::I32(order_len))?;
400            b.set_field(&mut *caller, F_ORDER_LEN, Val::I32(order_len + 1))?;
401            b.set_field(&mut *caller, F_SIZE, Val::I32(size + 1))?;
402            return Ok(*recv);
403        }
404        if is_tombstone(caller, &slot)? {
405            if first_tomb == -1 {
406                first_tomb = i;
407            }
408        } else if index_value(caller, &hashes, i as u32)? == key_hash
409            && equals(caller, key, &slot).await?
410        {
411            values.set(&mut *caller, i as u32, *value)?;
412            return Ok(*recv);
413        }
414        i = (i + 1) & (cap - 1);
415    }
416    Err(wasmtime::Error::msg("Map insertion found no empty bucket"))
417}
418
419/// `Map#delete(self, key) -> boolean`. Tombstones the slot and marks its ledger
420/// entry `-1`.
421pub(super) async fn delete(
422    caller: &mut Caller<'_, StoreData>,
423    recv: &Val,
424    key: &Val,
425) -> wasmtime::Result<bool> {
426    let encoded_key = encode_key(caller, key)?;
427    let key = &encoded_key;
428    let b = backing(caller, recv)?;
429    let keys = field_array(caller, &b, F_KEYS)?;
430    let values = field_array(caller, &b, F_VALUES)?;
431    let order = field_array(caller, &b, F_ORDER)?;
432    let positions = field_array(caller, &b, F_ORDER_POSITIONS)?;
433    let Some(index) = find_slot(caller, &b, key).await? else {
434        return Ok(false);
435    };
436    let position = index_value(caller, &positions, index)?;
437    let position = u32::try_from(position).map_err(crate::runtime::host::fatal_host_error)?;
438    let size = field_i32(caller, &b, F_SIZE)?;
439    let remaining = size.checked_sub(1).filter(|n| *n >= 0).ok_or_else(|| {
440        crate::runtime::host::fatal_host_error("Invalid collection size on deletion")
441    })?;
442    let tomb = host_map_tombstone(caller)?;
443    keys.set(&mut *caller, index, tomb)?;
444    values.set(&mut *caller, index, Val::null_any_ref())?;
445    order.set(&mut *caller, position, Val::I32(-1))?;
446    b.set_field(&mut *caller, F_SIZE, Val::I32(remaining))?;
447    Ok(true)
448}
449
450/// `Map#clear(self) -> void`. Swaps in fresh empty backing arrays.
451/// `Map#size` — the entry count as a `number`; reads the backing's `size` field.
452pub(super) fn size(caller: &mut Caller<'_, StoreData>, recv: &Val) -> wasmtime::Result<Val> {
453    let b = backing(caller, recv)?;
454    let n = field_i32(caller, &b, F_SIZE)?;
455    Ok(Val::F64((n as f64).to_bits()))
456}
457
458pub(super) fn clear(caller: &mut Caller<'_, StoreData>, recv: &Val) -> wasmtime::Result<()> {
459    let b = backing(caller, recv)?;
460    let old_keys = field_array(caller, &b, F_KEYS)?;
461    let old_order = field_array(caller, &b, F_ORDER)?;
462    let old_order_len = field_i32(caller, &b, F_ORDER_LEN)?;
463    let keys = new_raw_array(caller, INITIAL_CAPACITY)?;
464    let values = new_raw_array(caller, INITIAL_CAPACITY)?;
465    let order = new_index_array(caller, INITIAL_CAPACITY)?;
466    let hashes = new_index_array(caller, INITIAL_CAPACITY)?;
467    let positions = new_index_array(caller, INITIAL_CAPACITY)?;
468    b.set_field(&mut *caller, F_KEYS, Val::AnyRef(Some(keys.to_anyref())))?;
469    b.set_field(
470        &mut *caller,
471        F_VALUES,
472        Val::AnyRef(Some(values.to_anyref())),
473    )?;
474    b.set_field(&mut *caller, F_SIZE, Val::I32(0))?;
475    b.set_field(&mut *caller, F_ORDER, Val::AnyRef(Some(order.to_anyref())))?;
476    b.set_field(&mut *caller, F_ORDER_LEN, Val::I32(0))?;
477    b.set_field(
478        &mut *caller,
479        F_HASHES,
480        Val::AnyRef(Some(hashes.to_anyref())),
481    )?;
482    b.set_field(
483        &mut *caller,
484        F_ORDER_POSITIONS,
485        Val::AnyRef(Some(positions.to_anyref())),
486    )?;
487    forward_cleared(caller, &old_keys, &old_order, old_order_len, &keys, &order)
488}
489
490/// Rehash live entries into fresh arrays at the requested capacity, rebuilding the
491/// insertion-order ledger from the old one (skipping `-1` tombstones).
492fn rehash(
493    caller: &mut Caller<'_, StoreData>,
494    b: &Rooted<StructRef>,
495    new_cap: i32,
496) -> wasmtime::Result<()> {
497    let old_keys = field_array(caller, b, F_KEYS)?;
498    let old_vals = field_array(caller, b, F_VALUES)?;
499    let old_order = field_array(caller, b, F_ORDER)?;
500    let old_hashes = field_array(caller, b, F_HASHES)?;
501    let old_order_len = field_i32(caller, b, F_ORDER_LEN)?;
502    // Fresh keys and values arrays, then one reinsertion per ledger entry.
503    fuel::charge(
504        &mut *caller,
505        fuel::ELEM,
506        (new_cap as u64)
507            .saturating_mul(5)
508            .saturating_add(old_order_len as u64),
509    )?;
510
511    let new_keys = new_raw_array(caller, new_cap)?;
512    let new_vals = new_raw_array(caller, new_cap)?;
513    let new_order = new_index_array(caller, new_cap)?;
514    let new_hashes = new_index_array(caller, new_cap)?;
515    let new_positions = new_index_array(caller, new_cap)?;
516    let mut new_order_len: i32 = 0;
517
518    for o in 0..old_order_len {
519        let Val::I32(probe_idx) = old_order.get(&mut *caller, o as u32)? else {
520            return Err(wasmtime::Error::msg("Invalid collection ledger entry"));
521        };
522        if probe_idx == -1 {
523            continue;
524        }
525        let key = old_keys.get(&mut *caller, probe_idx as u32)?;
526        let val = old_vals.get(&mut *caller, probe_idx as u32)?;
527        let key_hash = index_value(caller, &old_hashes, probe_idx as u32)?;
528        let mut k = key_hash & (new_cap - 1);
529        let mut inserted = false;
530        for _ in 0..new_cap {
531            fuel::charge(&mut *caller, fuel::ELEM, 1)?;
532            if is_null(&new_keys.get(&mut *caller, k as u32)?) {
533                new_keys.set(&mut *caller, k as u32, key)?;
534                new_vals.set(&mut *caller, k as u32, val)?;
535                new_order.set(&mut *caller, new_order_len as u32, Val::I32(k))?;
536                new_hashes.set(&mut *caller, k as u32, Val::I32(key_hash))?;
537                new_positions.set(&mut *caller, k as u32, Val::I32(new_order_len))?;
538                new_order_len += 1;
539                inserted = true;
540                break;
541            }
542            k = (k + 1) & (new_cap - 1);
543        }
544        if !inserted {
545            return Err(wasmtime::Error::msg("Map rehash found no empty bucket"));
546        }
547    }
548
549    b.set_field(
550        &mut *caller,
551        F_KEYS,
552        Val::AnyRef(Some(new_keys.to_anyref())),
553    )?;
554    b.set_field(
555        &mut *caller,
556        F_VALUES,
557        Val::AnyRef(Some(new_vals.to_anyref())),
558    )?;
559    b.set_field(
560        &mut *caller,
561        F_ORDER,
562        Val::AnyRef(Some(new_order.to_anyref())),
563    )?;
564    b.set_field(&mut *caller, F_ORDER_LEN, Val::I32(new_order_len))?;
565    b.set_field(
566        &mut *caller,
567        F_HASHES,
568        Val::AnyRef(Some(new_hashes.to_anyref())),
569    )?;
570    b.set_field(
571        &mut *caller,
572        F_ORDER_POSITIONS,
573        Val::AnyRef(Some(new_positions.to_anyref())),
574    )?;
575    forward_rehashed(caller, &old_keys, &new_keys, &new_order)
576}
577
578// ---------------------------------------------------------------------------
579// forEach + iteration
580// ---------------------------------------------------------------------------
581
582/// Where the ledger walk finds a map's keys, ledger and ledger length.
583const LEDGER: LedgerFields = LedgerFields {
584    entries: F_KEYS,
585    order: F_ORDER,
586    order_len: F_ORDER_LEN,
587};
588
589/// `Map#forEach(self, callback)` — calls `callback(value, key, map)` for each
590/// entry in insertion order. Like an iterator it walks the map live, so it
591/// visits entries the callback adds and skips ones it deletes.
592pub(super) async fn for_each(
593    caller: &mut Caller<'_, StoreData>,
594    recv: &Val,
595    f: &Closure,
596) -> wasmtime::Result<()> {
597    let b = backing(caller, recv)?;
598    // Kept in a host-allocated struct: a callback that grows or clears the map
599    // leaves the cursor's arrays reachable from nothing else.
600    let stored = LedgerCursor::start(caller, b, LEDGER)?.to_val(caller)?;
601    loop {
602        let mut cursor = LedgerCursor::from_val(caller, &stored)?;
603        let Some(bucket) = cursor.next_bucket(caller, LEDGER)? else {
604            return Ok(());
605        };
606        cursor.store(caller, &stored)?;
607        let (key, value) = map_entry(caller, &cursor, bucket)?;
608        f.call_dynamic(caller, &[value, key, *recv]).await?;
609    }
610}
611
612/// The key and value in `bucket` of the map's current arrays.
613fn map_entry(
614    caller: &mut Caller<'_, StoreData>,
615    cursor: &LedgerCursor,
616    bucket: u32,
617) -> wasmtime::Result<(Val, Val)> {
618    let keys = cursor.current_array(caller, F_KEYS)?;
619    let values = cursor.current_array(caller, F_VALUES)?;
620    let key = keys.get(&mut *caller, bucket)?;
621    let key = decode_key(caller, key)?;
622    let value = values.get(&mut *caller, bucket)?;
623    Ok((key, value))
624}
625
626/// Build a `keys`/`values`/`entries` iterator over the map's ledger. It walks
627/// the map live, as a JavaScript `Map` iterator does: see [`LedgerCursor`].
628fn make_map_iterator(
629    caller: &mut Caller<'_, StoreData>,
630    recv: &Val,
631    kind: IterKind,
632) -> wasmtime::Result<Val> {
633    let b = backing(caller, recv)?;
634    let cursor = LedgerCursor::start(caller, b, LEDGER)?.to_val(caller)?;
635
636    let intr = intrinsic_types(&mut *caller)?;
637    let (next_ty, next_struct) = next_closure_type(caller.engine(), &intr)?;
638    let next = shared_next(
639        caller,
640        IteratorSource::Map,
641        kind,
642        next_ty,
643        move |caller, params, results| map_next_step(caller, params, results, kind),
644    )?;
645    build_iterator(caller, next_struct, next, cursor)
646}
647
648/// One `next()` step: yield the projected entry at the cursor's next live
649/// ledger slot, or `{ done: true }`.
650fn map_next_step(
651    caller: &mut Caller<'_, StoreData>,
652    params: &[Val],
653    results: &mut [Val],
654    kind: IterKind,
655) -> wasmtime::Result<()> {
656    let stored = abi_arg(params, 0)?;
657    let mut cursor = LedgerCursor::from_val(caller, stored)?;
658    let bucket = cursor.next_bucket(caller, LEDGER)?;
659    cursor.store(caller, stored)?;
660    let Some(bucket) = bucket else {
661        *abi_result(results, 0)? = iter_done(caller)?;
662        return Ok(());
663    };
664    let (key, value) = map_entry(caller, &cursor, bucket)?;
665    let yielded = match kind {
666        IterKind::Keys => key,
667        IterKind::Values => value,
668        IterKind::Entries => {
669            let pair = write_submilli_array_struct(caller, &[key, value])?;
670            Val::AnyRef(Some(pair.to_anyref()))
671        }
672    };
673    *abi_result(results, 0)? = iter_yield(caller, yielded)?;
674    Ok(())
675}
676
677pub(super) fn keys(caller: &mut Caller<'_, StoreData>, recv: &Val) -> wasmtime::Result<Val> {
678    make_map_iterator(caller, recv, IterKind::Keys)
679}
680
681pub(super) fn values(caller: &mut Caller<'_, StoreData>, recv: &Val) -> wasmtime::Result<Val> {
682    make_map_iterator(caller, recv, IterKind::Values)
683}
684
685/// `entries` and `iterator` are the same `[K, V]`-tuple cursor (`for…of` uses
686/// the iterator method).
687pub(super) fn entries(caller: &mut Caller<'_, StoreData>, recv: &Val) -> wasmtime::Result<Val> {
688    make_map_iterator(caller, recv, IterKind::Entries)
689}
690
691// ---------------------------------------------------------------------------
692// Constructor (`new Map(init)`)
693// ---------------------------------------------------------------------------
694
695/// `MapConstructor#new(init?) -> Map`. Builds an empty map, then populates it
696/// from the initializer (mirroring the Wasm `collect_into`): `null` → empty; an
697/// `$Array` of `[k, v]` tuples → read directly; a `$Map` → its `entries()`
698/// cursor; any other value → drive the iterator protocol (an `iterator()` method
699/// if present, else the value itself), reading each `{ done, value }` result.
700pub(super) async fn construct(
701    caller: &mut Caller<'_, StoreData>,
702    init: &Val,
703) -> wasmtime::Result<Val> {
704    let coll = build_empty(caller)?;
705    if super::undefined::is_nullish(caller, init)? {
706        return Ok(coll);
707    }
708
709    let intr = intrinsic_types(&mut *caller)?;
710    if is_a(caller, init, &intr.array)? {
711        for entry in read_array_vals(caller, init)? {
712            let (k, v) = read_pair(caller, &entry)?;
713            set(caller, &coll, &k, &v).await?;
714        }
715        return Ok(coll);
716    }
717
718    // The iterator and each object its `next()` returns come from guest calls.
719    let kept_iterator = KeptValue::new(caller)?;
720    let kept_result = KeptValue::new(caller)?;
721
722    let it = if is_a(caller, init, &map_backing_struct(caller.engine(), &intr)?)? {
723        entries(caller, init)?
724    } else if let Some(iter_method) = object_field(caller, init, "iterator")? {
725        let c = closure::read(caller, &iter_method, "Map ctor iterable")?;
726        c.call(caller, &[]).await?
727    } else {
728        *init
729    };
730    kept_iterator.set(caller, it)?;
731
732    let next = object_field(caller, &it, "next")?
733        .ok_or_else(|| wasmtime::Error::msg("Map ctor: initializer is not iterable"))?;
734    let next_closure = closure::read(caller, &next, "Map ctor iterator")?;
735    loop {
736        let result = next_closure.call(caller, &[]).await?;
737        kept_result.set(caller, result)?;
738        let done = object_field(caller, &result, "done")?
739            .ok_or_else(|| wasmtime::Error::msg("Map ctor: iterator result missing `done`"))?;
740        if unbox_bool(caller, &done)? {
741            break;
742        }
743        let value = object_field(caller, &result, "value")?
744            .ok_or_else(|| wasmtime::Error::msg("Map ctor: iterator result missing `value`"))?;
745        let (k, v) = read_pair(caller, &value)?;
746        set(caller, &coll, &k, &v).await?;
747    }
748    Ok(coll)
749}
750
751/// Build a `Map<string, string>` from host-supplied pairs, insertion order
752/// kept — the stdlib's way to hand a guest a `Query`/`Headers` map.
753pub(crate) async fn string_map_from_pairs(
754    caller: &mut Caller<'_, StoreData>,
755    pairs: &[(String, String)],
756) -> wasmtime::Result<Val> {
757    host_string_map_from_pairs(caller, pairs)
758}
759
760/// Construct host-supplied strings without dispatching guest vtable callbacks.
761/// Post-effect result marshalling can therefore finish even at zero fuel.
762pub(crate) fn host_string_map_from_pairs(
763    caller: &mut Caller<'_, StoreData>,
764    pairs: &[(String, String)],
765) -> wasmtime::Result<Val> {
766    let capacity = pairs
767        .len()
768        .checked_mul(2)
769        .and_then(usize::checked_next_power_of_two)
770        .and_then(|capacity| i32::try_from(capacity.max(INITIAL_CAPACITY as usize)).ok())
771        .ok_or_else(|| {
772            crate::runtime::host::range_error("host string map capacity limit exceeded")
773        })?;
774    let map = build_empty(caller)?;
775    let backing = backing(caller, &map)?;
776    if capacity > INITIAL_CAPACITY {
777        rehash(caller, &backing, capacity)?;
778    }
779    for (key, value) in pairs {
780        let _native = crate::runtime::limits::HostBytes::new(
781            &caller.data().tenant_limits,
782            (key.len() as u64).saturating_mul(8),
783        )?;
784        let mut units = Vec::new();
785        units
786            .try_reserve_exact(key.len())
787            .map_err(crate::runtime::host::fatal_host_error)?;
788        units.extend(key.encode_utf16());
789        let key = crate::runtime::host::write_submilli_string_struct(caller, key)?;
790        let value = crate::runtime::host::write_submilli_string_struct(caller, value)?;
791        insert_host_string(
792            caller,
793            &backing,
794            &units,
795            Val::AnyRef(Some(key.to_anyref())),
796            Val::AnyRef(Some(value.to_anyref())),
797        )?;
798    }
799    Ok(map)
800}
801
802fn insert_host_string(
803    caller: &mut Caller<'_, StoreData>,
804    backing: &Rooted<StructRef>,
805    units: &[u16],
806    key: Val,
807    value: Val,
808) -> wasmtime::Result<()> {
809    let keys = field_array(caller, backing, F_KEYS)?;
810    let values = field_array(caller, backing, F_VALUES)?;
811    let hashes = field_array(caller, backing, F_HASHES)?;
812    let capacity = probe_capacity(keys.len(&mut *caller)?)?;
813    let hash = super::vtable::string_hash(caller, &key)? as i32;
814    let mut bucket = hash & (capacity - 1);
815    for _ in 0..capacity {
816        fuel::charge(&mut *caller, fuel::ELEM, 1)?;
817        let slot = keys.get(&mut *caller, bucket as u32)?;
818        if is_null(&slot) {
819            let size = field_i32(caller, backing, F_SIZE)?;
820            let order = field_array(caller, backing, F_ORDER)?;
821            let positions = field_array(caller, backing, F_ORDER_POSITIONS)?;
822            keys.set(&mut *caller, bucket as u32, key)?;
823            values.set(&mut *caller, bucket as u32, value)?;
824            hashes.set(&mut *caller, bucket as u32, Val::I32(hash))?;
825            order.set(&mut *caller, size as u32, Val::I32(bucket))?;
826            positions.set(&mut *caller, bucket as u32, Val::I32(size))?;
827            backing.set_field(&mut *caller, F_SIZE, Val::I32(size + 1))?;
828            backing.set_field(&mut *caller, F_ORDER_LEN, Val::I32(size + 1))?;
829            return Ok(());
830        }
831        if index_value(caller, &hashes, bucket as u32)? == hash {
832            let existing = super::vtable::read_string_units(caller, &slot, "host map key")?;
833            fuel::charge(&mut *caller, fuel::SCAN, units.len() as u64)?;
834            if existing == units {
835                values.set(&mut *caller, bucket as u32, value)?;
836                return Ok(());
837            }
838        }
839        bucket = (bucket + 1) & (capacity - 1);
840    }
841    Err(crate::runtime::host::fatal_host_error(
842        "host string map has no empty bucket",
843    ))
844}
845
846/// Read a `Map<string, string>`'s live entries in insertion order — the
847/// stdlib's way to consume a guest-supplied `Query`/`Headers` map host-side.
848pub(crate) fn string_entries(
849    caller: &mut Caller<'_, StoreData>,
850    map: &Val,
851) -> wasmtime::Result<Vec<(String, String)>> {
852    let b = backing(caller, map)?;
853    let keys = field_array(caller, &b, F_KEYS)?;
854    let values = field_array(caller, &b, F_VALUES)?;
855    let order = field_array(caller, &b, F_ORDER)?;
856    let order_len = field_i32(caller, &b, F_ORDER_LEN)?;
857    let mut out = Vec::new();
858    for o in 0..order_len {
859        let Val::I32(idx) = order.get(&mut *caller, o as u32)? else {
860            continue;
861        };
862        if idx == -1 {
863            continue;
864        }
865        let key = keys.get(&mut *caller, idx as u32)?;
866        let key = decode_key(caller, key)?;
867        let value = values.get(&mut *caller, idx as u32)?;
868        out.push((
869            crate::runtime::host::read_string_arg(caller, &key, "map entry key")?,
870            crate::runtime::host::read_string_arg(caller, &value, "map entry value")?,
871        ));
872    }
873    Ok(out)
874}
875
876/// A fresh empty `$MapBacking` carrying the host object vtable.
877fn build_empty(caller: &mut Caller<'_, StoreData>) -> wasmtime::Result<Val> {
878    let intr = intrinsic_types(&mut *caller)?;
879    let ty = map_backing_struct(caller.engine(), &intr)?;
880    let vtable = host_object_vtable(caller)?;
881    let keys = new_raw_array(caller, INITIAL_CAPACITY)?;
882    let values = new_raw_array(caller, INITIAL_CAPACITY)?;
883    let order = new_index_array(caller, INITIAL_CAPACITY)?;
884    let hashes = new_index_array(caller, INITIAL_CAPACITY)?;
885    let positions = new_index_array(caller, INITIAL_CAPACITY)?;
886    let pre = StructRefPre::new(&mut *caller, ty);
887    let st = StructRef::new(
888        &mut *caller,
889        &pre,
890        &[
891            vtable,
892            Val::AnyRef(Some(keys.to_anyref())),
893            Val::AnyRef(Some(values.to_anyref())),
894            Val::I32(0),
895            Val::AnyRef(Some(order.to_anyref())),
896            Val::I32(0),
897            Val::AnyRef(Some(hashes.to_anyref())),
898            Val::AnyRef(Some(positions.to_anyref())),
899            Val::I64(0),
900        ],
901    )?;
902    Ok(Val::AnyRef(Some(st.to_anyref())))
903}
904
905/// Read a `[k, v]` tuple (`$Array` of two elements).
906fn read_pair(caller: &mut Caller<'_, StoreData>, tuple: &Val) -> wasmtime::Result<(Val, Val)> {
907    let storage = crate::runtime::array_storage::ArrayStorage::read(caller, tuple)?;
908    if storage.len < 2 {
909        return Err(crate::runtime::host::type_error(
910            "Map ctor: entry needs a key and value",
911        ));
912    }
913    Ok((
914        storage.backing.get(&mut *caller, 0)?,
915        storage.backing.get(&mut *caller, 1)?,
916    ))
917}
918
919#[cfg(test)]
920mod tests {
921    use super::*;
922    use crate::codegen::intrinsics::declare_intrinsic_types;
923    use crate::runtime::intrinsic_types::build_intrinsic_types;
924    use wasm_encoder::{
925        ConstExpr, ExportKind, ExportSection, GlobalSection, GlobalType as EncGlobalType,
926        HeapType as EncHeapType, Module, RefType as EncRefType, TypeSection, ValType as EncValType,
927    };
928    use wasmtime::{Config, Engine};
929
930    /// The host `$MapBacking` must canonically equal the `$Object` subtype
931    /// codegen emits (`declare_intrinsic_types`); otherwise a host-built map's
932    /// `ref.cast` to `$MapBacking` would trap at the receiver of every method.
933    #[test]
934    fn map_backing_matches_codegen() {
935        let mut config = Config::new();
936        config.wasm_gc(true);
937        config.wasm_function_references(true);
938        let engine = Engine::new(&config).unwrap();
939
940        let intr = build_intrinsic_types(&engine).unwrap();
941        let host = map_backing_struct(&engine, &intr).unwrap();
942
943        let mut module = Module::new();
944        let mut types = TypeSection::new();
945        let idx = declare_intrinsic_types(&mut types);
946        let backing_idx = idx.map;
947        module.section(&types);
948
949        let mut globals = GlobalSection::new();
950        globals.global(
951            EncGlobalType {
952                val_type: EncValType::Ref(EncRefType {
953                    nullable: true,
954                    heap_type: EncHeapType::Concrete(backing_idx),
955                }),
956                mutable: false,
957                shared: false,
958            },
959            &ConstExpr::ref_null(EncHeapType::Concrete(backing_idx)),
960        );
961        let mut exports = ExportSection::new();
962        exports.export("backing", ExportKind::Global, 0);
963        module.section(&globals);
964        module.section(&exports);
965
966        let module = wasmtime::Module::new(&engine, module.finish()).unwrap();
967        let recovered = module
968            .get_export("backing")
969            .unwrap()
970            .global()
971            .unwrap()
972            .content()
973            .as_ref()
974            .map(|r| r.heap_type().clone())
975            .unwrap()
976            .as_concrete_struct()
977            .unwrap()
978            .clone();
979        assert!(StructType::eq(&host, &recovered));
980    }
981}