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
2 changes: 2 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,8 @@ Violations cause silent corruption or invalid execution.
- Heap index `0` is permanently `Null`.
- `release()` must stay iterative, never recursive.
- Threaded closure errors should `panic`; `interp.Run()` recovers and annotates `at=<ip>`.
- A `frame` separates `addr` (template/code index for `i.code`/`i.instrs`/profiler/JIT) from `ref` (heap index released on `RETURN`). They differ for closures; every frame-creating `CALL`/fused path must set both, and non-closure paths must reset `upvals = nil`.
- `closure.new` takes the function ref from the stack top (like `call`) and transfers ownership of the function ref plus its upvals into the closure.

### Threaded Compiler

Expand Down
13 changes: 12 additions & 1 deletion docs/instruction-set.md
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ Offsets are signed 16-bit values encoded little-endian. `BR 5` skips 5 bytes pas
| `BR` | `{2}` | `→` | ◐ | Unconditional relative jump. JIT only when current segment has no pending return values. |
| `BR_IF` | `{2}` | `cond →` | ◐ | Jump if `cond ≠ 0`, else fall through. JIT only for simple stack shapes. |
| `BR_TABLE` | `{-2, 2}` | `index →` | ◐ | Jump table; negative or out-of-range index uses default target. JIT only for simple stack shapes. |
| `CALL` | `{}` | `fn →` | ⬜ | Call `*Function` or `*HostFunction`; pushes a frame. |
| `CALL` | `{}` | `fn →` | ⬜ | Call `*Function`, `*HostFunction`, or `*Closure`; pushes a frame. |
| `RETURN` | `{}` | `→` | ⬜ | Return from current frame. |

## Variables
Expand All @@ -97,6 +97,17 @@ Offsets are signed 16-bit values encoded little-endian. `BR 5` skips 5 bytes pas
| `REF_NE` | `{}` | `a b → i32` | ⬜ | Push `I32(1)` if refs differ. |
| `REF_TEST` | `{2}` | `ref → i32` | ⬜ | Push `I32(1)` if ref matches type at u16 index. |
| `REF_CAST` | `{2}` | `ref → ref` | ⬜ | Trap with `ErrTypeMismatch` if ref type mismatches. |
| `REF_NEW` | `{}` | `x → ref` | ⬜ | Box a non-ref scalar (`I32/I64/F32/F64`) onto the heap as a mutable cell; trap `ErrTypeMismatch` on a ref operand. Reuses the scalar heap rows. |
| `REF_GET` | `{}` | `ref → x` | ⬜ | Load the scalar held by a cell; trap `ErrTypeMismatch` if the target is not a scalar. Consumes (releases) the ref. |
| `REF_SET` | `{}` | `ref x →` | ⬜ | Overwrite a cell's scalar; trap `ErrTypeMismatch` if `x` is a ref. Consumes (releases) the ref. |

## Closures

| Opcode | Widths | Stack | JIT | Description |
|---|---|---|---|---|
| `CLOSURE_NEW` | `{}` | `upval1 … upvalN fn → closure` | ⬜ | Pop the `*Function` template (top of stack, like `call`), read `N = len(fn.Captures)`, pop N upvalues below it, and push a `*Closure` capturing them. Ownership of `fn` and the upvalues transfers into the closure. |
| `UPVAL_GET` | `{1}` | `→ x` | ⬜ | Push the closure upvalue at u8 index; traps `ErrSegmentationFault` outside a closure frame or out of range. |
| `UPVAL_SET` | `{1}` | `x →` | ⬜ | Store into the closure upvalue at u8 index (persists across calls to the same closure); same trap conditions as `UPVAL_GET`. |

## i32 Operations

