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 docs/coding-patterns.md
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,8 @@ func (c *jitCompiler) branchClosure(fn Caller, sig *Signature) func(*Interpreter
}
```

For JIT code, keep architecture-neutral `jitCompiler` state and helpers in `interp/jit.go`. Put only architecture selection, opcode handlers, and ISA-specific emission helpers in `interp/jit_<arch>.go`.

## 2. Type & Interface Design

### 2.1 Interface-first
Expand Down
2 changes: 2 additions & 0 deletions docs/guides/add-architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ A new architecture needs:
3. non-target platform stubs
4. `interp/jit_<arch>.go` setting `arch` and registering opcode handlers

Follow the JIT file boundary rules in `docs/coding-patterns.md`.

## Step 1 — Create `asm/<arch>/`

Mirror `asm/arm64/`.
Expand Down
6 changes: 3 additions & 3 deletions docs/instruction-set.md
Original file line number Diff line number Diff line change
Expand Up @@ -79,9 +79,9 @@ target = instruction_start + instruction_width + operand

| Opcode | Widths | Stack | JIT | Description |
|---|---|---|---|---|
| `GLOBAL_GET` | `{2}` | `→ x` | | Push global at u16 index. |
| `GLOBAL_SET` | `{2}` | `x →` | | Store global at u16 index. |
| `GLOBAL_TEE` | `{2}` | `x → x` | | Store global and keep value. |
| `GLOBAL_GET` | `{2}` | `→ x` | | Push global at u16 index. JIT supports same-segment proven numeric globals. |
| `GLOBAL_SET` | `{2}` | `x →` | | Store global at u16 index. JIT supports numeric globals. |
| `GLOBAL_TEE` | `{2}` | `x → x` | | Store global and keep value. JIT supports numeric globals. |
| `LOCAL_GET` | `{1}` | `→ x` | ◐ | Push u8 local relative to frame base. JIT supports numeric params/locals. |
| `LOCAL_SET` | `{1}` | `x →` | ◐ | Store u8 local. JIT supports numeric params/locals. |
| `LOCAL_TEE` | `{1}` | `x → x` | ◐ | Store local and keep value. JIT supports numeric params/locals. |
Expand Down
241 changes: 101 additions & 140 deletions docs/jit-internals.md

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions interp/interp.go
Original file line number Diff line number Diff line change
Expand Up @@ -492,6 +492,7 @@ func (i *Interpreter) jit(addr int) error {
addr: addr,
types: i.types,
constants: i.constants,
globals: i.globals,
heap: i.heap,
cutoff: i.cutoff,
}
Expand Down
116 changes: 116 additions & 0 deletions interp/interp_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -175,6 +175,37 @@ var tests = []struct {
),
values: []types.Value{types.I32(1)},
},
{
program: program.New(
[]instr.Instruction{
instr.New(instr.I64_CONST, 7),
instr.New(instr.GLOBAL_TEE, 0),
instr.New(instr.GLOBAL_GET, 0),
instr.New(instr.I64_ADD),
},
),
values: []types.Value{types.I64(14)},
},
{
program: program.New(
[]instr.Instruction{
instr.New(instr.F32_CONST, uint64(math.Float32bits(1.5))),
instr.New(instr.GLOBAL_SET, 0),
instr.New(instr.GLOBAL_GET, 0),
},
),
values: []types.Value{types.F32(1.5)},
},
{
program: program.New(
[]instr.Instruction{
instr.New(instr.F64_CONST, math.Float64bits(1.5)),
instr.New(instr.GLOBAL_SET, 0),
instr.New(instr.GLOBAL_GET, 0),
},
),
values: []types.Value{types.F64(1.5)},
},
{
program: program.New(
[]instr.Instruction{
Expand Down Expand Up @@ -2615,6 +2646,91 @@ func TestInterpreter_Run(t *testing.T) {
require.Equal(t, uint64(1), jit.Skips)
})

t.Run("jit compiles numeric globals", func(t *testing.T) {
if arch == nil {
t.Skip("jit is not available on this architecture")
}
p := prof.New()
p.Add(0, 0, byte(instr.I32_CONST))
i := New(program.New([]instr.Instruction{
instr.New(instr.I32_CONST, 9),
instr.New(instr.GLOBAL_SET, 0),
instr.New(instr.GLOBAL_GET, 0),
}), WithProfile(p), WithCutoff(1))
defer i.Close()
i.globals = append(i.globals, types.BoxI32(1))

err := i.jit(0)
require.NoError(t, err)

err = i.Run(context.Background())
require.NoError(t, err)
val, err := i.Pop()
require.NoError(t, err)
require.Equal(t, types.I32(9), val)

jit := p.Snapshot().JIT
require.Equal(t, uint64(1), jit.Attempts)
require.NotZero(t, jit.Emits)
require.NotZero(t, jit.Links)
})

t.Run("jit skips ref globals", func(t *testing.T) {
if arch == nil {
t.Skip("jit is not available on this architecture")
}
p := prof.New()
p.Add(0, 0, byte(instr.GLOBAL_GET))
i := New(program.New([]instr.Instruction{
instr.New(instr.GLOBAL_GET, 0),
}), WithProfile(p), WithCutoff(1))
defer i.Close()
i.globals = append(i.globals, types.BoxedNull)

err := i.jit(0)
require.NoError(t, err)

err = i.Run(context.Background())
require.NoError(t, err)
val, err := i.Pop()
require.NoError(t, err)
require.Equal(t, types.Null, val)

jit := p.Snapshot().JIT
require.Equal(t, uint64(1), jit.Attempts)
require.Zero(t, jit.Emits)
require.Zero(t, jit.Links)
})

t.Run("jit skips global get without proven kind", func(t *testing.T) {
if arch == nil {
t.Skip("jit is not available on this architecture")
}
p := prof.New()
p.Add(0, 0, byte(instr.NOP))
i := New(program.New([]instr.Instruction{
instr.New(instr.NOP),
instr.New(instr.GLOBAL_GET, 0),
}), WithProfile(p), WithCutoff(1))
defer i.Close()
i.globals = append(i.globals, types.BoxI32(1))

err := i.jit(0)
require.NoError(t, err)
i.globals[0] = types.BoxF32(2.5)

err = i.Run(context.Background())
require.NoError(t, err)
val, err := i.Pop()
require.NoError(t, err)
require.Equal(t, types.F32(2.5), val)

jit := p.Snapshot().JIT
require.Equal(t, uint64(1), jit.Attempts)
require.Zero(t, jit.Emits)
require.Zero(t, jit.Links)
})

t.Run("hook cancel is observed on next jit tick", func(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
Expand Down
95 changes: 62 additions & 33 deletions interp/jit.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,20 +14,23 @@ import (
)

type jitCompiler struct {
assembler *asm.Assembler
profile *prof.Stats
addr int
types []types.Type
constants []types.Boxed
heap []types.Value
code []byte
ip int
cutoff int
labels map[int]int
compilable map[int]bool
sigs map[int]*asm.Signature
scratch []asm.PReg
end int
assembler *asm.Assembler
profile *prof.Stats
addr int
types []types.Type
constants []types.Boxed
globals []types.Boxed
heap []types.Value
code []byte
ip int
cutoff int
entry int
labels map[int]int
compilable map[int]bool
sigs map[int]*asm.Signature
globalKinds map[int]types.Kind
scratch []asm.PReg
end int
}

type profiledBlock struct {
Expand All @@ -43,6 +46,7 @@ var (
const (
rStack = iota
rHeap
rGlobals
rNext
)

Expand Down Expand Up @@ -183,10 +187,13 @@ func (c *jitCompiler) compile(b *analysis.BasicBlock) ([]*asm.RelocObject, []int

func (c *jitCompiler) segment(code []byte, start, end int) (*asm.RelocObject, int, bool) {
c.ip = start
c.entry = start
c.end = end
c.globalKinds = make(map[int]types.Kind)
c.scratch = append(c.scratch[:0], c.assembler.Scratch())
c.scratch = append(c.scratch, c.assembler.Scratch())
c.scratch = append(c.scratch, c.assembler.Scratch())
c.scratch = append(c.scratch, c.assembler.Scratch())

jit[_PROLOGUE](c)
if id, ok := c.labels[start]; ok {
Expand Down Expand Up @@ -284,10 +291,18 @@ func (c *jitCompiler) linkable(targetIP int) bool {

func (c *jitCompiler) closure(fn asm.Caller, sig *asm.Signature) func(*Interpreter) {
nParams := len(sig.Params)
kinds := c.kinds(sig.Returns)
params := make([]uint64, nParams)
scratch := make([]uint64, len(sig.Scratch))

kinds := make([]types.Kind, nParams)
for i, p := range sig.Params {
p, ok := c.kind(p)
if !ok {
return nil
}
kinds[i] = p
}

return func(i *Interpreter) {
base := i.sp - nParams
for j := range nParams {
Expand All @@ -304,6 +319,13 @@ func (c *jitCompiler) closure(fn asm.Caller, sig *asm.Signature) func(*Interpret
scratch[rHeap] = 0
}
}
if len(scratch) > rGlobals {
if len(i.globals) > 0 {
scratch[rGlobals] = uint64(uintptr(unsafe.Pointer(&i.globals[0])))
} else {
scratch[rGlobals] = 0
}
}
rets, err := fn.Call(params, &scratch)
if err != nil {
panic(err)
Expand All @@ -316,25 +338,15 @@ func (c *jitCompiler) closure(fn asm.Caller, sig *asm.Signature) func(*Interpret
}
}

func (c *jitCompiler) kinds(regs []asm.PReg) []types.Kind {
kinds := make([]types.Kind, len(regs))
for i, p := range regs {
switch p.Type() {
case asm.RegTypeFloat:
if p.Width() == asm.Width32 {
kinds[i] = types.KindF32
} else {
kinds[i] = types.KindF64
}
default:
if p.Width() == asm.Width32 {
kinds[i] = types.KindI32
} else {
kinds[i] = types.KindI64
}
}
func (c *jitCompiler) global(idx int) (int16, bool) {
if idx < 0 || idx >= len(c.globals) {
return 0, false
}
return kinds
offset := idx * 8
if offset > int(^uint16(0)>>1) {
return 0, false
}
return int16(offset), true
}

func (c *jitCompiler) local(idx int) (types.Type, bool) {
Expand All @@ -358,3 +370,20 @@ func (c *jitCompiler) local(idx int) (types.Type, bool) {

return fn.Locals[idx], true
}

func (c *jitCompiler) kind(r0 asm.Reg) (types.Kind, bool) {
switch r0.Type() {
case asm.RegTypeFloat:
if r0.Width() == asm.Width32 {
return types.KindF32, true
}
return types.KindF64, true
case asm.RegTypeInt:
if r0.Width() == asm.Width32 {
return types.KindI32, true
}
return types.KindI64, true
default:
return 0, false
}
}
Loading
Loading