Skip to content

v0.12.0: Tier-2 optimizer (trace + uop interpreter)#20

Merged
tamnd merged 31 commits into
mainfrom
feat/v0.12.0-optimizer
May 8, 2026
Merged

v0.12.0: Tier-2 optimizer (trace + uop interpreter)#20
tamnd merged 31 commits into
mainfrom
feat/v0.12.0-optimizer

Conversation

@tamnd

@tamnd tamnd commented May 7, 2026

Copy link
Copy Markdown
Owner

Summary

v0.12.0 ports CPython's Tier-2 optimizer (interpreter-only, no JIT). The
goal is dis.dis parity with ENTER_EXECUTOR rows and the same
sys.monitoring / sys.settrace shape on warmed-up loops, not raw
speed.

What landed

Trace projection + executor lifecycle. New optimizer/ package
with _PyExecutorObject, _PyUOpInstruction, the 256-bit bloom
filter, and the side table that hangs off Code.Executors. The
two-phase free uses InterpState.ExecutorDeletionListHead so a
thread inside an executor cannot race tp_dealloc.

Warmup -> install -> dispatch. JUMP_BACKWARD warms a per-
codeunit counter; on overflow optimizer.Optimize projects a uop
trace, the install path patches the bytecode at the warmup site to
ENTER_EXECUTOR, and subsequent passes land in enterExecutor.

Uop dispatch loop. optimizer/uops.go is the driver; per-uop
methods on *Tier2State return Tier2Status (Continue / Error /
Deopt / Exit). 14 uops are hand-ported in uops_impl.go:
the trace prologue (_START_EXECUTOR, _MAKE_WARM,
_CHECK_VALIDITY, _SET_IP, _NOP, _EXIT_TRACE, _JUMP_TO_TOP)
plus the starter stack/local set (_LOAD_FAST, _LOAD_FAST_BORROW,
_STORE_FAST, _POP_TOP, _PUSH_NULL, _COPY, _SWAP). The
remaining 271 Tier-2-viable uops live as StatusDeopt stubs in the
generated uops_stubs_gen.go. Adding a body removes a stub on the
next regen.

Analysis pipeline. Analyze runs the three CPython phases:
removeGlobals (stub; the watcher fan-out lands in v0.13),
optimizeUops (init/fini bracket, contradiction and out-of-space
short-circuits; per-opcode bodies wait on the DSL case table), and
the full port of removeUnneededUops. The cleanup pass elides
_NOP rows, prunes _SET_IP ahead of non-escaping uops, and
collapses the load-then-pop idiom across the _LOAD_FAST /
_LOAD_CONST_INLINE family.

DSL generator. tools/uops_gen/ is the parallel to
tools/bytecodes_gen/, ported from Tools/cases_generator/. It
emits optimizer/uop_ids_gen.go, uop_meta_gen.go,
uops_dispatch_gen.go, and uops_stubs_gen.go. The generator
scans optimizer/ for hand-ported *Tier2State methods via
go/ast and writes only stubs for uops that do not already have a
method, so the dispatch switch and the stub set stay in sync as
ports land. The optimizer-cases pass over
Python/optimizer_bytecodes.c is in but the per-opcode body table
is left to v0.13 (along with the optimize_uops per-opcode arms).

Prep work bundled in. The COMPARE_OP oparg encoding bug
(emitCmpOp shipped the bare 0..5 kind; the dispatch reads
(oparg >> 5) & 0xf, so 1 == 1 evaluated as 1 < 1 and returned
False) is fixed at the codegen site to match
codegen.c codegen_addcompare. The lint backlog v0.11 merged with
is cleared.

The matching JIT path (jit.c) stays out of scope. gopy's Tier-2
stays interpreter-only.

Gates

  • v012test/optimizer_gate_test.go. Trace install + the
    ENTER_EXECUTOR deopt path against a tight LOAD_CONST,
    POP_TOP, JUMP_BACKWARD loop.
  • v012test/analysis_gate_test.go. The cleanup pass runs through
    Optimize; the giant-stacksize benign-bail path leaves the
    install site untouched.
  • v012test/uops_gate_test.go. The dispatch loop drives every
    hand-ported uop on a synthetic trace; a stubbed uop returns
    StatusDeopt cleanly; a real Optimize-produced trace terminates
    on StatusExit or StatusDeopt.
  • optimizer/uops_test.go, optimizer/analysis_test.go. Direct
    unit coverage for the dispatcher and the cleanup pass.