Expand Down
12 changes: 11 additions & 1 deletion docs/memory-model.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,8 +35,9 @@ RC is manually handled in every threaded closure touching refs.
| pop/use ref from stack | `release(addr)` |
| `DUP` ref | `retain(addr)` |
| `DROP` ref | `release(addr)` |
| store ref to local/global | `retain(new)`, `release(old)` |
| store ref to local/global/upvalue | `retain(new)`, `release(old)` |
| map insert/replace/delete/clear | transfer or release map-owned ref keys/values |
| `CLOSURE_NEW` | transfer popped `fn` + upvalue refs into the closure (no retain/release); `allocRoot` before adjusting `sp` |

`retain(addr)` increments `rc[addr]`.

Expand Down Expand Up @@ -120,6 +121,15 @@ Only `KindRef` values need RC. `KindI32`, `KindI64`, `KindF32`, `KindF64` are va

Never cache pointer into `heap` across potential `alloc`; slice may reallocate. Keep integer indices.

### Closure frames separate code identity from the released callable

A frame stores both `addr` (the function/template heap index used to index `i.code`,
`i.instrs`, and the profiler/JIT) and `ref` (the heap index released on `RETURN`). For a
plain function these are equal; for a closure they diverge — `addr` is the template
(`Closure.Fn`) while `ref` is the closure instance. Profiling/JIT must use `addr`;
release must use `ref`. A closure keeps its template alive through its `Fn` ref and its
captured cells alive through its upval refs until the closure's RC reaches `0`.

## Host Function Memory Access

Host functions use `Interpreter` API:
Expand Down
9 changes: 9 additions & 0 deletions docs/value-representation.md
Original file line number Diff line number Diff line change
Expand Up @@ -126,13 +126,22 @@ Heap objects implement `types.Value`.
| `*types.Map` | `KindRef` | `*MapType` | `Value`, `Traceable` |
| `*types.MapI32`, `*types.MapI64`, `*types.MapF32`, `*types.MapF64` | `KindRef` | `*MapType` | `Value`, `Traceable` |
| `*types.Function` | `KindRef` | `*FunctionType` | `Value` |
| `*types.Closure` | `KindRef` | `*FunctionType` | `Value`, `Traceable` |
| `*interp.HostFunction` | `KindRef` | `*FunctionType` | `Value` |
| `*interp.HostObject` | `KindRef` | `*StructType` | `Value`, `Traceable` |

`*types.Closure` **shares** `*FunctionType` with its underlying function: a closure and a
function with the same signature are type-equal. Captures live on `*types.Function`
(`Captures []Type`, parallel to `Locals`), never on `*FunctionType`, so they never affect
type equality. `REF_NEW`/`REF_GET`/`REF_SET` reuse the `types.I32/I64/F32/F64` heap rows as
mutable scalar cells.

`Traceable` exposes `Refs() []Ref` for GC graph traversal. Any heap object containing refs must implement `Traceable`.
`Array`, `Struct`, `Map` variants, and `HostObject` defer their `Refs()` result allocation until
the first nested ref is found, so release of values with no children stays
allocation-free while ref-containing values keep one pre-sized result slice.
`*types.Closure` always reports at least its template (`Fn`), so it pre-sizes its `Refs()`
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.

