Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions docs/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ Two layers:

`types.Boxed` (`uint64`) is VM stack/global currency. Heap objects are `types.Value` referenced by `KindRef` in `Boxed`. See `value-representation.md`.

`types.Traceable` marks heap objects containing refs (`Array`, `Struct`, `HostObject`); GC walks them via `Refs() []Ref`. `types.Fielded` is the indexed field-access contract used by `STRUCT_GET`/`STRUCT_SET`, implemented by `*Struct` and `*HostObject`.
`types.Traceable` marks heap objects containing refs (`Array`, `Struct`, `HostObject`); GC walks them via `Refs() []Ref`. `STRUCT_GET`/`STRUCT_SET` handle native `*types.Struct` directly and use a concrete `HostObject` fallback for host values.

### `interp/`

Expand All @@ -101,7 +101,7 @@ Two layers:

`HostFunction` (`host.go`): wraps `func(i *Interpreter, params []Boxed) ([]Boxed, error)` as `types.Value`. Lives in constants, called by `CONST_GET` + `CALL`. Use `Interpreter.Marshal`/`Unmarshal` to convert Go values; Go `func` marshals to `HostFunction`, final `error` return propagated as host-call error. `WithMarshaler` replaces default reflection-based converter.

`HostObject` (`host.go`): wraps a Go value that carries methods or unexported fields. Implements `types.Fielded` so `STRUCT_GET`/`STRUCT_SET` dispatch through the same indexed-field protocol used by `*types.Struct`. Field reads/writes reflect against an internal addressable copy of the receiver through the interpreter's `Marshaler`; methods are pre-bound as `*HostFunction` values allocated on the VM heap.
`HostObject` (`host.go`): wraps a Go value that carries methods or unexported fields. `STRUCT_GET`/`STRUCT_SET` use it as the concrete host-value fallback after the native `*types.Struct` fast path. Field reads/writes reflect against an internal addressable copy of the receiver through the interpreter's `Marshaler`; methods are pre-bound as `*HostFunction` values allocated on the VM heap.

`Pool` (`pool.go`): multi-goroutine entry point. `Interpreter` is single-goroutine; `program.Program` is the only object safe to share across goroutines. `NewPool(prog, size, opts...)` lends up to `size` Interpreters lazily; `Get`/`Put` or `Run(ctx, fn)` borrow one per goroutine. `Put` calls `Reset` between borrows; `Close` releases every idle Interpreter's JIT buffer. Outstanding interpreters are closed on their next `Put` after `Close`. Heap refs from a borrowed Interpreter are invalid after `Put` (`Reset` wipes the heap).

Expand Down
2 changes: 1 addition & 1 deletion docs/host-integration.md
Original file line number Diff line number Diff line change
Expand Up @@ -153,7 +153,7 @@ fn, err := vm.Marshal(add)

### Host Objects

`*HostObject` surfaces a Go value to the VM with both **data fields** and **bound methods** behind the same indexed-field protocol used by `*Struct`. `STRUCT_GET` / `STRUCT_SET` dispatch through the `types.Fielded` interface so opcodes are unchanged.
`*HostObject` surfaces a Go value to the VM with both **data fields** and **bound methods** behind the same indexed-field protocol used by `*Struct`. `STRUCT_GET` / `STRUCT_SET` handle native structs directly and use a concrete HostObject fallback for host values.

