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
7 changes: 6 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -189,7 +189,7 @@ vm := interp.New(prog,
interp.WithStack(4096), // value stack slots (default: 1024)
interp.WithHeap(512), // initial heap capacity (default: 128)
interp.WithFrame(256), // max call depth (default: 128)
interp.WithThreshold(4096), // ticks before JIT (default: 4096)
interp.WithThreshold(4096), // ticks before JIT; 0 = first sample, negative disables JIT
interp.WithTick(128), // sample/poll cadence (default: 128)
interp.WithFuel(10_000), // instruction budget (default: unlimited)
interp.WithHook(func(vm *interp.Interpreter) error {
Expand All @@ -201,6 +201,11 @@ vm := interp.New(prog,

`WithTick` controls profiling samples, context-cancellation polling, hook cadence, and fuel consumption. `WithFuel` accepts an expected instruction budget and rounds it up to the nearest tick interval internally; `WithFuel(0)` is unlimited. Hooks run synchronously on the `Run` goroutine and receive the live interpreter; avoid concurrent interpreter access and preserve VM invariants when mutating state.

For bytecode-level debugging, use `NewDebugger` with `WithDebugger`. The
debugger provides breakpoints plus `Step`, `Next`, and `Finish`;
`WithDebugger` configures instruction-accurate hooks and disables JIT. See
[`docs/debugging.md`](docs/debugging.md).

For profile snapshots, JIT counters, and REPL `.profile` output, see
[`docs/profile.md`](docs/profile.md).

Expand Down
7 changes: 6 additions & 1 deletion README_kr.md
Original file line number Diff line number Diff line change
Expand Up @@ -189,7 +189,7 @@ vm := interp.New(prog,
interp.WithStack(4096), // 값 스택 슬롯 수 (기본값: 1024)
interp.WithHeap(512), // 초기 힙 용량 (기본값: 128)
interp.WithFrame(256), // 최대 호출 깊이 (기본값: 128)
interp.WithThreshold(4096), // JIT 트리거 틱 수 (기본값: 4096)
interp.WithThreshold(4096), // JIT 트리거 틱 수; 0은 첫 샘플, 음수이면 JIT 비활성화
interp.WithTick(128), // 샘플/폴링 주기 (기본값: 128)
interp.WithFuel(10_000), // 명령어 예산 (기본값: 무제한)
interp.WithHook(func(vm *interp.Interpreter) error {
Expand All @@ -201,6 +201,11 @@ vm := interp.New(prog,

`WithTick`은 프로파일 샘플, context 취소 확인, hook 호출 주기, fuel 소비를 함께 제어합니다. `WithFuel`은 기대 명령어 예산을 받고 내부에서 가장 가까운 tick 간격으로 올림 변환합니다. `WithFuel(0)`은 무제한입니다. Hook은 `Run` 고루틴에서 동기적으로 실행되며 실행 중인 인터프리터를 받습니다. 인터프리터에 대한 동시 접근은 피하고, 상태를 변경할 때는 VM 불변식을 유지해야 합니다.

바이트코드 단위 디버깅은 `NewDebugger`를 `WithDebugger`와 함께 사용합니다.
디버거는 breakpoint와 `Step`, `Next`, `Finish`를 제공하며, `WithDebugger`는
명령어 단위 hook을 설정하고 JIT를 비활성화합니다. 자세한 내용은
[`docs/debugging.md`](docs/debugging.md)를 참고하세요.

---

## 구현 현황
Expand Down
8 changes: 7 additions & 1 deletion docs/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ Read this when a change crosses package boundaries or you need to know where sta
| If you touch | Also read |
| --- | --- |
| `interp/` runtime state, frames, globals | [memory-model.md](memory-model.md), [value-representation.md](value-representation.md) |
| `interp/` debugger API or bytecode stepping | [debugging.md](debugging.md), [profile.md](profile.md) |
| `prof/` or profile options | [profile.md](profile.md) |
| `interp/threaded.go` or `interp/jit*.go` | [jit-internals.md](jit-internals.md), [instruction-set.md](instruction-set.md) |
| `analysis/`, `transform/`, `optimize/`, `pass/` | [pass-system.md](pass-system.md) |
Expand Down Expand Up @@ -201,7 +202,7 @@ Thin cobra entry point. The root command (no subcommand) launches the REPL with
4. interp.Run(ctx)
├─ main loop: code[f.ip](i)
├─ every 128 instructions: check ctx, consume fuel, call hook, prof.Add(addr, ip, opcode)
└─ when prof.Samples(addr) == threshold/tick:
└─ when JIT is enabled and prof.Samples(addr) reaches threshold rounded to tick cadence:
jitCompiler.Compile(instrs[addr])
└─ heat-sorted two-pass over sampled basic blocks:
├─ pass 1: for each hot block, segment() loop → emit sampled eligible segments
Expand All @@ -214,6 +215,11 @@ Thin cobra entry point. The root command (no subcommand) launches the REPL with
└─ buffer.Free() → munmap
```

`WithThreshold(0)` activates JIT on the first sample; negative thresholds
disable JIT compilation. Bytecode-level debugging uses
`NewDebugger` through `WithDebugger`, which configures instruction-accurate
hooks and disables JIT for exact instruction-boundary stops.

## Focus Areas

| Area | Direction |
Expand Down
103 changes: 103 additions & 0 deletions docs/debugging.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
# Debugging

Bytecode-level debugging for embedders.

## When to Read This

Read this when you:

- build a debugger, tracer, or test harness around `interp.Run`
- need breakpoint, step, next, or finish control
- inspect the current bytecode location, call frames, or operand stack

## Setup

Use `NewDebugger` with `WithDebugger`:

```go
dbg := interp.NewDebugger()
dbg.Break(0, 5)

vm := interp.New(prog, interp.WithDebugger(dbg))
defer vm.Close()

for {
err := vm.Run(ctx)
if errors.Is(err, interp.ErrStopped) {
stop := dbg.Stop()
_ = stop

dbg.Continue()
continue
}
if err != nil {
return err
}
break
}
```

`WithDebugger` installs the debugger hook, sets `WithTick(1)`, disables JIT with
`WithThreshold(-1)`, and asks the threaded compiler to preserve exact bytecode
instruction boundaries.

## Controls

| Method | Effect |
|---|---|
| `Continue()` | Run until a breakpoint, runtime error, context cancellation, fuel exhaustion, or normal exit |
| `Step()` | Execute one bytecode instruction, entering calls |
| `Next()` | Execute one bytecode instruction, stepping over calls |
| `Finish()` | Run until the current frame returns |

All stops happen before the current instruction executes. `Run` returns
`ErrStopped`, and `Stop()` returns the function index, instruction pointer, and
breakpoint ID (`0` when the stop was caused by stepping).

## Breakpoints

Breakpoints are identified by function index and bytecode offset:

```go
id := dbg.Break(0, 10)
dbg.Enable(id, false)
dbg.Enable(id, true)
dbg.Clear(id)
```

Use `BreakIf` for conditional breakpoints:

```go
dbg.BreakIf(0, 10, func(vm *interp.Interpreter) bool {
return vm.Len() > 0
})
```

`Breakpoints()` returns a sorted snapshot by breakpoint ID. Each breakpoint
tracks `Hits`.

## Inspection

Use the stopped interpreter directly:

| Method | Use |
|---|---|
| `Func()` | current function slot (`0` for top-level code) |
| `IP()` | current bytecode offset |
| `Opcode()` | opcode at the current bytecode offset |
| `FrameDepth()` | active frame count |
| `Frame(n)` | frame snapshot; `0` is current frame, `1` is caller |
| `Len()` / `Peek(n)` | operand stack inspection |
| `Local(n)` / `Global(n)` / `Const(n)` | value inspection |
| `Load(addr)` | resolve heap references |

`Frame(n)` returns `(fn, ip, bp, err)` and does not expose mutable internal frame
state.

## JIT and Precision

Debugging is bytecode-level. Exact stepping requires instruction-boundary
execution, so `WithDebugger` disables JIT and uses `WithTick(1)`, which makes
threaded compilation preserve observable bytecode offsets. Normal non-debug
execution keeps threaded fusion optimizations such as NOP run collapsing and
`const.get` + `call` fusion.
2 changes: 1 addition & 1 deletion docs/instruction-set.md
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ skips 5 bytes past the end of the 3-byte `BR` instruction.

| Opcode | Widths | Stack | JIT | Description |
|---|---|---|---|---|
| `NOP` | `{}` | `→` | ✅ | No-op. Threaded execution collapses consecutive NOP runs into a single dispatch step. JIT emits no native instruction. |
| `NOP` | `{}` | `→` | ✅ | No-op. Normal threaded execution collapses consecutive NOP runs into a single dispatch step; `WithTick(1)` preserves per-instruction hooks. JIT emits no native instruction. |
| `DROP` | `{}` | `x →` | ✅ | Pop and discard the top value. |
| `DUP` | `{}` | `x → x x` | ✅ | Duplicate the top value. |
| `SWAP` | `{}` | `a b → b a` | ✅ | Swap the top two stack values. |
Expand Down
4 changes: 3 additions & 1 deletion docs/jit-internals.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,9 @@ instr.OPCODE: func(c *threadedCompiler) func(i *Interpreter) {
- Reference counting: call `i.retain(addr)` when a ref enters the stack, `i.release(addr)` when consumed.
- Do not catch errors inside closures — `panic(ErrX)` and let `interp.Run`'s `recover` handle it.

**Special case — NOP (threaded):** Each NOP scans forward to count all consecutive NOPs starting at its own position, then advances `c.ip` by 1. This means `n` consecutive NOPs produce `n` closures, but only the first is reached in execution — it jumps `n` bytes forward, bypassing the rest. The NOP-padding that `ConstantFoldingPass` inserts therefore takes one dispatch for the whole run.
**Special case — NOP (threaded):** In normal execution, each NOP scans forward to count all consecutive NOPs starting at its own position, then advances `c.ip` by 1. This means `n` consecutive NOPs produce `n` closures, but only the first is reached in execution — it jumps `n` bytes forward, bypassing the rest. The NOP-padding that `ConstantFoldingPass` inserts therefore takes one dispatch for the whole run.

When `WithTick(1)` is configured, threaded compilation preserves exact bytecode instruction boundaries for hooks, profiling, and debugging. In that mode each NOP advances by one byte.

```go
instr.NOP: func(c *threadedCompiler) func(i *Interpreter) {
Expand Down
2 changes: 1 addition & 1 deletion docs/pass-system.md
Original file line number Diff line number Diff line change
Expand Up @@ -136,7 +136,7 @@ Before: [I32_CONST 3][I32_CONST 4][I32_ADD] (11 bytes)
After: [NOP][NOP][NOP][NOP][NOP][NOP][I32_CONST 7] (11 bytes)
```

The threaded NOP handler fast-forwards past all consecutive NOPs at runtime, so the left padding takes one dispatch for the whole run.
The normal threaded NOP handler fast-forwards past all consecutive NOPs at runtime, so the left padding takes one dispatch for the whole run. `WithTick(1)` preserves per-instruction boundaries for exact hooks and debugging.

Supported folds:
- `I32_CONST × I32_CONST × op` for all arithmetic/bitwise/comparison ops
Expand Down
12 changes: 8 additions & 4 deletions docs/profile.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,9 @@ default tick is 128. Each sample records:

The same tick cadence also drives context polling, fuel accounting, and hook
callbacks. Lower ticks give denser profile data but add more polling and
sampling overhead. The REPL `.profile` command intentionally uses `WithTick(1)`
so small examples show exact per-instruction samples.
sampling overhead. `WithDebugger` configures instruction-accurate hooks for
bytecode debugging. The REPL `.profile` command also uses `WithTick(1)` so
small examples show exact per-instruction samples.

## Library API

Expand Down Expand Up @@ -86,8 +87,11 @@ decisions. Treat them separately when tuning thresholds.

## Profile-Guided JIT

The JIT activates for a function when `Samples(fn) == WithThreshold / WithTick`.
The default is `4096 / 128 = 32` samples.
The JIT activates for a function when `Samples(fn)` reaches the configured
threshold rounded up to the tick cadence. The default is `4096 / 128 = 32`
samples.
`WithThreshold(0)` activates on the first sample. Negative thresholds disable
JIT compilation.

At compilation time, the JIT uses profile data in two places:

Expand Down
Loading
Loading