Expand Down
9 changes: 9 additions & 0 deletions instr/opcode.go
Original file line number Diff line number Diff line change
Expand Up @@ -183,4 +183,13 @@ const (
MAP_SET
MAP_DELETE
MAP_CLEAR

REF_NEW
REF_GET
REF_SET

CLOSURE_NEW

UPVAL_GET
UPVAL_SET
)
15 changes: 15 additions & 0 deletions instr/parse_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,21 @@ func TestParse(t *testing.T) {
line: "local.get 0x02",
want: New(LOCAL_GET, 2),
},
{
name: "closure.new",
line: "closure.new",
want: New(CLOSURE_NEW),
},
{
name: "upval.get",
line: "upval.get 0x01",
want: New(UPVAL_GET, 1),
},
{
name: "ref.new",
line: "ref.new",
want: New(REF_NEW),
},
{
name: "br_table",
line: "br_table 0x02 0x0000 0x0001 0x0000",
Expand Down
9 changes: 9 additions & 0 deletions instr/type.go
Original file line number Diff line number Diff line change
Expand Up @@ -186,6 +186,15 @@ var types = map[Opcode]Type{
MAP_SET: {"map.set", []int{}},
MAP_DELETE: {"map.delete", []int{}},
MAP_CLEAR: {"map.clear", []int{}},

REF_NEW: {"ref.new", []int{}},
REF_GET: {"ref.get", []int{}},
REF_SET: {"ref.set", []int{}},

CLOSURE_NEW: {"closure.new", []int{}},

UPVAL_GET: {"upval.get", []int{1}},
UPVAL_SET: {"upval.set", []int{1}},
}

func TypeOf(op Opcode) Type {
Expand Down
9 changes: 9 additions & 0 deletions instr/type_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -185,6 +185,15 @@ func TestTypeOf(t *testing.T) {
{opcode: MAP_SET},
{opcode: MAP_DELETE},
{opcode: MAP_CLEAR},

{opcode: REF_NEW},
{opcode: REF_GET},
{opcode: REF_SET},

{opcode: CLOSURE_NEW},

{opcode: UPVAL_GET},
{opcode: UPVAL_SET},
}

for _, tt := range tests {
Expand Down
2 changes: 2 additions & 0 deletions interp/fuse.go
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,9 @@ func (c *threadedCompiler) fuseFunction(fn *types.Function, addr int) func(*Inte
i.fr.ip += 4
f := &i.frames[i.fp]
f.code = i.code[addr]
f.upvals = nil
f.addr = addr
f.ref = addr
f.ip = 0
f.bp = i.sp - params
f.returns = returns
Expand Down
4 changes: 3 additions & 1 deletion interp/interp.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,11 +43,13 @@ type Interpreter struct {

type frame struct {
code []func(*Interpreter)
upvals []types.Boxed
addr int
ref int
release bool
ip int
bp int
returns int
release bool
}

type option struct {
Expand Down
198 changes: 198 additions & 0 deletions interp/interp_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2090,6 +2090,204 @@ var tests = []test{
),
values: []types.Value{types.I64(3628800)},
},
// --- refs: REF_NEW, REF_GET, REF_SET ---
{
program: program.New(
[]instr.Instruction{
instr.New(instr.I32_CONST, 7),
instr.New(instr.REF_NEW),
instr.New(instr.REF_GET),
},
),
values: []types.Value{types.I32(7)},
},
{
program: program.New(
[]instr.Instruction{
instr.New(instr.I32_CONST, 1),
instr.New(instr.REF_NEW),
instr.New(instr.DUP),
instr.New(instr.I32_CONST, 9),
instr.New(instr.REF_SET),
instr.New(instr.REF_GET),
},
),
values: []types.Value{types.I32(9)},
},
{
program: program.New(
[]instr.Instruction{
instr.New(instr.REF_NULL),
instr.New(instr.REF_NEW),
},
),
err: ErrTypeMismatch,
},
{
program: program.New(
[]instr.Instruction{
instr.New(instr.REF_GET),
},
),
err: ErrStackUnderflow,
},
{
program: program.New(
[]instr.Instruction{
instr.New(instr.CONST_GET, 0),
instr.New(instr.REF_GET),
},
program.WithConstants(
types.NewFunctionBuilder(nil).Build(),
),
),
err: ErrTypeMismatch,
},
{
program: program.New(
[]instr.Instruction{
instr.New(instr.I32_CONST, 0),
instr.New(instr.REF_NEW),
instr.New(instr.REF_NULL),
instr.New(instr.REF_SET),
},
),
err: ErrTypeMismatch,
},
// --- closures: CLOSURE_NEW, UPVAL_GET, UPVAL_SET ---
{
// no-capture closure: behaves like calling the function directly
program: program.New(
[]instr.Instruction{
instr.New(instr.CONST_GET, 0),
instr.New(instr.CLOSURE_NEW),
instr.New(instr.CALL),
},
program.WithConstants(
types.NewFunctionBuilder(&types.FunctionType{
Returns: []types.Type{types.TypeI32},
}).Emit(
instr.New(instr.I32_CONST, 42),
instr.New(instr.RETURN),
).Build(),
),
),
values: []types.Value{types.I32(42)},
},
{
// single mutable closure: a counter, called three times yields 3
program: program.New(
[]instr.Instruction{
instr.New(instr.I32_CONST, 0),
instr.New(instr.CONST_GET, 0),
instr.New(instr.CLOSURE_NEW),
instr.New(instr.GLOBAL_SET, 0),
instr.New(instr.GLOBAL_GET, 0),
instr.New(instr.CALL),
instr.New(instr.DROP),
instr.New(instr.GLOBAL_GET, 0),
instr.New(instr.CALL),
instr.New(instr.DROP),
instr.New(instr.GLOBAL_GET, 0),
instr.New(instr.CALL),
},
program.WithConstants(
types.NewFunctionBuilder(&types.FunctionType{
Returns: []types.Type{types.TypeI32},
}).WithCaptures(types.TypeI32).Emit(
instr.New(instr.UPVAL_GET, 0),
instr.New(instr.I32_CONST, 1),
instr.New(instr.I32_ADD),
instr.New(instr.UPVAL_SET, 0),
instr.New(instr.UPVAL_GET, 0),
instr.New(instr.RETURN),
).Build(),
),
),
values: []types.Value{types.I32(3)},
},
{
// two closures sharing one heap-boxed variable via ref.new
program: program.New(
[]instr.Instruction{
instr.New(instr.I32_CONST, 0),
instr.New(instr.REF_NEW),
instr.New(instr.DUP),
instr.New(instr.CONST_GET, 0),
instr.New(instr.CLOSURE_NEW),
instr.New(instr.GLOBAL_SET, 0),
instr.New(instr.CONST_GET, 1),
instr.New(instr.CLOSURE_NEW),
instr.New(instr.GLOBAL_SET, 1),
instr.New(instr.GLOBAL_GET, 0),
instr.New(instr.CALL),
instr.New(instr.GLOBAL_GET, 0),
instr.New(instr.CALL),
instr.New(instr.GLOBAL_GET, 1),
instr.New(instr.CALL),
},
program.WithConstants(
types.NewFunctionBuilder(&types.FunctionType{}).
WithCaptures(types.TypeRef).Emit(
instr.New(instr.UPVAL_GET, 0),
instr.New(instr.UPVAL_GET, 0),
instr.New(instr.REF_GET),
instr.New(instr.I32_CONST, 1),
instr.New(instr.I32_ADD),
instr.New(instr.REF_SET),
instr.New(instr.RETURN),
).Build(),
types.NewFunctionBuilder(&types.FunctionType{
Returns: []types.Type{types.TypeI32},
}).WithCaptures(types.TypeRef).Emit(
instr.New(instr.UPVAL_GET, 0),
instr.New(instr.REF_GET),
instr.New(instr.RETURN),
).Build(),
),
),
values: []types.Value{types.I32(2)},
},
{
program: program.New(
[]instr.Instruction{
instr.New(instr.CLOSURE_NEW),
},
),
err: ErrStackUnderflow,
},
{
program: program.New(
[]instr.Instruction{
instr.New(instr.I32_CONST, 5),
instr.New(instr.CLOSURE_NEW),
},
),
err: ErrTypeMismatch,
},
{
// function expects two captures but none are on the stack
program: program.New(
[]instr.Instruction{
instr.New(instr.CONST_GET, 0),
instr.New(instr.CLOSURE_NEW),
},
program.WithConstants(
types.NewFunctionBuilder(nil).
WithCaptures(types.TypeI32, types.TypeI32).Build(),
),
),
err: ErrStackUnderflow,
},
{
// upval.get outside a closure frame has no upvalues
program: program.New(
[]instr.Instruction{
instr.New(instr.UPVAL_GET, 0),
},
),
err: ErrSegmentationFault,
},
}

type recordingMarshaler struct {
Expand Down
Loading
Loading