Test plan

  • go test ./... (full suite)
  • golangci-lint run ./... clean
  • All three v012 gates pass on Linux / macOS / Windows
  • dis.dis parity row for ENTER_EXECUTOR (covered by the
    install gate)

tamnd added 18 commits May 7, 2026 23:18
emitCmpOp emitted the bare 0..5 kind as the COMPARE_OP oparg. Dispatch
in vm/eval_simple.go reads `(oparg >> 5) & 0xf` to recover the kind.
With the bare encoding `1 == 1` evaluated as `1 < 1` and returned False.

Pre-encode the constants as `(cmp << 5) | compare_masks[cmp]` so the
high 5 bits carry Py_LT..Py_GE and the low 4 bits carry the TO_BOOL
mask CPython's flowgraph pass also relies on.
v0.11 merged with the lint check red. Fix the backlog before v0.12 work
opens new files:

* gofmt-realign monitor/events.go, specialize/backoff_test.go,
  specialize/deopt.go, vm/legacy_tracing.go.
* Rename objects.Type_ -> objects.ExactType (revive var-naming) and
  the normalizeEventSet `max` parameter to `maxBits`
  (redefines-builtin).
* Drop redundant `var x objects.Object = ...` declarations and
  unconvert calls in vm/legacy_tracing.go and vm/instrument_fire.go.
* Drop the unused `ev` parameter on monitor.addTools/removeTools and
  the unread bool result of specializeClassCall and the bumpVersion
  return value (unparam).
* Combine adjacent appends in monitor/line_test.go and
  vm/instrument_line_test.go (gocritic) and drop the redundant `|0`
  flagged by SA4016.
* Extend .golangci.yml exclusions for compile/codegen_expr_op.go
  (NB_*) and compile/instrseq.go (MIN_*) which mirror CPython enum
  spelling, and for monitor.forceInstrument /
  BuildSysMonitoring / vm.specializeAt whose branch trees mirror
  Python/instrumentation.c and Python/specialize.c.