```go
type Counter struct{ Count int32 }
Expand Down
8 changes: 5 additions & 3 deletions docs/value-representation.md
Original file line number Diff line number Diff line change
Expand Up @@ -122,15 +122,17 @@ Heap objects implement `types.Value`.
| `types.Ref` | `KindRef` | `TypeRef` | `Value` |
| `types.String` | `KindRef` | `TypeString` | `Value` |
| `*types.Array` | `KindRef` | `*ArrayType` | `Value`, `Traceable` |
| `*types.Struct` | `KindRef` | `*StructType` | `Value`, `Traceable`, `Fielded` |
| `*types.Struct` | `KindRef` | `*StructType` | `Value`, `Traceable` |
| `*types.Map` | `KindRef` | `*MapType` | `Value`, `Traceable` |
| `*types.Function` | `KindRef` | `*FunctionType` | `Value` |
| `*interp.HostFunction` | `KindRef` | `*FunctionType` | `Value` |
| `*interp.HostObject` | `KindRef` | `*StructType` | `Value`, `Traceable`, `Fielded` |
| `*interp.HostObject` | `KindRef` | `*StructType` | `Value`, `Traceable` |

`Traceable` exposes `Refs() []Ref` for GC graph traversal. Any heap object containing refs must implement `Traceable`.

`Fielded` exposes indexed `Field` / `SetField` / `Raw` / `SetRaw` plus `StructType()`; `STRUCT_GET` and `STRUCT_SET` dispatch through it so both VM-native structs and host-supplied objects use the same opcodes. See [host-integration.md](host-integration.md) for HostObject semantics.
`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.

Use constructors for compound runtime types (`NewStructType`, `NewStruct`, `NewMapType`, `NewMap`, `NewMapWithCapacity`). These constructors initialize cached metadata and internal storage used by interpreter hot paths.

## Unbox to Value

Expand Down
9 changes: 3 additions & 6 deletions interp/host.go
Original file line number Diff line number Diff line change
Expand Up @@ -54,12 +54,10 @@ func (s hostSlot) isMethod() bool { return s.field < 0 }
var (
_ types.Value = (*HostObject)(nil)
_ types.Traceable = (*HostObject)(nil)
_ types.Fielded = (*HostObject)(nil)
)

func (h *HostObject) Kind() types.Kind { return types.KindRef }
func (h *HostObject) Type() types.Type { return h.Typ }
func (h *HostObject) StructType() *types.StructType { return h.Typ }
func (h *HostObject) Kind() types.Kind { return types.KindRef }
func (h *HostObject) Type() types.Type { return h.Typ }

func (h *HostObject) String() string {
if !h.Receiver.IsValid() {
Expand Down Expand Up @@ -144,8 +142,7 @@ func (h *HostObject) lookup(i int) (hostSlot, types.StructField, bool) {

// marshal delegates to the interpreter's injected Marshaler so HostObject
// stays decoupled from the reflect-based default implementation. Errors
// propagate via panic — there is no return path on the Fielded contract,
// and the opcode handler converts panics to VM errors.
// propagate via panic; opcode handlers convert panics to VM errors.
func (h *HostObject) marshal(rv reflect.Value) types.Boxed {
v, err := h.interp.Marshal(rv.Interface())
if err != nil {
Expand Down
28 changes: 22 additions & 6 deletions interp/interp.go
Original file line number Diff line number Diff line change
Expand Up @@ -207,7 +207,7 @@ func New(prog *program.Program, opts ...func(*option)) *Interpreter {
case types.Ref:
val = types.BoxRef(int(v))
default:
val = types.BoxRef(i.alloc(v))
val = types.BoxRef(i.allocRoot(v))
}
i.constants[j] = val
}
Expand Down Expand Up @@ -413,7 +413,7 @@ func (i *Interpreter) Alloc(val types.Value) (int, error) {
}
val = types.Unbox(v)
}
return i.alloc(val), nil
return i.allocRoot(val), nil
}

func (i *Interpreter) Retain(addr int) (types.Value, error) {
Expand Down Expand Up @@ -619,7 +619,7 @@ func (i *Interpreter) box(val types.Value) types.Boxed {
case types.Ref:
return types.BoxRef(int(v))
default:
addr := i.alloc(v)
addr := i.allocRoot(v)
return types.BoxRef(addr)
}
}
Expand All @@ -635,9 +635,6 @@ func (i *Interpreter) unbox(val types.Boxed) types.Value {
}

func (i *Interpreter) alloc(val types.Value) int {
roots := i.traceRoot(val)
defer i.unroot(roots)

if len(i.free) > 0 {
addr := i.free[len(i.free)-1]
i.free = i.free[:len(i.free)-1]
Expand Down Expand Up @@ -674,6 +671,12 @@ func (i *Interpreter) alloc(val types.Value) int {
return len(i.heap) - 1
}

func (i *Interpreter) allocRoot(val types.Value) int {
roots := i.traceRoot(val)
defer i.unroot(roots)
return i.alloc(val)
}

func (i *Interpreter) retain(addr int) {
i.rc[addr]++
}
Expand Down Expand Up @@ -788,3 +791,16 @@ func (i *Interpreter) gc() {
}
}
}

func unboxRef[T types.Value](i *Interpreter, val types.Boxed) T {
if val.Kind() != types.KindRef {
panic(ErrTypeMismatch)
}
addr := val.Ref()
v, ok := i.heap[addr].(T)
if !ok {
panic(ErrTypeMismatch)
}
i.release(addr)
return v
}
29 changes: 21 additions & 8 deletions interp/interp_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@ import (
)

type test struct {
name string
program *program.Program
opts []func(*option)
values []types.Value
Expand Down Expand Up @@ -2008,6 +2007,21 @@ var tests = []test{
),
values: []types.Value{types.I32(0)},
},
{
program: program.New(
[]instr.Instruction{
instr.New(instr.I32_CONST, 1),
instr.New(instr.I64_CONST, uint64(int64(1<<50))),
instr.New(instr.I32_CONST, 1),
instr.New(instr.MAP_NEW, 0),
instr.New(instr.I32_CONST, 1),
instr.New(instr.MAP_GET),
},
program.WithTypes(types.NewMapType(types.TypeI32, types.TypeI64)),
),
opts: []func(*option){WithHeap(1)},
values: []types.Value{types.I64(1 << 50)},
},
// --- recursive: fibonacci (i32), factorial (i64) ---
{
program: program.New(
Expand Down Expand Up @@ -2337,11 +2351,7 @@ func runAll(t *testing.T, modeOpts ...func(*option)) {
t.Helper()
for _, tt := range tests {
tt := tt
name := tt.name
if name == "" {
name = tt.program.String()
}
t.Run(name, func(t *testing.T) {
t.Run(tt.program.String(), func(t *testing.T) {
opts := append([]func(*option){}, tt.opts...)
opts = append(opts, modeOpts...)
i := New(tt.program, opts...)
Expand Down Expand Up @@ -3308,7 +3318,9 @@ func TestInterpreter_Marshal(t *testing.T) {
require.True(t, ok)
require.True(t, m.Typ.Key.Equals(types.TypeString))
require.True(t, m.Typ.Elem.Equals(types.TypeI32))
require.Equal(t, types.BoxI32(1), m.Entries[types.MapKey{Kind: types.KindRef, Text: "a"}].Value)
entry, ok := m.Get(types.MapKey{Kind: types.KindRef, Text: "a"})
require.True(t, ok)
require.Equal(t, types.BoxI32(1), entry.Value)
})

t.Run("int map uses i64 value boxes", func(t *testing.T) {
Expand All @@ -3320,7 +3332,8 @@ func TestInterpreter_Marshal(t *testing.T) {

m, ok := got.(*types.Map)
require.True(t, ok)
entry := m.Entries[types.MapKey{Kind: types.KindRef, Text: "a"}]
entry, ok := m.Get(types.MapKey{Kind: types.KindRef, Text: "a"})
require.True(t, ok)
require.True(t, m.Typ.Elem.Equals(types.TypeI64))
require.Equal(t, types.KindI64, entry.Value.Kind())
})
Expand Down
120 changes: 0 additions & 120 deletions interp/map.go

This file was deleted.

Loading
Loading