From 2206811903c4723ba0fb7641639f080fc5e3d4e2 Mon Sep 17 00:00:00 2001 From: siyul-park Date: Fri, 19 Jun 2026 20:05:58 +0900 Subject: [PATCH] feat(iterator): add MapIterator and support for custom iterators in coroutine opcodes --- docs/host-integration.md | 6 ++ docs/instruction-set.md | 9 +-- docs/value-representation.md | 8 +++ instr/opcode.go | 2 + instr/type.go | 1 + instr/type_test.go | 1 + interp/interp_test.go | 135 +++++++++++++++++++++++++++++++++++ interp/threaded.go | 126 +++++++++++++++++++++++++++++--- interp/trace.go | 1 + types/map.go | 117 ++++++++++++++++++++++++++++++ types/map_test.go | 25 +++++++ types/value.go | 7 ++ 12 files changed, 425 insertions(+), 13 deletions(-) diff --git a/docs/host-integration.md b/docs/host-integration.md index 8e1d883..9d5fa6a 100644 --- a/docs/host-integration.md +++ b/docs/host-integration.md @@ -310,6 +310,12 @@ converted the same way (it marshals as `ref`). Implement **both** interfaces for a round-trip: a direction the type does not implement returns `ErrUnsupportedMarshalType`. +Custom producer types can marshal to a heap value implementing +`types.Iterator`. Bytecode consumes that value with `RESUME`, `CORO_DONE`, and +`CORO_VALUE`, the same opcodes used for coroutine handles. An iterator yields +one `types.Value` at a time from `Current`; if it retains heap refs, implement +`types.Traceable` so the collector can see its backing state and current value. + ### External types — `WithConverter` For a type you do **not** own (so you cannot add `MarshalVM` / `UnmarshalVM`), diff --git a/docs/instruction-set.md b/docs/instruction-set.md index 7557397..5cbeb24 100644 --- a/docs/instruction-set.md +++ b/docs/instruction-set.md @@ -293,17 +293,18 @@ Map keys use primitive value identity for `i32`, `i64`, `f32`, and `f64`; all re | `MAP_DELETE` | `{}` | `map key →` | ◐ | Delete entry; missing key is a no-op. JIT keeps framed entries by exiting locally to the threaded handler. | | `MAP_CLEAR` | `{}` | `map →` | ◐ | Delete all entries. JIT keeps framed entries by exiting locally to the threaded handler. | | `MAP_KEYS` | `{}` | `map → array` | ⬜ | Snapshot keys into a new `[]K` array (`K` = map key type), in unspecified order. Enables guest map iteration with `ARRAY_LEN`/`ARRAY_GET` + `MAP_GET`. | +| `MAP_ITER` | `{}` | `map → iterator` | ⬜ | Create a `types.MapIterator` over the map without snapshotting keys, advance it to the first key, and transfer map ownership into the iterator. The iterator yields keys only; use `MAP_GET` when the value is needed. Iteration order and mutation visibility are unspecified, matching Go map range semantics. | ## Coroutines -A function whose body contains `YIELD` is a coroutine-function: its `CALL` allocates a `Coroutine` handle and runs the body until the first `YIELD` (suspend) or `RETURN` (finish), yielding the handle instead of plain returns. `RESUME` re-enters a suspended handle. A `YIELD` in the entry frame (`fp == 1`) is an interpreter escape: it panics the private `errYield`, `Run` returns the exported `ErrYield` without losing state, and the next `Run` resumes after the `YIELD`. +A function whose body contains `YIELD` is a coroutine-function: its `CALL` allocates a `Coroutine` handle and runs the body until the first `YIELD` (suspend) or `RETURN` (finish), yielding the handle instead of plain returns. `RESUME` re-enters a suspended handle. A `YIELD` in the entry frame (`fp == 1`) is an interpreter escape: it panics the private `errYield`, `Run` returns the exported `ErrYield` without losing state, and the next `Run` resumes after the `YIELD`. `RESUME`, `CORO_DONE`, and `CORO_VALUE` also accept custom `types.Iterator` heap values; iterators are single-value producers whose `Current` value is read with `CORO_VALUE`. | Opcode | Widths | Stack | JIT | Description | |---|---|---|---|---| | `YIELD` | `{}` | `value → result` | ◐ | Suspend the current coroutine frame, capturing `value` into the handle and unwinding to the caller; `RESUME` later delivers its in-value as `result`. In the entry frame it escapes via `ErrYield`. JIT records a `YIELD` reached in the trace's anchor frame as a terminal and lowers it to an unconditional deopt that runs the threaded suspend; a `YIELD` inside an inlined callee frame aborts the trace. | -| `RESUME` | `{}` | `coro in → coro` | ◐ | Resume a suspended handle, delivering `in` as the pending `YIELD`'s result, running to the next `YIELD`/`RETURN`, and pushing the handle back; traps `ErrCoroutineDone` if already finished. JIT lowers an anchor-frame `RESUME` to a terminal deopt that runs the threaded resume. | -| `CORO_DONE` | `{}` | `coro → i32` | ◐ | Push `I32(1)` if the handle has finished, else `I32(0)`; does not release the handle. JIT has a native fast path behind an itab guard (reads the `done` byte). | -| `CORO_VALUE` | `{}` | `coro → value` | ◐ | Push the handle's last yielded or returned value and release the handle. JIT has a native fast path behind an itab guard (loads the boxed `value`, retains it, releases the handle). | +| `RESUME` | `{}` | `coro in → coro` | ◐ | Resume a suspended coroutine handle, delivering `in` as the pending `YIELD`'s result, or advance a `types.Iterator` while ignoring `in`. Traps `ErrCoroutineDone` if already finished. JIT lowers an anchor-frame `RESUME` to a terminal deopt that runs the threaded resume. | +| `CORO_DONE` | `{}` | `coro → i32` | ◐ | Push `I32(1)` if the coroutine or iterator has finished, else `I32(0)`; does not release the handle. JIT has a native fast path for `*Coroutine` behind an itab guard and falls back for custom iterators. | +| `CORO_VALUE` | `{}` | `coro → value` | ◐ | Push the coroutine's last yielded/returned value or the iterator's current value, then release the handle. JIT has a native fast path for `*Coroutine` behind an itab guard and falls back for custom iterators. | ## Extensions diff --git a/docs/value-representation.md b/docs/value-representation.md index c4ca653..4d0f2d4 100644 --- a/docs/value-representation.md +++ b/docs/value-representation.md @@ -132,6 +132,7 @@ Heap objects implement `types.Value`. | `*types.Struct` | `KindRef` | `*StructType` | `Value`, `Traceable` | | `*types.Map` | `KindRef` | `*MapType` | `Value`, `Traceable` | | `*types.MapI32`, `*types.MapI64`, `*types.MapF32`, `*types.MapF64` | `KindRef` | `*MapType` | `Value`, `Traceable` | +| `*types.MapIterator` | `KindRef` | `TypeRef` | `Value`, `Traceable`, `Iterator` | | `*types.Function` | `KindRef` | `*FunctionType` | `Value` | | `*types.Closure` | `KindRef` | `*FunctionType` | `Value`, `Traceable` | | `*interp.HostFunction` | `KindRef` | `*FunctionType` | `Value` | @@ -152,6 +153,13 @@ slice to `1 + len(Upvals)` and never takes the lazy-nil path. `STRUCT_GET` and `STRUCT_SET` handle VM-native `*types.Struct` directly and fall back to `*interp.HostObject` for host-supplied values. See [host-integration.md](host-integration.md) for HostObject semantics. +`Iterator` marks heap values that can be consumed by `RESUME`, `CORO_DONE`, and +`CORO_VALUE`. `Next()` advances one item, `Current()` returns the single current +VM value, and `Done()` reports exhaustion. Iterators that hold refs must also +implement `Traceable` and report retained backing refs plus any current ref. +`MapIterator` keeps its source map and current ref key live while it traverses +without building a key array; it yields keys only. + User-defined heap types add no new `Kind`: a custom value implements `types.Value` / `types.Type` and is `KindRef` like every heap object (implement `Traceable` if it holds refs, `io.Closer` to release native resources). `Zero` returns `BoxedNull` for `KindRef`, so a custom type's default is null until an opcode constructs it. Reference the type programmatically via `b.Type(t)` and marshal Go values with `WithConverter` / `ValueMarshaler`; reconstructing a custom type from a textual type string is not yet supported. Defined scalar values with methods marshal as their underlying primitive unless passed by pointer. Pointer form becomes a `HostObject`; field `0` is reserved as `Value` and exposes the current primitive for ordinary opcodes. diff --git a/instr/opcode.go b/instr/opcode.go index 2d050e7..95492ab 100644 --- a/instr/opcode.go +++ b/instr/opcode.go @@ -264,6 +264,8 @@ const ( MAP_KEYS CLOSURE_NEW + + MAP_ITER ) // EXT is the reserved prefix opcode for user-registered extension instructions. diff --git a/instr/type.go b/instr/type.go index cd71c28..de904b1 100644 --- a/instr/type.go +++ b/instr/type.go @@ -246,6 +246,7 @@ var types = map[Opcode]Type{ MAP_DELETE: {"map.delete", []int{}}, MAP_CLEAR: {"map.clear", []int{}}, MAP_KEYS: {"map.keys", []int{}}, + MAP_ITER: {"map.iter", []int{}}, REF_NEW: {"ref.new", []int{}}, REF_GET: {"ref.get", []int{}}, diff --git a/instr/type_test.go b/instr/type_test.go index 50ae13b..dd974ce 100644 --- a/instr/type_test.go +++ b/instr/type_test.go @@ -245,6 +245,7 @@ func TestTypeOf(t *testing.T) { {opcode: MAP_DELETE}, {opcode: MAP_CLEAR}, {opcode: MAP_KEYS}, + {opcode: MAP_ITER}, {opcode: REF_NEW}, {opcode: REF_GET}, diff --git a/interp/interp_test.go b/interp/interp_test.go index 7bfc4fe..5552612 100644 --- a/interp/interp_test.go +++ b/interp/interp_test.go @@ -30,6 +30,36 @@ func (fn callableFunc) Call(ctx uintptr) error { return fn(ctx) } +type testIterator struct { + values []types.Value + value types.Value + done bool +} + +var _ types.Iterator = (*testIterator)(nil) + +func (it *testIterator) Kind() types.Kind { return types.KindRef } + +func (it *testIterator) Type() types.Type { return types.TypeRef } + +func (it *testIterator) String() string { return "test.iterator" } + +func (it *testIterator) Next() bool { + if len(it.values) == 0 { + it.value = types.BoxedNull + it.done = true + return false + } + it.value = it.values[0] + it.values = it.values[1:] + it.done = false + return true +} + +func (it *testIterator) Current() types.Value { return it.value } + +func (it *testIterator) Done() bool { return it.done } + // vmPoint opts into its own VM representation (a "x,y" string) even though it // has exported fields and methods that would otherwise route to a HostObject. type vmPoint struct { @@ -2694,6 +2724,65 @@ var tests = []test{ ), values: []types.Value{types.I32(1)}, }, + { + program: program.New( + []instr.Instruction{ + instr.New(instr.I32_CONST, 5), + instr.New(instr.I32_CONST, 42), + instr.New(instr.I32_CONST, 1), + instr.New(instr.MAP_NEW, 0), + instr.New(instr.MAP_ITER), + instr.New(instr.CORO_VALUE), + }, + program.WithTypes(types.NewMapType(types.TypeI32, types.TypeI32)), + ), + values: []types.Value{types.I32(5)}, + }, + { + program: program.New( + []instr.Instruction{ + instr.New(instr.I64_CONST, uint64(int64(1<<50))), + instr.New(instr.I32_CONST, 42), + instr.New(instr.I32_CONST, 1), + instr.New(instr.MAP_NEW, 0), + instr.New(instr.MAP_ITER), + instr.New(instr.CORO_VALUE), + }, + program.WithTypes(types.NewMapType(types.TypeI64, types.TypeI32)), + ), + values: []types.Value{types.I64(1 << 50)}, + }, + { + program: program.New( + []instr.Instruction{ + instr.New(instr.CONST_GET, 0), + instr.New(instr.I32_CONST, 42), + instr.New(instr.I32_CONST, 1), + instr.New(instr.MAP_NEW, 0), + instr.New(instr.MAP_ITER), + instr.New(instr.CORO_VALUE), + }, + program.WithConstants(types.String("key")), + program.WithTypes(types.NewMapType(types.TypeString, types.TypeI32)), + ), + values: []types.Value{types.String("key")}, + }, + { + program: program.New( + []instr.Instruction{ + instr.New(instr.I32_CONST, 5), + instr.New(instr.I32_CONST, 42), + instr.New(instr.I32_CONST, 1), + instr.New(instr.MAP_NEW, 0), + instr.New(instr.MAP_ITER), + instr.New(instr.I32_CONST, 0), + instr.New(instr.RESUME), + instr.New(instr.CORO_DONE), + }, + program.WithTypes(types.NewMapType(types.TypeI32, types.TypeI32)), + ), + values: []types.Value{types.I32(1)}, + }, { program: program.New( []instr.Instruction{ @@ -3587,6 +3676,52 @@ func TestInterpreter_Run(t *testing.T) { } }) + t.Run("custom iterator resumes through coroutine opcodes", func(t *testing.T) { + for _, mode := range modes { + mode := mode + t.Run(mode.name, func(t *testing.T) { + it := &testIterator{values: []types.Value{types.I32(7), types.I32(11)}} + require.True(t, it.Next()) + prog := program.New([]instr.Instruction{ + instr.New(instr.CONST_GET, 0), + instr.New(instr.I32_CONST, 0), + instr.New(instr.RESUME), + instr.New(instr.CORO_VALUE), + }, program.WithConstants(it)) + + i := New(prog, mode.opts...) + defer i.Close() + require.NoError(t, i.Run(context.Background())) + got, err := i.Pop() + require.NoError(t, err) + require.Equal(t, types.I32(11), got) + }) + } + }) + + t.Run("custom iterator reports done", func(t *testing.T) { + for _, mode := range modes { + mode := mode + t.Run(mode.name, func(t *testing.T) { + it := &testIterator{values: []types.Value{types.I32(7)}} + require.True(t, it.Next()) + prog := program.New([]instr.Instruction{ + instr.New(instr.CONST_GET, 0), + instr.New(instr.I32_CONST, 0), + instr.New(instr.RESUME), + instr.New(instr.CORO_DONE), + }, program.WithConstants(it)) + + i := New(prog, mode.opts...) + defer i.Close() + require.NoError(t, i.Run(context.Background())) + got, err := i.Pop() + require.NoError(t, err) + require.Equal(t, types.I32(1), got) + }) + } + }) + t.Run("call starts coroutine and first yield delivers a value", func(t *testing.T) { // CALL of a coroutine-function (one containing YIELD) produces a Coroutine // handle instead of return values; coro.value reads the first yielded value. diff --git a/interp/threaded.go b/interp/threaded.go index b6af741..74b88e8 100644 --- a/interp/threaded.go +++ b/interp/threaded.go @@ -264,10 +264,22 @@ var threaded = [256]func(c *threadedCompiler) func(i *Interpreter){ if i.sp == 0 { panic(ErrStackUnderflow) } - co := unboxRef[*Coroutine](i, i.stack[i.sp-1]) + ref := i.stack[i.sp-1] + if ref.Kind() != types.KindRef { + panic(ErrTypeMismatch) + } done := int32(0) - if co.done { - done = 1 + switch co := i.heap[ref.Ref()].(type) { + case *Coroutine: + if co.done { + done = 1 + } + case types.Iterator: + if co.Done() { + done = 1 + } + default: + panic(ErrTypeMismatch) } i.stack[i.sp-1] = types.BoxI32(done) i.fr.ip++ @@ -283,12 +295,16 @@ var threaded = [256]func(c *threadedCompiler) func(i *Interpreter){ if box.Kind() != types.KindRef { panic(ErrTypeMismatch) } - co, ok := i.heap[box.Ref()].(*Coroutine) - if !ok { + var val types.Boxed + switch co := i.heap[box.Ref()].(type) { + case *Coroutine: + val = co.value + i.retainBox(val) + case types.Iterator: + val = i.boxIteratorCurrent(co.Current()) + default: panic(ErrTypeMismatch) } - val := co.value - i.retainBox(val) i.releaseBox(box) i.stack[i.sp-1] = val i.fr.ip++ @@ -3878,6 +3894,33 @@ var threaded = [256]func(c *threadedCompiler) func(i *Interpreter){ i.fr.ip++ } }, + instr.MAP_ITER: func(c *threadedCompiler) func(i *Interpreter) { + c.ip++ + return func(i *Interpreter) { + if i.sp < 1 { + panic(ErrStackUnderflow) + } + ref := i.stack[i.sp-1] + if ref.Kind() != types.KindRef { + panic(ErrTypeMismatch) + } + addr := ref.Ref() + switch i.heap[addr].(type) { + case *types.TypedMap[int32], + *types.TypedMap[int64], + *types.TypedMap[float32], + *types.TypedMap[float64], + *types.Map: + default: + panic(ErrTypeMismatch) + } + iter := types.NewMapIterator(types.Ref(addr), i.heap[addr]) + iter.Next() + i.retainIteratorCurrent(iter) + i.stack[i.sp-1] = types.BoxRef(i.keep(iter)) + i.fr.ip++ + } + }, instr.CLOSURE_NEW: func(c *threadedCompiler) func(i *Interpreter) { c.ip++ return func(i *Interpreter) { @@ -4173,10 +4216,17 @@ func (i *Interpreter) resume() { panic(ErrTypeMismatch) } coAddr := box.Ref() - co, ok := i.heap[coAddr].(*Coroutine) - if !ok { + switch co := i.heap[coAddr].(type) { + case *Coroutine: + i.resumeCoroutine(coAddr, co, in) + case types.Iterator: + i.resumeIterator(co, in) + default: panic(ErrTypeMismatch) } +} + +func (i *Interpreter) resumeCoroutine(coAddr int, co *Coroutine, in types.Boxed) { if co.done { panic(ErrCoroutineDone) } @@ -4212,6 +4262,64 @@ func (i *Interpreter) resume() { i.fr = f } +func (i *Interpreter) resumeIterator(iter types.Iterator, in types.Boxed) { + if iter.Done() { + panic(ErrCoroutineDone) + } + i.releaseBox(in) + i.releaseIteratorCurrent(iter) + iter.Next() + i.retainIteratorCurrent(iter) + i.sp-- + i.fr.ip++ +} + +func (i *Interpreter) retainIteratorCurrent(iter types.Iterator) { + if iter.Done() { + return + } + if _, ok := iter.(*types.MapIterator); ok { + i.retainValue(iter.Current()) + } +} + +func (i *Interpreter) releaseIteratorCurrent(iter types.Iterator) { + if iter.Done() { + return + } + if _, ok := iter.(*types.MapIterator); ok { + i.releaseValue(iter.Current()) + } +} + +func (i *Interpreter) boxIteratorCurrent(val types.Value) types.Boxed { + if val == nil { + i.retain(0) + return types.BoxedNull + } + box := i.box(val) + i.retainValue(val) + return box +} + +func (i *Interpreter) retainValue(val types.Value) { + switch val := val.(type) { + case types.Boxed: + i.retainBox(val) + case types.Ref: + i.retain(int(val)) + } +} + +func (i *Interpreter) releaseValue(val types.Value) { + switch val := val.(type) { + case types.Boxed: + i.releaseBox(val) + case types.Ref: + i.release(int(val)) + } +} + // satI32 truncates v toward zero into a signed i32, saturating out-of-range // inputs to the i32 bounds and mapping NaN to 0 (WebAssembly trunc_sat_s). func (*Interpreter) satI32(v float64) int32 { diff --git a/interp/trace.go b/interp/trace.go index 92e3c16..92606bb 100644 --- a/interp/trace.go +++ b/interp/trace.go @@ -629,6 +629,7 @@ func (r *Tracer) unrecordable(i *Interpreter, op instr.Opcode) bool { instr.MAP_DELETE, instr.MAP_CLEAR, instr.MAP_KEYS, + instr.MAP_ITER, instr.REF_NEW, instr.REF_SET, instr.CLOSURE_NEW: diff --git a/types/map.go b/types/map.go index 747f916..512de2d 100644 --- a/types/map.go +++ b/types/map.go @@ -3,6 +3,7 @@ package types import ( "fmt" "math" + "reflect" "sort" "strings" ) @@ -38,12 +39,33 @@ type MapType struct { TraceValues bool } +type MapIterator struct { + iter *reflect.MapIter + current Value + ref Ref + kind mapIteratorKind + done bool +} + +type mapIteratorKind byte + +const ( + mapIteratorInvalid mapIteratorKind = iota + mapIteratorI32 + mapIteratorI64 + mapIteratorF32 + mapIteratorF64 + mapIteratorGeneric +) + var ( _ Traceable = (*Map)(nil) _ Traceable = (*TypedMap[int32])(nil) _ Traceable = (*TypedMap[int64])(nil) _ Traceable = (*TypedMap[float32])(nil) _ Traceable = (*TypedMap[float64])(nil) + _ Traceable = (*MapIterator)(nil) + _ Iterator = (*MapIterator)(nil) _ Type = (*MapType)(nil) ) @@ -78,6 +100,28 @@ func NewMapForType(typ *MapType, capacity int) Value { } } +func NewMapIterator(ref Ref, val Value) *MapIterator { + it := &MapIterator{ref: ref, done: true, current: BoxedNull} + switch m := val.(type) { + case *TypedMap[int32]: + it.kind = mapIteratorI32 + it.iter = reflect.ValueOf(m.entries).MapRange() + case *TypedMap[int64]: + it.kind = mapIteratorI64 + it.iter = reflect.ValueOf(m.entries).MapRange() + case *TypedMap[float32]: + it.kind = mapIteratorF32 + it.iter = reflect.ValueOf(m.entries).MapRange() + case *TypedMap[float64]: + it.kind = mapIteratorF64 + it.iter = reflect.ValueOf(m.entries).MapRange() + case *Map: + it.kind = mapIteratorGeneric + it.iter = reflect.ValueOf(m.entries).MapRange() + } + return it +} + func NewMapType(key Type, elem Type) *MapType { return &MapType{ Key: key, @@ -223,6 +267,59 @@ func (m *Map) Refs() []Ref { return refs } +func (it *MapIterator) Kind() Kind { return KindRef } + +func (it *MapIterator) Type() Type { return TypeRef } + +func (it *MapIterator) String() string { return "map.iterator" } + +func (it *MapIterator) Next() bool { + if it.iter == nil || !it.iter.Next() { + it.current = BoxedNull + it.done = true + return false + } + it.done = false + switch it.kind { + case mapIteratorI32: + it.current = I32(int32(it.iter.Key().Int())) + case mapIteratorI64: + it.current = I64(it.iter.Key().Int()) + case mapIteratorF32: + it.current = F32(float32(it.iter.Key().Float())) + case mapIteratorF64: + it.current = F64(it.iter.Key().Float()) + case mapIteratorGeneric: + key := it.iter.Key().Interface().(MapKey) + entry := it.iter.Value().Interface().(MapEntry) + it.current = key.value(entry) + default: + it.current = BoxedNull + it.done = true + return false + } + return true +} + +func (it *MapIterator) Current() Value { return it.current } + +func (it *MapIterator) Done() bool { return it.done } + +func (it *MapIterator) Refs() []Ref { + refs := []Ref{it.ref} + if !it.done { + switch current := it.current.(type) { + case Boxed: + if current.Kind() == KindRef { + refs = append(refs, Ref(current.Ref())) + } + case Ref: + refs = append(refs, current) + } + } + return refs +} + func (k MapKey) String() string { switch k.Kind { case KindI32: @@ -240,6 +337,26 @@ func (k MapKey) String() string { } } +func (k MapKey) value(entry MapEntry) Value { + if entry.Key != 0 { + return entry.Key + } + switch k.Kind { + case KindI32: + return I32(int32(k.Bits)) + case KindI64: + return I64(int64(k.Bits)) + case KindF32: + return F32(math.Float32frombits(uint32(k.Bits))) + case KindF64: + return F64(math.Float64frombits(k.Bits)) + case KindRef: + return Ref(int32(k.Bits)) + default: + return BoxedNull + } +} + func (t *MapType) Kind() Kind { return KindRef } func (t *MapType) String() string { diff --git a/types/map_test.go b/types/map_test.go index f5004dd..cc97121 100644 --- a/types/map_test.go +++ b/types/map_test.go @@ -118,6 +118,31 @@ func TestMap_Clear(t *testing.T) { require.Equal(t, 0, m.Len()) } +func TestMapIterator(t *testing.T) { + t.Run("typed map key", func(t *testing.T) { + m := NewTypedMap[int64](NewMapType(TypeI64, TypeI32), 0) + m.Set(1<<50, BoxI32(2)) + + iter := NewMapIterator(Ref(7), m) + require.True(t, iter.Done()) + require.True(t, iter.Next()) + require.Equal(t, I64(1<<50), iter.Current()) + require.False(t, iter.Next()) + require.True(t, iter.Done()) + require.Equal(t, []Ref{7}, iter.Refs()) + }) + + t.Run("generic map ref key", func(t *testing.T) { + m := NewMap(NewMapType(TypeString, TypeI32)) + m.Set(MapKey{Kind: KindRef, Bits: 9}, MapEntry{Key: BoxRef(9), Value: BoxI32(2)}) + + iter := NewMapIterator(Ref(7), m) + require.True(t, iter.Next()) + require.Equal(t, BoxRef(9), iter.Current()) + require.Equal(t, []Ref{7, 9}, iter.Refs()) + }) +} + func TestMap_Refs(t *testing.T) { t.Run("inline i64 value", func(t *testing.T) { m := NewMap(NewMapType(TypeI32, TypeI64)) diff --git a/types/value.go b/types/value.go index 7a91d22..9d24ae5 100644 --- a/types/value.go +++ b/types/value.go @@ -18,6 +18,13 @@ type Traceable interface { Refs() []Ref } +type Iterator interface { + Value + Next() bool + Current() Value + Done() bool +} + func Zero(kind Kind) Boxed { switch kind { case KindI32: