Various fixes to codegen and generator runtime - #23

Merged
gf712 merged 11 commits into
mainfrom
fix/generator-resume-stack-rebase
Jun 24, 2026
Merged

Various fixes to codegen and generator runtime#23
gf712 merged 11 commits into
mainfrom
fix/generator-resume-stack-rebase

Conversation

@gf712

Copy link
Copy Markdown
Owner

No description provided.

gf712and others added 11 commits June 23, 2026 20:12
…type
The Try-handler codegen stored the matched exception *type* into the bound
name (`store_name(handler->name(), handler->type()->codegen(), …)`), so
`except ValueError as e:` left `e` as the `ValueError` class instead of the
raised instance — `isinstance(e, ValueError)` was false, `type(e)` was
`type`, and any use of `e`'s value (e.g. `str(e)`) was wrong. (This is the
real bug behind a "miscompile" that looked layout-fragile only because of a
separate wrong-traceback-line issue.)
Add a `py.load_exception` op (lowered to a new LOAD_EXCEPTION bytecode
instruction, opcode 84) that reads the active exception instance from
`execution_frame()->exception_info()->exception`, modeled on
LoadAssertionError across the pipeline (Python + EmitPythonBytecode dialect
ops, ControlFlow lowering, bytecode emitter, instruction decoder). Bind the
name to that instance at the start of the matched handler block instead of
to the type in the cond block.
Each builtin exception is a distinct C++ class with its own create(), so
each must define __new__ to allocate its own type; the inherited
Exception::__new__ both hardcodes the Exception class and asserts
`type == types::exception()`. RuntimeError and NameError were missing
__new__, so `raise RuntimeError(...)` / `raise NameError(...)` aborted at
that assert. ModuleNotFoundError::__new__ dereferenced `kwargs->map()`
unconditionally, so `raise ModuleNotFoundError("m")` (no kwargs)
segfaulted.
- Add RuntimeError::__new__ and NameError::__new__ (mirroring ValueError).
- Guard the null kwargs in ModuleNotFoundError::__new__.
- BaseException.args now returns an empty tuple (not None) when the
exception was constructed without args, matching CPython.
Regression test integration/tests/exception_types.py raises every builtin
exception and checks isinstance/type/args. Full ctest (194) + integration
suite green.
(Note: the test calls its per-type check via a helper rather than inline in
the loop, to avoid the separate FOR_ITER iterator-register clobber bug that
a heavy loop body triggers — see integration/minimal_foriter_bug.py.)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XZ4THY2jXpZnNoxVTzDn32
A try body lowers to blocks whose only explicit CFG successors are the
normal-flow ones (e.g. RAISE_VARARGS -> exit); the edge from a faulting op
in the try body to the handler is implicit (established by SETUP_EXC_HANDLE
in the *predecessor*). mlir::Liveness therefore never sees body-op ->
handler, so a value that is live at the handler — or live after it — was
considered dead inside the try body, and the register allocator happily
reused its register there.
When an exception actually unwound, that register had been overwritten.
The clearest case: a `for` loop whose body contains `try/except`. The
FOR_ITER iterator is read again after the handler (next iteration), but its
register was reused for a value inside the try body, so the resumed loop
read a clobbered iterator (abort in ForIter / "object is not an iterator").
The same bug corrupted exception args across sequential try/except blocks.
Fix: in LiveAnalysis, for every block in a try body (those dominated by the
SETUP_EXC_HANDLE / SETUP_WITH try-entry successor), add the handler's
live-in set to the block's live-out before computing per-op liveness. Any
op in the try body can transfer to the handler, so values live at the
handler are live across the whole try body. Dominance keeps it bounded and
handles nesting; the pass is skipped entirely when a function has no
handlers.
This makes the iterator (and any value live past the handler) interfere
with try-body temporaries, so they no longer share a register.
…_context__
`raise X from Y` already stored the cause on the exception
(RaiseVarargs::execute), but BaseException exposed none of the chaining
attributes, so __cause__ was unreadable.
- Add context()/set_context() and suppress_context()/set_suppress_context()
accessors on BaseException (m_context/m_cause/m_suppress_context already
existed and are GC-visited).
- Register __cause__, __context__ and __suppress_context__ as read/write
properties. Setting __cause__ also sets __suppress_context__ = True, and
__suppress_context__ coerces via truthiness, matching the data model.
- `raise X from Y` now also sets __suppress_context__ = True (and `from None`
keeps the suppression with a None cause).
Implicit __context__ chaining (auto-setting the new exception's context to the
one being handled) is intentionally left out: the frame exception stack is not
reliably popped — internally-consumed StopIterations linger (the same
pre-existing bug that makes a bare `raise` outside a handler re-raise a stale
exception instead of erroring), so reading it would attach a spurious context.
…ext__
The frame exception stack is shared across a call chain, and the eval loop
pushed every raised exception onto it (so handlers can read it) but only
popped on handler completion. Exceptions that propagated out of a frame
uncaught — including a generator/genexpr's completion StopIteration consumed
by the FOR_ITER that resumed it — were pushed and never popped, so they
accumulated. A later bare `raise` re-raised that stale StopIteration instead
of erroring, and implicit __context__ would pick it up.
Balance the lifecycle: when an exception propagates out of a frame that has
no handler, the eval loop now pops the entry it just pushed (the caller's
eval loop re-pushes it). With the stack kept clean:
- ReRaise returns RuntimeError("No active exception to reraise") on an empty
stack instead of asserting (bare `raise` outside a handler).
- The top-level driver uses the propagated result value rather than popping
the exception off the (now-empty) stack.
- from_iterable's manual StopIteration cleanup is removed — it is handled by
the eval loop now, and keeping it would wrongly pop a pre-existing
exception when iterating during exception handling.
- RaiseVarargs re-enables implicit __context__ chaining (set the new
exception's context to the one being handled); now reliable since the
stack no longer holds stale state.
exception_chaining.py now covers implicit __context__, bare `raise` ->
RuntimeError, and iterating generators/comprehensions while handling an
exception.
@gf712
gf712 merged commit af25589 into mainJun 24, 2026
1 check passed
@gf712
gf712 deleted the fix/generator-resume-stack-rebase branch June 24, 2026 08:40
Sign up for freeto 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

@gf712
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content

Various fixes to codegen and generator runtime - #23

Merged
gf712 merged 11 commits into
mainfrom
fix/generator-resume-stack-rebase
Jun 24, 2026
Merged

Various fixes to codegen and generator runtime#23
gf712 merged 11 commits into
mainfrom
fix/generator-resume-stack-rebase

Conversation

@gf712

Copy link
Copy Markdown
Owner

No description provided.

gf712and others added 11 commits June 23, 2026 20:12
…type
The Try-handler codegen stored the matched exception *type* into the bound
name (`store_name(handler->name(), handler->type()->codegen(), …)`), so
`except ValueError as e:` left `e` as the `ValueError` class instead of the
raised instance — `isinstance(e, ValueError)` was false, `type(e)` was
`type`, and any use of `e`'s value (e.g. `str(e)`) was wrong. (This is the
real bug behind a "miscompile" that looked layout-fragile only because of a
separate wrong-traceback-line issue.)
Add a `py.load_exception` op (lowered to a new LOAD_EXCEPTION bytecode
instruction, opcode 84) that reads the active exception instance from
`execution_frame()->exception_info()->exception`, modeled on
LoadAssertionError across the pipeline (Python + EmitPythonBytecode dialect
ops, ControlFlow lowering, bytecode emitter, instruction decoder). Bind the
name to that instance at the start of the matched handler block instead of
to the type in the cond block.
Each builtin exception is a distinct C++ class with its own create(), so
each must define __new__ to allocate its own type; the inherited
Exception::__new__ both hardcodes the Exception class and asserts
`type == types::exception()`. RuntimeError and NameError were missing
__new__, so `raise RuntimeError(...)` / `raise NameError(...)` aborted at
that assert. ModuleNotFoundError::__new__ dereferenced `kwargs->map()`
unconditionally, so `raise ModuleNotFoundError("m")` (no kwargs)
segfaulted.
- Add RuntimeError::__new__ and NameError::__new__ (mirroring ValueError).
- Guard the null kwargs in ModuleNotFoundError::__new__.
- BaseException.args now returns an empty tuple (not None) when the
exception was constructed without args, matching CPython.
Regression test integration/tests/exception_types.py raises every builtin
exception and checks isinstance/type/args. Full ctest (194) + integration
suite green.
(Note: the test calls its per-type check via a helper rather than inline in
the loop, to avoid the separate FOR_ITER iterator-register clobber bug that
a heavy loop body triggers — see integration/minimal_foriter_bug.py.)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XZ4THY2jXpZnNoxVTzDn32
A try body lowers to blocks whose only explicit CFG successors are the
normal-flow ones (e.g. RAISE_VARARGS -> exit); the edge from a faulting op
in the try body to the handler is implicit (established by SETUP_EXC_HANDLE
in the *predecessor*). mlir::Liveness therefore never sees body-op ->
handler, so a value that is live at the handler — or live after it — was
considered dead inside the try body, and the register allocator happily
reused its register there.
When an exception actually unwound, that register had been overwritten.
The clearest case: a `for` loop whose body contains `try/except`. The
FOR_ITER iterator is read again after the handler (next iteration), but its
register was reused for a value inside the try body, so the resumed loop
read a clobbered iterator (abort in ForIter / "object is not an iterator").
The same bug corrupted exception args across sequential try/except blocks.
Fix: in LiveAnalysis, for every block in a try body (those dominated by the
SETUP_EXC_HANDLE / SETUP_WITH try-entry successor), add the handler's
live-in set to the block's live-out before computing per-op liveness. Any
op in the try body can transfer to the handler, so values live at the
handler are live across the whole try body. Dominance keeps it bounded and
handles nesting; the pass is skipped entirely when a function has no
handlers.
This makes the iterator (and any value live past the handler) interfere
with try-body temporaries, so they no longer share a register.
…_context__
`raise X from Y` already stored the cause on the exception
(RaiseVarargs::execute), but BaseException exposed none of the chaining
attributes, so __cause__ was unreadable.
- Add context()/set_context() and suppress_context()/set_suppress_context()
accessors on BaseException (m_context/m_cause/m_suppress_context already
existed and are GC-visited).
- Register __cause__, __context__ and __suppress_context__ as read/write
properties. Setting __cause__ also sets __suppress_context__ = True, and
__suppress_context__ coerces via truthiness, matching the data model.
- `raise X from Y` now also sets __suppress_context__ = True (and `from None`
keeps the suppression with a None cause).
Implicit __context__ chaining (auto-setting the new exception's context to the
one being handled) is intentionally left out: the frame exception stack is not
reliably popped — internally-consumed StopIterations linger (the same
pre-existing bug that makes a bare `raise` outside a handler re-raise a stale
exception instead of erroring), so reading it would attach a spurious context.
…ext__
The frame exception stack is shared across a call chain, and the eval loop
pushed every raised exception onto it (so handlers can read it) but only
popped on handler completion. Exceptions that propagated out of a frame
uncaught — including a generator/genexpr's completion StopIteration consumed
by the FOR_ITER that resumed it — were pushed and never popped, so they
accumulated. A later bare `raise` re-raised that stale StopIteration instead
of erroring, and implicit __context__ would pick it up.
Balance the lifecycle: when an exception propagates out of a frame that has
no handler, the eval loop now pops the entry it just pushed (the caller's
eval loop re-pushes it). With the stack kept clean:
- ReRaise returns RuntimeError("No active exception to reraise") on an empty
stack instead of asserting (bare `raise` outside a handler).
- The top-level driver uses the propagated result value rather than popping
the exception off the (now-empty) stack.
- from_iterable's manual StopIteration cleanup is removed — it is handled by
the eval loop now, and keeping it would wrongly pop a pre-existing
exception when iterating during exception handling.
- RaiseVarargs re-enables implicit __context__ chaining (set the new
exception's context to the one being handled); now reliable since the
stack no longer holds stale state.
exception_chaining.py now covers implicit __context__, bare `raise` ->
RuntimeError, and iterating generators/comprehensions while handling an
exception.
@gf712
gf712 merged commit af25589 into mainJun 24, 2026
1 check passed
@gf712
gf712 deleted the fix/generator-resume-stack-rebase branch June 24, 2026 08:40
Sign up for freeto 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

@gf712
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Various fixes to codegen and generator runtime - #23

Merged
gf712 merged 11 commits into
mainfrom
fix/generator-resume-stack-rebase
Jun 24, 2026
Merged

Various fixes to codegen and generator runtime#23
gf712 merged 11 commits into
mainfrom
fix/generator-resume-stack-rebase

Conversation

@gf712

Copy link
Copy Markdown
Owner

No description provided.

gf712and others added 11 commits June 23, 2026 20:12
…type
The Try-handler codegen stored the matched exception *type* into the bound
name (`store_name(handler->name(), handler->type()->codegen(), …)`), so
`except ValueError as e:` left `e` as the `ValueError` class instead of the
raised instance — `isinstance(e, ValueError)` was false, `type(e)` was
`type`, and any use of `e`'s value (e.g. `str(e)`) was wrong. (This is the
real bug behind a "miscompile" that looked layout-fragile only because of a
separate wrong-traceback-line issue.)
Add a `py.load_exception` op (lowered to a new LOAD_EXCEPTION bytecode
instruction, opcode 84) that reads the active exception instance from
`execution_frame()->exception_info()->exception`, modeled on
LoadAssertionError across the pipeline (Python + EmitPythonBytecode dialect
ops, ControlFlow lowering, bytecode emitter, instruction decoder). Bind the
name to that instance at the start of the matched handler block instead of
to the type in the cond block.
Each builtin exception is a distinct C++ class with its own create(), so
each must define __new__ to allocate its own type; the inherited
Exception::__new__ both hardcodes the Exception class and asserts
`type == types::exception()`. RuntimeError and NameError were missing
__new__, so `raise RuntimeError(...)` / `raise NameError(...)` aborted at
that assert. ModuleNotFoundError::__new__ dereferenced `kwargs->map()`
unconditionally, so `raise ModuleNotFoundError("m")` (no kwargs)
segfaulted.
- Add RuntimeError::__new__ and NameError::__new__ (mirroring ValueError).
- Guard the null kwargs in ModuleNotFoundError::__new__.
- BaseException.args now returns an empty tuple (not None) when the
exception was constructed without args, matching CPython.
Regression test integration/tests/exception_types.py raises every builtin
exception and checks isinstance/type/args. Full ctest (194) + integration
suite green.
(Note: the test calls its per-type check via a helper rather than inline in
the loop, to avoid the separate FOR_ITER iterator-register clobber bug that
a heavy loop body triggers — see integration/minimal_foriter_bug.py.)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XZ4THY2jXpZnNoxVTzDn32
A try body lowers to blocks whose only explicit CFG successors are the
normal-flow ones (e.g. RAISE_VARARGS -> exit); the edge from a faulting op
in the try body to the handler is implicit (established by SETUP_EXC_HANDLE
in the *predecessor*). mlir::Liveness therefore never sees body-op ->
handler, so a value that is live at the handler — or live after it — was
considered dead inside the try body, and the register allocator happily
reused its register there.
When an exception actually unwound, that register had been overwritten.
The clearest case: a `for` loop whose body contains `try/except`. The
FOR_ITER iterator is read again after the handler (next iteration), but its
register was reused for a value inside the try body, so the resumed loop
read a clobbered iterator (abort in ForIter / "object is not an iterator").
The same bug corrupted exception args across sequential try/except blocks.
Fix: in LiveAnalysis, for every block in a try body (those dominated by the
SETUP_EXC_HANDLE / SETUP_WITH try-entry successor), add the handler's
live-in set to the block's live-out before computing per-op liveness. Any
op in the try body can transfer to the handler, so values live at the
handler are live across the whole try body. Dominance keeps it bounded and
handles nesting; the pass is skipped entirely when a function has no
handlers.
This makes the iterator (and any value live past the handler) interfere
with try-body temporaries, so they no longer share a register.
…_context__
`raise X from Y` already stored the cause on the exception
(RaiseVarargs::execute), but BaseException exposed none of the chaining
attributes, so __cause__ was unreadable.
- Add context()/set_context() and suppress_context()/set_suppress_context()
accessors on BaseException (m_context/m_cause/m_suppress_context already
existed and are GC-visited).
- Register __cause__, __context__ and __suppress_context__ as read/write
properties. Setting __cause__ also sets __suppress_context__ = True, and
__suppress_context__ coerces via truthiness, matching the data model.
- `raise X from Y` now also sets __suppress_context__ = True (and `from None`
keeps the suppression with a None cause).
Implicit __context__ chaining (auto-setting the new exception's context to the
one being handled) is intentionally left out: the frame exception stack is not
reliably popped — internally-consumed StopIterations linger (the same
pre-existing bug that makes a bare `raise` outside a handler re-raise a stale
exception instead of erroring), so reading it would attach a spurious context.
…ext__
The frame exception stack is shared across a call chain, and the eval loop
pushed every raised exception onto it (so handlers can read it) but only
popped on handler completion. Exceptions that propagated out of a frame
uncaught — including a generator/genexpr's completion StopIteration consumed
by the FOR_ITER that resumed it — were pushed and never popped, so they
accumulated. A later bare `raise` re-raised that stale StopIteration instead
of erroring, and implicit __context__ would pick it up.
Balance the lifecycle: when an exception propagates out of a frame that has
no handler, the eval loop now pops the entry it just pushed (the caller's
eval loop re-pushes it). With the stack kept clean:
- ReRaise returns RuntimeError("No active exception to reraise") on an empty
stack instead of asserting (bare `raise` outside a handler).
- The top-level driver uses the propagated result value rather than popping
the exception off the (now-empty) stack.
- from_iterable's manual StopIteration cleanup is removed — it is handled by
the eval loop now, and keeping it would wrongly pop a pre-existing
exception when iterating during exception handling.
- RaiseVarargs re-enables implicit __context__ chaining (set the new
exception's context to the one being handled); now reliable since the
stack no longer holds stale state.
exception_chaining.py now covers implicit __context__, bare `raise` ->
RuntimeError, and iterating generators/comprehensions while handling an
exception.
@gf712
gf712 merged commit af25589 into mainJun 24, 2026
1 check passed
@gf712
gf712 deleted the fix/generator-resume-stack-rebase branch June 24, 2026 08:40
Sign up for freeto 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

@gf712
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Various fixes to codegen and generator runtime - #23

Merged
gf712 merged 11 commits into
mainfrom
fix/generator-resume-stack-rebase
Jun 24, 2026
Merged

Various fixes to codegen and generator runtime#23
gf712 merged 11 commits into
mainfrom
fix/generator-resume-stack-rebase

Conversation

@gf712

Copy link
Copy Markdown
Owner

No description provided.

gf712and others added 11 commits June 23, 2026 20:12
…type
The Try-handler codegen stored the matched exception *type* into the bound
name (`store_name(handler->name(), handler->type()->codegen(), …)`), so
`except ValueError as e:` left `e` as the `ValueError` class instead of the
raised instance — `isinstance(e, ValueError)` was false, `type(e)` was
`type`, and any use of `e`'s value (e.g. `str(e)`) was wrong. (This is the
real bug behind a "miscompile" that looked layout-fragile only because of a
separate wrong-traceback-line issue.)
Add a `py.load_exception` op (lowered to a new LOAD_EXCEPTION bytecode
instruction, opcode 84) that reads the active exception instance from
`execution_frame()->exception_info()->exception`, modeled on
LoadAssertionError across the pipeline (Python + EmitPythonBytecode dialect
ops, ControlFlow lowering, bytecode emitter, instruction decoder). Bind the
name to that instance at the start of the matched handler block instead of
to the type in the cond block.
Each builtin exception is a distinct C++ class with its own create(), so
each must define __new__ to allocate its own type; the inherited
Exception::__new__ both hardcodes the Exception class and asserts
`type == types::exception()`. RuntimeError and NameError were missing
__new__, so `raise RuntimeError(...)` / `raise NameError(...)` aborted at
that assert. ModuleNotFoundError::__new__ dereferenced `kwargs->map()`
unconditionally, so `raise ModuleNotFoundError("m")` (no kwargs)
segfaulted.
- Add RuntimeError::__new__ and NameError::__new__ (mirroring ValueError).
- Guard the null kwargs in ModuleNotFoundError::__new__.
- BaseException.args now returns an empty tuple (not None) when the
exception was constructed without args, matching CPython.
Regression test integration/tests/exception_types.py raises every builtin
exception and checks isinstance/type/args. Full ctest (194) + integration
suite green.
(Note: the test calls its per-type check via a helper rather than inline in
the loop, to avoid the separate FOR_ITER iterator-register clobber bug that
a heavy loop body triggers — see integration/minimal_foriter_bug.py.)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XZ4THY2jXpZnNoxVTzDn32
A try body lowers to blocks whose only explicit CFG successors are the
normal-flow ones (e.g. RAISE_VARARGS -> exit); the edge from a faulting op
in the try body to the handler is implicit (established by SETUP_EXC_HANDLE
in the *predecessor*). mlir::Liveness therefore never sees body-op ->
handler, so a value that is live at the handler — or live after it — was
considered dead inside the try body, and the register allocator happily
reused its register there.
When an exception actually unwound, that register had been overwritten.
The clearest case: a `for` loop whose body contains `try/except`. The
FOR_ITER iterator is read again after the handler (next iteration), but its
register was reused for a value inside the try body, so the resumed loop
read a clobbered iterator (abort in ForIter / "object is not an iterator").
The same bug corrupted exception args across sequential try/except blocks.
Fix: in LiveAnalysis, for every block in a try body (those dominated by the
SETUP_EXC_HANDLE / SETUP_WITH try-entry successor), add the handler's
live-in set to the block's live-out before computing per-op liveness. Any
op in the try body can transfer to the handler, so values live at the
handler are live across the whole try body. Dominance keeps it bounded and
handles nesting; the pass is skipped entirely when a function has no
handlers.
This makes the iterator (and any value live past the handler) interfere
with try-body temporaries, so they no longer share a register.
…_context__
`raise X from Y` already stored the cause on the exception
(RaiseVarargs::execute), but BaseException exposed none of the chaining
attributes, so __cause__ was unreadable.
- Add context()/set_context() and suppress_context()/set_suppress_context()
accessors on BaseException (m_context/m_cause/m_suppress_context already
existed and are GC-visited).
- Register __cause__, __context__ and __suppress_context__ as read/write
properties. Setting __cause__ also sets __suppress_context__ = True, and
__suppress_context__ coerces via truthiness, matching the data model.
- `raise X from Y` now also sets __suppress_context__ = True (and `from None`
keeps the suppression with a None cause).
Implicit __context__ chaining (auto-setting the new exception's context to the
one being handled) is intentionally left out: the frame exception stack is not
reliably popped — internally-consumed StopIterations linger (the same
pre-existing bug that makes a bare `raise` outside a handler re-raise a stale
exception instead of erroring), so reading it would attach a spurious context.
…ext__
The frame exception stack is shared across a call chain, and the eval loop
pushed every raised exception onto it (so handlers can read it) but only
popped on handler completion. Exceptions that propagated out of a frame
uncaught — including a generator/genexpr's completion StopIteration consumed
by the FOR_ITER that resumed it — were pushed and never popped, so they
accumulated. A later bare `raise` re-raised that stale StopIteration instead
of erroring, and implicit __context__ would pick it up.
Balance the lifecycle: when an exception propagates out of a frame that has
no handler, the eval loop now pops the entry it just pushed (the caller's
eval loop re-pushes it). With the stack kept clean:
- ReRaise returns RuntimeError("No active exception to reraise") on an empty
stack instead of asserting (bare `raise` outside a handler).
- The top-level driver uses the propagated result value rather than popping
the exception off the (now-empty) stack.
- from_iterable's manual StopIteration cleanup is removed — it is handled by
the eval loop now, and keeping it would wrongly pop a pre-existing
exception when iterating during exception handling.
- RaiseVarargs re-enables implicit __context__ chaining (set the new
exception's context to the one being handled); now reliable since the
stack no longer holds stale state.
exception_chaining.py now covers implicit __context__, bare `raise` ->
RuntimeError, and iterating generators/comprehensions while handling an
exception.
@gf712
gf712 merged commit af25589 into mainJun 24, 2026
1 check passed
@gf712
gf712 deleted the fix/generator-resume-stack-rebase branch June 24, 2026 08:40
Sign up for freeto 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

@gf712
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

Various fixes to codegen and generator runtime - #23

Merged
gf712 merged 11 commits into
mainfrom
fix/generator-resume-stack-rebase
Jun 24, 2026
Merged

Various fixes to codegen and generator runtime#23
gf712 merged 11 commits into
mainfrom
fix/generator-resume-stack-rebase

Conversation

@gf712

Copy link
Copy Markdown
Owner

No description provided.

gf712and others added 11 commits June 23, 2026 20:12
…type
The Try-handler codegen stored the matched exception *type* into the bound
name (`store_name(handler->name(), handler->type()->codegen(), …)`), so
`except ValueError as e:` left `e` as the `ValueError` class instead of the
raised instance — `isinstance(e, ValueError)` was false, `type(e)` was
`type`, and any use of `e`'s value (e.g. `str(e)`) was wrong. (This is the
real bug behind a "miscompile" that looked layout-fragile only because of a
separate wrong-traceback-line issue.)
Add a `py.load_exception` op (lowered to a new LOAD_EXCEPTION bytecode
instruction, opcode 84) that reads the active exception instance from
`execution_frame()->exception_info()->exception`, modeled on
LoadAssertionError across the pipeline (Python + EmitPythonBytecode dialect
ops, ControlFlow lowering, bytecode emitter, instruction decoder). Bind the
name to that instance at the start of the matched handler block instead of
to the type in the cond block.
Each builtin exception is a distinct C++ class with its own create(), so
each must define __new__ to allocate its own type; the inherited
Exception::__new__ both hardcodes the Exception class and asserts
`type == types::exception()`. RuntimeError and NameError were missing
__new__, so `raise RuntimeError(...)` / `raise NameError(...)` aborted at
that assert. ModuleNotFoundError::__new__ dereferenced `kwargs->map()`
unconditionally, so `raise ModuleNotFoundError("m")` (no kwargs)
segfaulted.
- Add RuntimeError::__new__ and NameError::__new__ (mirroring ValueError).
- Guard the null kwargs in ModuleNotFoundError::__new__.
- BaseException.args now returns an empty tuple (not None) when the
exception was constructed without args, matching CPython.
Regression test integration/tests/exception_types.py raises every builtin
exception and checks isinstance/type/args. Full ctest (194) + integration
suite green.
(Note: the test calls its per-type check via a helper rather than inline in
the loop, to avoid the separate FOR_ITER iterator-register clobber bug that
a heavy loop body triggers — see integration/minimal_foriter_bug.py.)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XZ4THY2jXpZnNoxVTzDn32
A try body lowers to blocks whose only explicit CFG successors are the
normal-flow ones (e.g. RAISE_VARARGS -> exit); the edge from a faulting op
in the try body to the handler is implicit (established by SETUP_EXC_HANDLE
in the *predecessor*). mlir::Liveness therefore never sees body-op ->
handler, so a value that is live at the handler — or live after it — was
considered dead inside the try body, and the register allocator happily
reused its register there.
When an exception actually unwound, that register had been overwritten.
The clearest case: a `for` loop whose body contains `try/except`. The
FOR_ITER iterator is read again after the handler (next iteration), but its
register was reused for a value inside the try body, so the resumed loop
read a clobbered iterator (abort in ForIter / "object is not an iterator").
The same bug corrupted exception args across sequential try/except blocks.
Fix: in LiveAnalysis, for every block in a try body (those dominated by the
SETUP_EXC_HANDLE / SETUP_WITH try-entry successor), add the handler's
live-in set to the block's live-out before computing per-op liveness. Any
op in the try body can transfer to the handler, so values live at the
handler are live across the whole try body. Dominance keeps it bounded and
handles nesting; the pass is skipped entirely when a function has no
handlers.
This makes the iterator (and any value live past the handler) interfere
with try-body temporaries, so they no longer share a register.
…_context__
`raise X from Y` already stored the cause on the exception
(RaiseVarargs::execute), but BaseException exposed none of the chaining
attributes, so __cause__ was unreadable.
- Add context()/set_context() and suppress_context()/set_suppress_context()
accessors on BaseException (m_context/m_cause/m_suppress_context already
existed and are GC-visited).
- Register __cause__, __context__ and __suppress_context__ as read/write
properties. Setting __cause__ also sets __suppress_context__ = True, and
__suppress_context__ coerces via truthiness, matching the data model.
- `raise X from Y` now also sets __suppress_context__ = True (and `from None`
keeps the suppression with a None cause).
Implicit __context__ chaining (auto-setting the new exception's context to the
one being handled) is intentionally left out: the frame exception stack is not
reliably popped — internally-consumed StopIterations linger (the same
pre-existing bug that makes a bare `raise` outside a handler re-raise a stale
exception instead of erroring), so reading it would attach a spurious context.
…ext__
The frame exception stack is shared across a call chain, and the eval loop
pushed every raised exception onto it (so handlers can read it) but only
popped on handler completion. Exceptions that propagated out of a frame
uncaught — including a generator/genexpr's completion StopIteration consumed
by the FOR_ITER that resumed it — were pushed and never popped, so they
accumulated. A later bare `raise` re-raised that stale StopIteration instead
of erroring, and implicit __context__ would pick it up.
Balance the lifecycle: when an exception propagates out of a frame that has
no handler, the eval loop now pops the entry it just pushed (the caller's
eval loop re-pushes it). With the stack kept clean:
- ReRaise returns RuntimeError("No active exception to reraise") on an empty
stack instead of asserting (bare `raise` outside a handler).
- The top-level driver uses the propagated result value rather than popping
the exception off the (now-empty) stack.
- from_iterable's manual StopIteration cleanup is removed — it is handled by
the eval loop now, and keeping it would wrongly pop a pre-existing
exception when iterating during exception handling.
- RaiseVarargs re-enables implicit __context__ chaining (set the new
exception's context to the one being handled); now reliable since the
stack no longer holds stale state.
exception_chaining.py now covers implicit __context__, bare `raise` ->
RuntimeError, and iterating generators/comprehensions while handling an
exception.
@gf712
gf712 merged commit af25589 into mainJun 24, 2026
1 check passed
@gf712
gf712 deleted the fix/generator-resume-stack-rebase branch June 24, 2026 08:40
Sign up for freeto 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

@gf712
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Various fixes to codegen and generator runtime - #23

Merged
gf712 merged 11 commits into
mainfrom
fix/generator-resume-stack-rebase
Jun 24, 2026
Merged

Various fixes to codegen and generator runtime#23
gf712 merged 11 commits into
mainfrom
fix/generator-resume-stack-rebase

Conversation

@gf712

Copy link
Copy Markdown
Owner

No description provided.

gf712and others added 11 commits June 23, 2026 20:12
…type
The Try-handler codegen stored the matched exception *type* into the bound
name (`store_name(handler->name(), handler->type()->codegen(), …)`), so
`except ValueError as e:` left `e` as the `ValueError` class instead of the
raised instance — `isinstance(e, ValueError)` was false, `type(e)` was
`type`, and any use of `e`'s value (e.g. `str(e)`) was wrong. (This is the
real bug behind a "miscompile" that looked layout-fragile only because of a
separate wrong-traceback-line issue.)
Add a `py.load_exception` op (lowered to a new LOAD_EXCEPTION bytecode
instruction, opcode 84) that reads the active exception instance from
`execution_frame()->exception_info()->exception`, modeled on
LoadAssertionError across the pipeline (Python + EmitPythonBytecode dialect
ops, ControlFlow lowering, bytecode emitter, instruction decoder). Bind the
name to that instance at the start of the matched handler block instead of
to the type in the cond block.
Each builtin exception is a distinct C++ class with its own create(), so
each must define __new__ to allocate its own type; the inherited
Exception::__new__ both hardcodes the Exception class and asserts
`type == types::exception()`. RuntimeError and NameError were missing
__new__, so `raise RuntimeError(...)` / `raise NameError(...)` aborted at
that assert. ModuleNotFoundError::__new__ dereferenced `kwargs->map()`
unconditionally, so `raise ModuleNotFoundError("m")` (no kwargs)
segfaulted.
- Add RuntimeError::__new__ and NameError::__new__ (mirroring ValueError).
- Guard the null kwargs in ModuleNotFoundError::__new__.
- BaseException.args now returns an empty tuple (not None) when the
exception was constructed without args, matching CPython.
Regression test integration/tests/exception_types.py raises every builtin
exception and checks isinstance/type/args. Full ctest (194) + integration
suite green.
(Note: the test calls its per-type check via a helper rather than inline in
the loop, to avoid the separate FOR_ITER iterator-register clobber bug that
a heavy loop body triggers — see integration/minimal_foriter_bug.py.)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XZ4THY2jXpZnNoxVTzDn32
A try body lowers to blocks whose only explicit CFG successors are the
normal-flow ones (e.g. RAISE_VARARGS -> exit); the edge from a faulting op
in the try body to the handler is implicit (established by SETUP_EXC_HANDLE
in the *predecessor*). mlir::Liveness therefore never sees body-op ->
handler, so a value that is live at the handler — or live after it — was
considered dead inside the try body, and the register allocator happily
reused its register there.
When an exception actually unwound, that register had been overwritten.
The clearest case: a `for` loop whose body contains `try/except`. The
FOR_ITER iterator is read again after the handler (next iteration), but its
register was reused for a value inside the try body, so the resumed loop
read a clobbered iterator (abort in ForIter / "object is not an iterator").
The same bug corrupted exception args across sequential try/except blocks.
Fix: in LiveAnalysis, for every block in a try body (those dominated by the
SETUP_EXC_HANDLE / SETUP_WITH try-entry successor), add the handler's
live-in set to the block's live-out before computing per-op liveness. Any
op in the try body can transfer to the handler, so values live at the
handler are live across the whole try body. Dominance keeps it bounded and
handles nesting; the pass is skipped entirely when a function has no
handlers.
This makes the iterator (and any value live past the handler) interfere
with try-body temporaries, so they no longer share a register.
…_context__
`raise X from Y` already stored the cause on the exception
(RaiseVarargs::execute), but BaseException exposed none of the chaining
attributes, so __cause__ was unreadable.
- Add context()/set_context() and suppress_context()/set_suppress_context()
accessors on BaseException (m_context/m_cause/m_suppress_context already
existed and are GC-visited).
- Register __cause__, __context__ and __suppress_context__ as read/write
properties. Setting __cause__ also sets __suppress_context__ = True, and
__suppress_context__ coerces via truthiness, matching the data model.
- `raise X from Y` now also sets __suppress_context__ = True (and `from None`
keeps the suppression with a None cause).
Implicit __context__ chaining (auto-setting the new exception's context to the
one being handled) is intentionally left out: the frame exception stack is not
reliably popped — internally-consumed StopIterations linger (the same
pre-existing bug that makes a bare `raise` outside a handler re-raise a stale
exception instead of erroring), so reading it would attach a spurious context.
…ext__
The frame exception stack is shared across a call chain, and the eval loop
pushed every raised exception onto it (so handlers can read it) but only
popped on handler completion. Exceptions that propagated out of a frame
uncaught — including a generator/genexpr's completion StopIteration consumed
by the FOR_ITER that resumed it — were pushed and never popped, so they
accumulated. A later bare `raise` re-raised that stale StopIteration instead
of erroring, and implicit __context__ would pick it up.
Balance the lifecycle: when an exception propagates out of a frame that has
no handler, the eval loop now pops the entry it just pushed (the caller's
eval loop re-pushes it). With the stack kept clean:
- ReRaise returns RuntimeError("No active exception to reraise") on an empty
stack instead of asserting (bare `raise` outside a handler).
- The top-level driver uses the propagated result value rather than popping
the exception off the (now-empty) stack.
- from_iterable's manual StopIteration cleanup is removed — it is handled by
the eval loop now, and keeping it would wrongly pop a pre-existing
exception when iterating during exception handling.
- RaiseVarargs re-enables implicit __context__ chaining (set the new
exception's context to the one being handled); now reliable since the
stack no longer holds stale state.
exception_chaining.py now covers implicit __context__, bare `raise` ->
RuntimeError, and iterating generators/comprehensions while handling an
exception.
@gf712
gf712 merged commit af25589 into mainJun 24, 2026
1 check passed
@gf712
gf712 deleted the fix/generator-resume-stack-rebase branch June 24, 2026 08:40
Sign up for freeto 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

@gf712
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Various fixes to codegen and generator runtime - #23

Merged
gf712 merged 11 commits into
mainfrom
fix/generator-resume-stack-rebase
Jun 24, 2026
Merged

Various fixes to codegen and generator runtime#23
gf712 merged 11 commits into
mainfrom
fix/generator-resume-stack-rebase

Conversation

@gf712

Copy link
Copy Markdown
Owner

No description provided.

gf712and others added 11 commits June 23, 2026 20:12
…type
The Try-handler codegen stored the matched exception *type* into the bound
name (`store_name(handler->name(), handler->type()->codegen(), …)`), so
`except ValueError as e:` left `e` as the `ValueError` class instead of the
raised instance — `isinstance(e, ValueError)` was false, `type(e)` was
`type`, and any use of `e`'s value (e.g. `str(e)`) was wrong. (This is the
real bug behind a "miscompile" that looked layout-fragile only because of a
separate wrong-traceback-line issue.)
Add a `py.load_exception` op (lowered to a new LOAD_EXCEPTION bytecode
instruction, opcode 84) that reads the active exception instance from
`execution_frame()->exception_info()->exception`, modeled on
LoadAssertionError across the pipeline (Python + EmitPythonBytecode dialect
ops, ControlFlow lowering, bytecode emitter, instruction decoder). Bind the
name to that instance at the start of the matched handler block instead of
to the type in the cond block.
Each builtin exception is a distinct C++ class with its own create(), so
each must define __new__ to allocate its own type; the inherited
Exception::__new__ both hardcodes the Exception class and asserts
`type == types::exception()`. RuntimeError and NameError were missing
__new__, so `raise RuntimeError(...)` / `raise NameError(...)` aborted at
that assert. ModuleNotFoundError::__new__ dereferenced `kwargs->map()`
unconditionally, so `raise ModuleNotFoundError("m")` (no kwargs)
segfaulted.
- Add RuntimeError::__new__ and NameError::__new__ (mirroring ValueError).
- Guard the null kwargs in ModuleNotFoundError::__new__.
- BaseException.args now returns an empty tuple (not None) when the
exception was constructed without args, matching CPython.
Regression test integration/tests/exception_types.py raises every builtin
exception and checks isinstance/type/args. Full ctest (194) + integration
suite green.
(Note: the test calls its per-type check via a helper rather than inline in
the loop, to avoid the separate FOR_ITER iterator-register clobber bug that
a heavy loop body triggers — see integration/minimal_foriter_bug.py.)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XZ4THY2jXpZnNoxVTzDn32
A try body lowers to blocks whose only explicit CFG successors are the
normal-flow ones (e.g. RAISE_VARARGS -> exit); the edge from a faulting op
in the try body to the handler is implicit (established by SETUP_EXC_HANDLE
in the *predecessor*). mlir::Liveness therefore never sees body-op ->
handler, so a value that is live at the handler — or live after it — was
considered dead inside the try body, and the register allocator happily
reused its register there.
When an exception actually unwound, that register had been overwritten.
The clearest case: a `for` loop whose body contains `try/except`. The
FOR_ITER iterator is read again after the handler (next iteration), but its
register was reused for a value inside the try body, so the resumed loop
read a clobbered iterator (abort in ForIter / "object is not an iterator").
The same bug corrupted exception args across sequential try/except blocks.
Fix: in LiveAnalysis, for every block in a try body (those dominated by the
SETUP_EXC_HANDLE / SETUP_WITH try-entry successor), add the handler's
live-in set to the block's live-out before computing per-op liveness. Any
op in the try body can transfer to the handler, so values live at the
handler are live across the whole try body. Dominance keeps it bounded and
handles nesting; the pass is skipped entirely when a function has no
handlers.
This makes the iterator (and any value live past the handler) interfere
with try-body temporaries, so they no longer share a register.
…_context__
`raise X from Y` already stored the cause on the exception
(RaiseVarargs::execute), but BaseException exposed none of the chaining
attributes, so __cause__ was unreadable.
- Add context()/set_context() and suppress_context()/set_suppress_context()
accessors on BaseException (m_context/m_cause/m_suppress_context already
existed and are GC-visited).
- Register __cause__, __context__ and __suppress_context__ as read/write
properties. Setting __cause__ also sets __suppress_context__ = True, and
__suppress_context__ coerces via truthiness, matching the data model.
- `raise X from Y` now also sets __suppress_context__ = True (and `from None`
keeps the suppression with a None cause).
Implicit __context__ chaining (auto-setting the new exception's context to the
one being handled) is intentionally left out: the frame exception stack is not
reliably popped — internally-consumed StopIterations linger (the same
pre-existing bug that makes a bare `raise` outside a handler re-raise a stale
exception instead of erroring), so reading it would attach a spurious context.
…ext__
The frame exception stack is shared across a call chain, and the eval loop
pushed every raised exception onto it (so handlers can read it) but only
popped on handler completion. Exceptions that propagated out of a frame
uncaught — including a generator/genexpr's completion StopIteration consumed
by the FOR_ITER that resumed it — were pushed and never popped, so they
accumulated. A later bare `raise` re-raised that stale StopIteration instead
of erroring, and implicit __context__ would pick it up.
Balance the lifecycle: when an exception propagates out of a frame that has
no handler, the eval loop now pops the entry it just pushed (the caller's
eval loop re-pushes it). With the stack kept clean:
- ReRaise returns RuntimeError("No active exception to reraise") on an empty
stack instead of asserting (bare `raise` outside a handler).
- The top-level driver uses the propagated result value rather than popping
the exception off the (now-empty) stack.
- from_iterable's manual StopIteration cleanup is removed — it is handled by
the eval loop now, and keeping it would wrongly pop a pre-existing
exception when iterating during exception handling.
- RaiseVarargs re-enables implicit __context__ chaining (set the new
exception's context to the one being handled); now reliable since the
stack no longer holds stale state.
exception_chaining.py now covers implicit __context__, bare `raise` ->
RuntimeError, and iterating generators/comprehensions while handling an
exception.
@gf712
gf712 merged commit af25589 into mainJun 24, 2026
1 check passed
@gf712
gf712 deleted the fix/generator-resume-stack-rebase branch June 24, 2026 08:40
Sign up for freeto 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

@gf712
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

Various fixes to codegen and generator runtime - #23

Merged
gf712 merged 11 commits into
mainfrom
fix/generator-resume-stack-rebase
Jun 24, 2026
Merged

Various fixes to codegen and generator runtime#23
gf712 merged 11 commits into
mainfrom
fix/generator-resume-stack-rebase

Conversation

@gf712

Copy link
Copy Markdown
Owner

No description provided.

gf712and others added 11 commits June 23, 2026 20:12
…type
The Try-handler codegen stored the matched exception *type* into the bound
name (`store_name(handler->name(), handler->type()->codegen(), …)`), so
`except ValueError as e:` left `e` as the `ValueError` class instead of the
raised instance — `isinstance(e, ValueError)` was false, `type(e)` was
`type`, and any use of `e`'s value (e.g. `str(e)`) was wrong. (This is the
real bug behind a "miscompile" that looked layout-fragile only because of a
separate wrong-traceback-line issue.)
Add a `py.load_exception` op (lowered to a new LOAD_EXCEPTION bytecode
instruction, opcode 84) that reads the active exception instance from
`execution_frame()->exception_info()->exception`, modeled on
LoadAssertionError across the pipeline (Python + EmitPythonBytecode dialect
ops, ControlFlow lowering, bytecode emitter, instruction decoder). Bind the
name to that instance at the start of the matched handler block instead of
to the type in the cond block.
Each builtin exception is a distinct C++ class with its own create(), so
each must define __new__ to allocate its own type; the inherited
Exception::__new__ both hardcodes the Exception class and asserts
`type == types::exception()`. RuntimeError and NameError were missing
__new__, so `raise RuntimeError(...)` / `raise NameError(...)` aborted at
that assert. ModuleNotFoundError::__new__ dereferenced `kwargs->map()`
unconditionally, so `raise ModuleNotFoundError("m")` (no kwargs)
segfaulted.
- Add RuntimeError::__new__ and NameError::__new__ (mirroring ValueError).
- Guard the null kwargs in ModuleNotFoundError::__new__.
- BaseException.args now returns an empty tuple (not None) when the
exception was constructed without args, matching CPython.
Regression test integration/tests/exception_types.py raises every builtin
exception and checks isinstance/type/args. Full ctest (194) + integration
suite green.
(Note: the test calls its per-type check via a helper rather than inline in
the loop, to avoid the separate FOR_ITER iterator-register clobber bug that
a heavy loop body triggers — see integration/minimal_foriter_bug.py.)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XZ4THY2jXpZnNoxVTzDn32
A try body lowers to blocks whose only explicit CFG successors are the
normal-flow ones (e.g. RAISE_VARARGS -> exit); the edge from a faulting op
in the try body to the handler is implicit (established by SETUP_EXC_HANDLE
in the *predecessor*). mlir::Liveness therefore never sees body-op ->
handler, so a value that is live at the handler — or live after it — was
considered dead inside the try body, and the register allocator happily
reused its register there.
When an exception actually unwound, that register had been overwritten.
The clearest case: a `for` loop whose body contains `try/except`. The
FOR_ITER iterator is read again after the handler (next iteration), but its
register was reused for a value inside the try body, so the resumed loop
read a clobbered iterator (abort in ForIter / "object is not an iterator").
The same bug corrupted exception args across sequential try/except blocks.
Fix: in LiveAnalysis, for every block in a try body (those dominated by the
SETUP_EXC_HANDLE / SETUP_WITH try-entry successor), add the handler's
live-in set to the block's live-out before computing per-op liveness. Any
op in the try body can transfer to the handler, so values live at the
handler are live across the whole try body. Dominance keeps it bounded and
handles nesting; the pass is skipped entirely when a function has no
handlers.
This makes the iterator (and any value live past the handler) interfere
with try-body temporaries, so they no longer share a register.
…_context__
`raise X from Y` already stored the cause on the exception
(RaiseVarargs::execute), but BaseException exposed none of the chaining
attributes, so __cause__ was unreadable.
- Add context()/set_context() and suppress_context()/set_suppress_context()
accessors on BaseException (m_context/m_cause/m_suppress_context already
existed and are GC-visited).
- Register __cause__, __context__ and __suppress_context__ as read/write
properties. Setting __cause__ also sets __suppress_context__ = True, and
__suppress_context__ coerces via truthiness, matching the data model.
- `raise X from Y` now also sets __suppress_context__ = True (and `from None`
keeps the suppression with a None cause).
Implicit __context__ chaining (auto-setting the new exception's context to the
one being handled) is intentionally left out: the frame exception stack is not
reliably popped — internally-consumed StopIterations linger (the same
pre-existing bug that makes a bare `raise` outside a handler re-raise a stale
exception instead of erroring), so reading it would attach a spurious context.
…ext__
The frame exception stack is shared across a call chain, and the eval loop
pushed every raised exception onto it (so handlers can read it) but only
popped on handler completion. Exceptions that propagated out of a frame
uncaught — including a generator/genexpr's completion StopIteration consumed
by the FOR_ITER that resumed it — were pushed and never popped, so they
accumulated. A later bare `raise` re-raised that stale StopIteration instead
of erroring, and implicit __context__ would pick it up.
Balance the lifecycle: when an exception propagates out of a frame that has
no handler, the eval loop now pops the entry it just pushed (the caller's
eval loop re-pushes it). With the stack kept clean:
- ReRaise returns RuntimeError("No active exception to reraise") on an empty
stack instead of asserting (bare `raise` outside a handler).
- The top-level driver uses the propagated result value rather than popping
the exception off the (now-empty) stack.
- from_iterable's manual StopIteration cleanup is removed — it is handled by
the eval loop now, and keeping it would wrongly pop a pre-existing
exception when iterating during exception handling.
- RaiseVarargs re-enables implicit __context__ chaining (set the new
exception's context to the one being handled); now reliable since the
stack no longer holds stale state.
exception_chaining.py now covers implicit __context__, bare `raise` ->
RuntimeError, and iterating generators/comprehensions while handling an
exception.
@gf712
gf712 merged commit af25589 into mainJun 24, 2026
1 check passed
@gf712
gf712 deleted the fix/generator-resume-stack-rebase branch June 24, 2026 08:40
Sign up for freeto 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

@gf712