Code grows an Executors slice the install path uses to thread
trace projections back into the frame, Dict tracks a small
mutationCount the watcher fans into and exposes EntryAt for the
analysis pass, Function gains GetVersionForCurrentState mirroring
the CPython helper, and type_specialize sprouts the modify hook
the type watcher uses.
The optimizer needs to read the current function (for the version
stamp on the rewritten globals guard) and the per-interpreter
canonical builtins dict (for the custom-builtins bail). Add the
FrameFunc accessor on the interface, implement it on frame.Frame,
fix up the test fakes, and park Builtins / BuiltinDictMutations on
state.Interpreter.
Every warm hit on JUMP_BACKWARD calls tryWarmupTier2, which routes
to optimizer.Optimize. On success the install site flips to
ENTER_EXECUTOR and the next hit lands in enterExecutor. With no
uop dispatcher yet the executor arm deopts back to whatever Tier-1
opcode the install path stashed in Executor.VMData.
Port the 256-bit / K=6 dependency bloom (Python/optimizer.c
add_to_trace / dependency-tracking helpers), the per-code
Executors side table with has_space_for_executor /
get_index_for_executor / insert_executor, the AllocateExecutor /
ExecutorInit / Detach / Clear surface, and the two-phase
pending-deletion dance. Invalidation walks the side table per
Code and fans into ExecutorsInvalidateDependency.
translate_bytecode_to_trace runs branch-for-branch with CPython,
including the lookupMacro identity fallback and the
_START_EXECUTOR / _MAKE_WARM / _JUMP_TO_TOP prelude. The lattice
mirrors optimizer_symbols.c with SymMeet / SymJoin and the per-tag
switches kept verbatim so the gate fixtures stay aligned. dis.dis
gains an ENTER_EXECUTOR row matching upstream output.
Port the three-phase orchestrator from optimizer_analysis.c:
remove_unneeded_uops drops _SET_IP / _CHECK_VALIDITY rows where no
escape can reach since _START_EXECUTOR, collapses load-then-pop,
and resurrects _SET_IP ahead of escapes. removeGlobals folds
LOAD_GLOBAL_MODULE / _BUILTINS / _LOAD_ATTR_MODULE into inline
constants, rewrites the first _GUARD_GLOBALS_VERSION into
_CHECK_FUNCTION stamped with the function version, and registers
the dict against the globals watcher. globalsWatcherCallback /
typeWatcherCallback land on ExecutorsInvalidateDependency and
unsubscribe.
Two gate suites: optimizer_gate_test.go drives Optimize against a
synthetic warm code object and confirms the trace lands in the
side table, analysis_gate_test.go feeds shaped uop streams through
Analyze and asserts the rewrites match the per-pass expectations.
Also park the optimizer-specific golangci exemptions next to the
existing per-path lint carve-outs.
Parses pycore_uop_ids.h and pycore_uop_metadata.h, emits
optimizer/uop_ids_gen.go and optimizer/uop_meta_gen.go, and ships
a drift check so regenerated tables can not silently fall behind
upstream.
Master regex alternation lexer with longest-first operator
ordering, 1-based column tracking via linestart=-1 sentinel, all
C keywords plus the inst / op / macro / label / spilled DSL
keywords plus the annotation set (pure, replicate, tier1, tier2,
no_save_ip, ...). Stress tests against the real upstream
sources confirm exact token-count parity (31570 for bytecodes.c,
6438 for optimizer_bytecodes.c).
PLexer wraps Tokenize: cursor, peek / next / backup / expect /
require / consume_to. Comments are skipped by default; raw=true
exposes them. ConsumeTo tracks paren depth so nested arglists
behave.
Stateful AST builder over PLexer producing inst / op / macro /
family / pseudo / label declarations. Annotations include the
replicate(N) form, stack effects accept the 'name: type [*]' and
'name[size]' shapes, and macro RHS handles 'op + op + cache/N'
chains. Block parser handles if / for / while plus the
#if/#else/#endif macro form, and rejects switch with the same
message upstream uses. Real-source test parses
Python/bytecodes.c between BEGIN/END markers without errors.
Most of these are surface tidy-ups: AssignUniqueId / ReleaseUniqueId
take the ID suffix, the uniqueid resize never returned a real
error, the QSBRPad cache-line padding becomes a blank field, and
the cases_generator parameter names drop the trailing underscore
they wore in the Python source. The .golangci.yml carve-outs for
optimizer/analysis.go and tools/uops_gen/{lexer,parsing,plexer}.go
mark the files where parity with the upstream shape is the point.
@tamnd

tamnd commented May 8, 2026

Copy link
Copy Markdown
Owner Author

Pushed the lexer + plexer + parsing.py ports today. Token count now matches the Python reference exactly on both upstream sources (31570 for bytecodes.c, 6438 for optimizer_bytecodes.c), so we can rule out silent drift before we start parsing.

The recursive-descent shape stays the same as the Python: ifStmt / forStmt / whileStmt / macroIf, contextual stamping via setContext at the end of each successful production, ConsumeTo tracking paren depth so nested arglists do the right thing inside if/for headers. Real-source test parses bytecodes.c between the BEGIN/END markers without any errors, which is what I really wanted before moving on.

@tamnd

tamnd commented May 8, 2026

Copy link
Copy Markdown
Owner Author

A couple of bumps along the way worth flagging:

  1. lexer.py treats a leading . as the start of a number for the .5 form; my first cut only branched on 0..9. That fell out as a bad-token error on a real input. Fix was a one-character change in the kind switch but it took longer than I want to admit to spot.
  2. column tracking. I started with start - linestart - 1 and linestart = end - 1, which is the obvious zero-based formula. lexer.py is 1-based with linestart = -1 as the initial sentinel; matching that exactly was the only way to keep the parity tests honest.
  3. on .5 lexing, the Python regex matches any . followed by a digit before the . ever gets classified as TokPeriod, which is why the master regex needs to keep number ahead of operator. Got that right after the first try.

