Skip to main content

Debugging

gopy is still building out its inspection toolkit. The fast path is to lean on the surfaces that are wired today and use CPython for the rest. Because gopy's bytecode is byte-equivalent with CPython 3.14, CPython's own dis, inspect, and ast tooling answers most "what is the interpreter seeing?" questions.

Reading a traceback

Unhandled exceptions print a CPython-shaped traceback to stderr:

Traceback (most recent call last):
File "demo.py", line 4, in <module>
raise ValueError("bad input")
ValueError: bad input

The file/line/column information comes from PEP 657 location tables emitted by the assembler. Lines under ^^^ highlight the exact sub-expression that raised, just like CPython.

From a Go embedding caller, the error returned by pythonrun is a *errors.PyError. Unwrap it for the same data:

import "github.com/tamnd/gopy/errors"

if err := pythonrun.RunSimpleString(ts, src, globals, &buf); err != nil {
var pyErr *errors.PyError
if errors.As(err, &pyErr) {
fmt.Println("type:", pyErr.Type)
fmt.Println("msg:", pyErr.Value)
for _, frame := range pyErr.Traceback {
fmt.Printf(" %s:%d in %s\n", frame.Filename, frame.Lineno, frame.Name)
}
}
}

Disassembling bytecode

gopy does not currently ship a Python-level dis module. The recommended workflow is to disassemble under CPython 3.14: gopy's bytecode matches byte-for-byte, so what CPython prints is what gopy runs.

python3 -m dis demo.py

From Go, the compiled code object is available directly:

mod, _ := parser.ParseString(src, "<inline>", parser.ModeFile)
code, _ := compile.Compile(mod, "<inline>", 0)
fmt.Printf("co_name: %s\n", code.Name)
fmt.Printf("co_argcount: %d\n", code.Argcount)
fmt.Printf("co_varnames: %v\n", code.Varnames)
fmt.Printf("co_consts: %v\n", code.Consts)
fmt.Printf("co_code len: %d bytes\n", len(code.Code))

The fields mirror CPython's PyCodeObject. See compile/code.go for the full struct.

Inspecting the parser and AST

To see what the parser produced:

mod, _ := parser.ParseString(src, "<inline>", parser.ModeFile)
ast.Dump(os.Stdout, mod)

ast.Dump formats the tree the same way CPython's ast.dump does. The two outputs are diffed in parser/parity_test.go; if they differ that is a parity bug, not a debugging tip.

For the token stream:

toks, _ := tokenize.Tokenize(strings.NewReader(src), "<inline>")
for _, t := range toks {
fmt.Printf("%-10s %q @%d:%d\n", t.Type, t.Value, t.Start.Line, t.Start.Col)
}

Tier-2 trace introspection

The Tier-2 projector records uop sequences when a hot loop trips its threshold. The executor exposes them for inspection:

import "github.com/tamnd/gopy/optimizer"

// After running code that hits the trace threshold.
for _, exec := range optimizer.AllExecutors(ts) {
fmt.Println("trace:", exec.Code.Name, "len:", exec.TraceLen())
for i := 0; i < exec.TraceLen(); i++ {
op, oparg := exec.UopAt(i)
fmt.Printf(" %3d %-30s %d\n", i, op, oparg)
}
}

Per-uop bodies are still being ported. A trace whose uops are all _NOP or _EXIT_TRACE means the projector saw the shape but the executor has not yet been wired for those opcodes; the interpreter quietly falls back to Tier-1 dispatch.

Specializer state

PEP 659 specialization records per-instruction caches. To see the specialized form of a call site:

for i, instr := range code.Instructions() {
if instr.IsSpecialized() {
fmt.Printf("%d: %s -> %s (hits=%d)\n",
i, instr.BaseOp(), instr.SpecializedOp(), instr.HitCount())
}
}

Cache layouts live in specialize/. Each _ADAPTIVE opcode has a matching specialised form (LOAD_ATTR_INSTANCE_VALUE, CALL_PY_EXACT_ARGS, etc.) once the heuristics fire.

sys.monitoring

The PEP 669 monitoring API is fully wired. To trace every function call from inside a script:

import sys
events = sys.monitoring.events
tool = 0
sys.monitoring.use_tool_id(tool, "tracer")
sys.monitoring.set_events(tool, events.PY_START | events.PY_RETURN)

def on_start(code, offset):
print("enter", code.co_qualname)

sys.monitoring.register_callback(tool, events.PY_START, on_start)

The full event set: PY_START, PY_RESUME, PY_RETURN, PY_YIELD, CALL, LINE, INSTRUCTION, JUMP, BRANCH, STOP_ITERATION, RAISE, EXCEPTION_HANDLED, PY_UNWIND, PY_THROW, RERAISE.

Legacy tracing

sys.settrace is also wired and produces the events CPython documents. It is slower than sys.monitoring; prefer the new API unless you are running a debugger that has not been updated.

Logging from a script

The print builtin works. The full logging module is not yet ported. For embedded callers, capture stdout (see Recipes -> Capture stdout) and pipe it into your Go logger.

When to reach for CPython

For anything not on the list above, run the same source under CPython 3.14:

QuestionTool
Bytecode for a functionpython3 -m dis file.py
ASTpython3 -c "import ast; print(ast.dump(ast.parse(open('file.py').read())))"
Token streampython3 -m tokenize file.py
.pyc cache layoutCPython 3.14's dis.dis(open('cache.pyc', 'rb').read())
gc debug outputCPython's gc.set_debug(...)

When the gopy answer differs from CPython's, that is a parity bug. File it via Parity -> Reporting a parity bug.

Reference

  • errors/pyerror.go. The Go-side error type.
  • optimizer/executor.go. Tier-2 introspection.
  • specialize/. Per-opcode cache layouts.
  • monitor/. PEP 669 implementation.