Skip to main content

submilli_engine/runtime/prelude/set/
mod.rs

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