Token is a value type because the Python source treats it that way, even though gocritic flags every method that takes one. The exemptions in .golangci.yml carry the // parity with upstream shape reasoning so we don't litigate it again next time.

@tamnd

tamnd commented May 8, 2026

Copy link
Copy Markdown
Owner Author

Status check before I keep going:

  • lexer (15 commits): done, parity verified end-to-end
  • plexer: done, with parity tests for the small surface (peek, expect, require, ConsumeTo)
  • parsing.py: done. inst / op / macro / family / pseudo / label all parse, real-source test green
  • next up: analyzer.py, then stack.py, then the generator pipeline that emits optimizer_cases.c.h

The motivation for porting the generator stack instead of hand-translating the generated case bodies is the exact thing that bit me on COMPARE_OP last release: hand-rolled tables drift from upstream the moment someone touches a uop, and you find out at runtime. With the generator in our tree we re-run it whenever bytecodes.c changes and the parity check catches the drift at build time.

Lint is at zero on the whole tree, full go test ./... is green. Going to keep pushing.

tamnd added 2 commits May 8, 2026 11:17
The PushFrame and ReturnValue cases in analysis.go were casting a
uint64 operand back to *objects.Function via unsafe.Pointer(uintptr).
Go vet flags that as misuse and -race trips checkptr. Today trace.go
never writes a real function pointer there (PushFrame ships operand=0,
ReturnValue ships a code pointer with low-bit tag), so both paths just
early-return. The function-pointer projection lands once we route it
through a side table.

The globals watcher callback was casting an unsafe.Pointer back to
*objects.Dict to call IncrementMutations. Same checkptr issue when a
test fires the watcher with a non-Dict pointer for identity. Move the
IncrementMutations call to objects.DictMutationHook in WatcherInit
where we already have a typed *objects.Dict.
CWriter is the token-aware C emitter that backs the case-body
generators. It tracks indent levels, optionally emits #line directives,
and runs a small spill/reload state machine for the
_PyFrame_{Set,Get}StackPointer pairs the Tier-2 generators need.

Tests cover the spacing, brace indent, StartLine, spill/reload arming
and cancellation, header guards, and the splitlines / isLabel helpers.
The cwriter file joins the lexer/parsing/plexer carve-out for
gocritic/gocyclo/prealloc since Token is held by value to mirror
cwriter.py.
@tamnd

tamnd commented May 8, 2026

Copy link
Copy Markdown
Owner Author

CI is green again.

Two distinct things were red. The vet job was unhappy about unsafe.Pointer(uintptr(operand)) casts in optimizer/analysis.go (the PushFrame and ReturnValue arms of removeGlobals). And the test job was crashing under -race because the globals watcher callback was casting an unsafe.Pointer back to *objects.Dict, which trips checkptr the moment a test fires the watcher with a pointer that isn't a real Dict (the dispatch path uses identity, not type).

For analysis.go I dropped the round-trip entirely. trace.go currently writes operand=0 for PushFrame and a code-pointer with a low-bit tag for ReturnValue, so both arms were already early-returning before reaching the cast. The function-pointer projection that the upstream remove_globals consumes will land once we route function pointers through a side table rather than a uint64 operand. Until then I left a comment explaining why the case bails out.

For the watcher I moved the IncrementMutations call up into objects.DictMutationHook in WatcherInit, where we still have a typed *objects.Dict. The callback itself just invalidates and unsubscribes now, which is what CPython's globals_watcher_callback actually does.

Also slipped in cwriter.py while I was here. CWriter is the token-aware C emitter the case-body generators feed into, with the spill/reload state machine for the _PyFrame_{Set,Get}StackPointer pairs. Tests cover the spacing, indent, header guard, and the spill cancellation logic.

tamnd added 6 commits May 8, 2026 11:51
Translates the 1207-line semantic analysis pass into Go: the Properties
record (escapes / error_with_pop / uses_locals / has_free / side_exit /
oparg / etc.), Skip / Flush / StackItem / CacheEntry / Uop / Label /
Instruction / PseudoInstruction / AnalyzedFamily / Analysis records,
and the analyzeStack / analyzeCaches / findEscapingAPICalls /
checkEscapingCalls / alwaysExits / computeProperties / makeUop / addOp /
desugarInst / addMacro / addFamily / addPseudo / addLabel /
assignOpcodes / getInstructionSizeForUop walkers. AnalyzeForest is the
top-level entry the case-body emitters consume.

Tests cover synthetic property classification (DEOPT / ESCAPES / PURE /
ERROR_IF), peek / used stack marking, end-to-end analysis of
Python/bytecodes.c with sanity checks on LOAD_FAST / _CHECK_VALIDITY /
_BINARY_OP_ADD_INT / RESUME_CHECK, the Py_DECREF vs Py_INCREF escape
classification, and bad-stack rejection.

The Python dump_* debug printers are intentionally not ported; they
have no downstream consumer in the case-body emitters.
Translates the 737-line abstract-stack model the case-body emitter
walks during render: PointerOffset (signed delta over a fixed base),
Local (an SSA-ish slot bound to a StackItem with defined / in_memory /
value tracking), Stack (the per-effect grow / shrink helpers,
flush_locals, merge / pop / push), and Storage (the higher-level
wrapper that holds stack + locals + outputs + spilled flag, plus
flush / save / reload / close_inputs / assign_locals / push_outputs).

Tests cover PointerOffset arithmetic, OffsetFromItem, the four
canonical effect shapes (pure pop, pure push, peek+replace, growing
macro), spill / reload bracketing, and a parse -> analyze -> render
round-trip that asserts the preamble / postamble bracket the body
with the right stack_pointer adjustment.
Translates the 708-line shared rewrite layer the tier1, tier2, and
optimizer generators share. Brings over the TokenIterator cursor,
writeHeader / rootRelativePath helpers, the Emitter dispatch table
(Dispatch / DeoptIf / ExitIf / ErrorIf / ErrorNoPop / DecrefInputs /
Kill / KillInputs / SyncSp / StackPointer / SaveStack / ReloadStack /
StackrefCloseSpecialized / StackrefSteal / InstructionSize), the
_emit_stmt family (simple / macro_if / if / block / for / while), the
public EmitTokens entrypoint, and the CFlags formatter.

CodeSection is the analyzer-Uop-or-Label union the upstream uses;
gopy lifts it to a small interface with adapters. Storage / Stack /
Token are reused from analyzer.go and stack.go without redefinition.
ROOT becomes a per-call projectRoot parameter so the generator
drivers can pass it from a flag.

Tests cover TokenIterator next / peek, emitTo paren balancing,
alwaysTrue, typeAndNull, CFlags formatting, rootRelativePath,
writeHeader, plus per-replacer dispatch goldens (DECREF_INPUTS,
SYNC_SP, ERROR_IF conditional and unconditional, ERROR_NO_POP,
DEOPT_IF, DISPATCH, stack_pointer spilled-error, INSTRUCTION_SIZE)
and an end-to-end EmitTokens run.
Translates the 244-line abstract-interpreter case-table emitter into
Go. Reads the main bytecodes.c forest plus the optimizer_bytecodes.c
overlay, runs both through AnalyzeForest, and emits per-uop stub
handlers preceded by the rewritten C body inside a /* ... */ block.
Where optimizer_bytecodes.c overrides a uop, the symbolic body wins.

OptimizerEmitter overrides SAVE_STACK / RELOAD_STACK / goto_label;
the latter is wired through a new Emitter.GotoLabelHook in
generators_common.go so subclasses can swap the JUMP_TO_LABEL
default. Storage.Merge had a Go-port bug where the inputs zip
indexed past the shorter list when CheckLiveness was off; bounded
to the shorter list, matching upstream zip().

Tests cover: output parses as Go, representative symbolic stubs
exist (LOAD_FAST, GUARD_TOS_INT, GUARD_NOS_INT, BINARY_OP_ADD_INT,
LOAD_CONST_INLINE_BORROW, CHECK_VALIDITY), the LOAD_FAST override
from optimizer_bytecodes.c wins over the base DSL definition, and
sym_new_const / sym_set_type reach the expected uops.
Translates the 228-line Tier-2 case-table emitter into Go. Output
target is a Go source file (rather than executor_cases.c.h): each
viable Uop emits a /* uop NAME body: */ comment block carrying the
rewritten C body line by line, followed by a func uopNAME() int {
return 0 } stub. The hand-written Tier-2 interpreter loop in
optimizer/uops.go (#431) plugs into the stubs once it lands.

Tier2Emitter overrides DEOPT_IF, EXIT_IF, and oparg on top of the
shared Emitter from generators_common. Iteration is sorted by uop
name for deterministic output.

Tests parse Python/bytecodes.c, run AnalyzeForest, run GenerateTier2,
and assert: representative uops have stubs (LOAD_FAST, RESUME_CHECK,
CHECK_VALIDITY, BINARY_OP_ADD_INT, GUARD_TOS_INT, GUARD_NOS_INT,
EXIT_TRACE), the output parses as Go via go/parser.ParseFile, and
the EXIT_IF replacer emits the Tier-2 JUMP_TO_JUMP_TARGET shape.
Adds BuildUopIDsFromAnalysis and BuildUopMetaFromAnalysis: the
analyzer-driven path that mirrors uop_id_generator.py and
uop_metadata_generator.py upstream. Both walk analysis.Uops sorted
by uop.Name, hard-code _EXIT_TRACE / _SET_IP at IDs 300 / 301
(or 1 / 2 when distinctNamespace), filter Tier-1 uops, and emit
the alias form for implicitly-created non-replicated uops. The meta
path computes num_popped via the same Stack pop loop the upstream
uses against NullCWriter.

Cross-check: with 318 IDs and 289 meta rows from Python/bytecodes.c,
the analyzer-driven output matches pycore_uop_ids.h and
pycore_uop_metadata.h byte-for-byte (modulo trivial map-iteration
order, which the sort fixes).

The header-parser path stays in place as the drift checker. main.go
gains a --from-analysis flag that switches the driver to the new
path; the default behavior is unchanged.
On Windows the root path passed to rootRelativePath was being compared
against an absolute filename, which filepath.Rel rejects when the two
paths sit on different drive prefixes (the unmodified root has no
drive, the absoluted filename does). Resolve both paths to absolute
before the Rel call so the relative form computes regardless of
platform.

Fixes the Windows-only failure on TestRootRelativePath_RelativeOk and
TestWriteHeader.
@tamnd

tamnd commented May 8, 2026

Copy link
Copy Markdown
Owner Author

The cases_generator port is done.

Six commits since the last update, walking up the dependency chain.

analyzer.go (1207 lines upstream) lands first because everything else feeds off the Properties / Uop / Instruction records it produces. Tests run AnalyzeForest on the real Python/bytecodes.c and assert the per-uop properties for LOAD_FAST, BINARY_OP_ADD_INT, CHECK_VALIDITY, and RESUME_CHECK plus the escape classification of Py_DECREF vs Py_INCREF.

stack.go (737 lines) and the abstract stack model: PointerOffset, Local, Stack, Storage, plus the four canonical effect shapes for tests (pure pop, pure push, peek+replace, growing macro), spill/reload bracketing, and a parse-analyze-render round trip.

generators_common.go (708 lines) is the shared rewrite layer: TokenIterator, the Emitter dispatch table (DECREF_INPUTS, SYNC_SP, ERROR_IF, DEOPT_IF, EXIT_IF, DISPATCH, etc.), the per-stmt walker, the public EmitTokens entrypoint, and CFlags. Every replacer has a golden snippet test.

tier2_generator.go and optimizer_generator.go both emit Go source files. Each viable uop becomes a /* uop NAME body: */ comment block with the rewritten C body line-by-line, followed by a stub func that the hand-written Tier-2 loop will plug into. The optimizer generator overlays optimizer_bytecodes.c on top of the main forest; tests confirm the LOAD_FAST override wins over the base DSL.

Last piece: the analyzer-driven id and metadata builders. With 318 IDs and 289 meta rows, the Go output matches Include/internal/pycore_uop_ids.h and pycore_uop_metadata.h byte-for-byte. The header-parser path stays as the drift checker; the new --from-analysis flag is the canonical source going forward.

Caught one Windows-only path bug at the end: rootRelativePath was passing the unabsolutized projectRoot to filepath.Rel, which trips when the target path has a drive prefix and the root doesn't. Two-line fix.

CI green across linux, macos, windows.

tamnd added 4 commits May 8, 2026 14:22
Both modes parse the bytecodes-flavored DSL (slicing the upstream
// BEGIN BYTECODES // / // END BYTECODES // section so the lexer does
not trip on the C preprocessor preamble), run the analyzer, and feed
the resulting Analysis into GenerateTier2 / GenerateOptimizer. The
buffer is run through go/format before it lands on disk so the output
is gofmt-clean by construction. Smoke-tested against
cpython-314/Python/bytecodes.c (285 viable Tier-2 uops, 7989 lines
of generated stubs) and optimizer_bytecodes.c (3995 lines).
The tier2 generator now emits two files instead of one fat stubs file:
uops_dispatch_gen.go (executeUop fan-out switch) and uops_stubs_gen.go
(StatusDeopt stub per Tier-2-viable uop). Each stub carries the
Tier-2-rewritten C body as a // comment block above it so a porter
has the spec inline.

The generator auto-detects which uop method names *Tier2State already
defines elsewhere in optimizer/ (skipping *_gen.go and *_test.go to
avoid reading its own output) and omits stubs for that set. To port
a uop: write the method on *Tier2State in uops_impl.go, regenerate,
and the stub disappears while dispatch keeps routing to the same
method name. Future CPython uop additions land as new stubs without
touching hand-ported bodies.

Hand-ported starter set covers the trace prologue plus the
loadfast/storefast/poptop/pushnull/copy/swap quartet so a projected
trace can advance past _START_EXECUTOR / _MAKE_WARM /
_CHECK_VALIDITY / _SET_IP / _JUMP_TO_TOP / _EXIT_TRACE under real
dispatch. The driver loop in uops.go owns the for(;;) over the
trace, mirrors enter_tier_two's status branching, and pumps NextUop
through jumpToJumpTarget for the JUMP_TO_TOP edge.

Smoke tests in optimizer/uops_test.go drive the dispatch loop end
to end against the hand-ported set, and a generator round-trip test
verifies the ported-detection filter actually drops re-stubbed
methods. CLI gains -stubs-out and -ported-from for the cases mode.
Three checks pin the integration between the projector, the analysis
pass, and RunExecutor:

The hand-ported happy path drives a synthetic trace through every
method in uops_impl.go (start, check_validity, set_ip, nop,
load_fast, copy, swap, push_null, pop_top, store_fast,
load_fast_borrow, make_warm, exit_trace) and asserts the dispatch
loop stitched each method into the right slot. Local round-trip
plus the warm bit double as witnesses that side effects landed.

The stub-bail path runs a trace ending in _BINARY_OP (a stubbed uop
as of v0.12) and asserts a clean StatusDeopt without panic, so the
fallback story stays runtime-safe while the rest of the hand-port
catches up.

The end-to-end check feeds an Optimize-produced trace from the loop
fixture into RunExecutor and asserts dispatch terminates on either
StatusExit or StatusDeopt. Both are valid v0.12 outcomes; the gate
just refuses anything else, so a bug that drops dispatch into an
infinite loop or a status we never defined fails loudly.
The v0.12 changelog said the uop interpreter loop was forwarded to
v0.13, but the dispatcher and a 14-uop hand-ported starter set landed
in commits bc866e5 and 31dc040 of this same drop. Update the body to
match: the loop is shipped, the long tail of per-uop bodies is what
slots into v0.13.
@tamnd tamnd marked this pull request as ready for review May 8, 2026 08:30
@tamnd tamnd merged commit bc3369c into main May 8, 2026
6 checks passed
@tamnd tamnd deleted the feat/v0.12.0-optimizer branch May 15, 2026 11:02
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant