Interpreter EH implementation - #116046

Merged
janvorli merged 5 commits into
dotnet:mainfrom
janvorli:interpreter-eh
Jun 2, 2025
Merged

Interpreter EH implementation#116046
janvorli merged 5 commits into
dotnet:mainfrom
janvorli:interpreter-eh

Conversation

@janvorli

Copy link
Copy Markdown
Member

This change implements EH support in the interpreter compiler and execution parts.

Here is a summary of the changes:

On the compilation side:

  • Adds support for CEE_THROW, CEE_RETHROW, CEE_ENDFILTER, CEE_ENDFINALLY, CEE_LEAVE and CEE_ISINST opcodes
  • Adds building of EH info with IR code offsets
  • Implements proper funclet handling in the same way the JIT does. All handlers and filters are moved to the end of the method and out of any try ranges recursively to enable proper behavior of the EH.
  • Fixes a bug related to wrong SVar and and an issue with IL offset of inserted instructions in AllocOffsets
  • Fixes a bug in the CEEOpcodeSize - off by one check for the end of the code

On the execution side:

  • Add funclet start address extraction
  • Add calling funclets in the interpreted code
  • Removed GCX_PREEMP_NO_DTOR from the CallDescrWorkerUnwindFrameChainHandler, because it was not correct
  • Added new IR opcodes:
    • INTOP_LOAD_FRAMEVAR to load parent frame stack pointer in filter funclets that need to run in the context of the parent, but at the top of the current interpreter stack.
    • INTOP_RETHROW to rethrow an exception
    • INTOP_CALL_FINALLY to call finally funclet in non-exceptional cases
    • INTOP_LEAVE_FILTER to exit a filter funclet and return the filter result
    • INTOP_LEAVE_CATCH to exit a catch handler and return the resume address
  • Added calls to COMPlusThrow for division by zero, stack overflow and few other exceptions where the interpreter had just TODO and assert for adding those.
  • Modified the InterpExecMethod so that extra information can be passed in case of a funclet invocation.

It also adds tests to verify various interesting EH scenarios

Here are some more details on moving out the funclets: For each finally funclet, we create a finally call island that stays in the code where the original finally was. That island calls the finally and then branches to the next (outer) finally if any. The last finally call island branches to the actual leave target.
The interpreter compiler generates a separate sequence of finally call islands for each leave instruction target. And when the leave is executed, it jumps to the beginning or into the middle of the chain. In other words, for example, if all leave instructions in the method go to the same target, there would be just one call finally island chain.

@janvorlijanvorli added this to the 10.0.0 milestone May 27, 2025
@janvorlijanvorli self-assigned this May 27, 2025
CopilotAI review requested due to automatic review settings May 27, 2025 22:28
@janvorli
janvorli requested review from BrzVlad and kg as code ownersMay 27, 2025 22:28
@dotnet-policy-service

Copy link
Copy Markdown
Contributor

Tagging subscribers to this area: @BrzVlad, @janvorli, @kg
See info in area-owners.md if you want to be subscribed.

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull Request Overview

This PR adds full exception-handling (EH) support to the interpreter, covering both compilation and execution phases. It introduces new IR opcodes, builds EH metadata, implements funclet-based EH in the interpreter, and adds tests for various EH scenarios.

  • Added support for EH opcodes (throw, rethrow, leave, filters, finally) and IR-level exception clauses
  • Updated interpreter execution (InterpExecMethod) to dispatch filters, catches, and finally blocks
  • Built and emitted EH information (funclet offsets, EH tables) and wired new EH helpers

Reviewed Changes

Copilot reviewed 16 out of 16 changed files in this pull request and generated 2 comments.

Show a summary per file
FileDescription
src/coreclr/vm/prestub.cppIntroduce retVal storage for interpreter return value
src/coreclr/vm/jithelpers.cppSwap in DispatchRethrownManagedException call
src/coreclr/vm/interpexec.hExtend InterpExecMethod signature with EH args
src/coreclr/vm/interpexec.cppHandle new INTOP_* opcodes and null/overflow checks
src/coreclr/vm/exceptionhandling.hDeclare DispatchRethrownManagedException overloads
src/coreclr/vm/exceptionhandling.cppImplement DispatchRethrownManagedException and adjust dispatch logic
src/coreclr/vm/eetwain.cppFlesh out InterpreterCodeManager::CallFunclet
src/coreclr/vm/codeman.hUpdate GetFuncletStartOffsets declaration
src/coreclr/vm/codeman.cppImplement funclet start address/offset enumeration
src/coreclr/interpreter/intops.defAdd EH-related INTOP definitions
src/coreclr/interpreter/intops.cppFix CEEOpcodeSize boundary condition
src/coreclr/interpreter/eeinterp.cppInvoke BuildEHInfo() during compilation
src/coreclr/interpreter/compileropt.cppCorrect ilOffset propagation and SetSVar call
src/coreclr/interpreter/compiler.hExtend compiler IR and block structures for EH
Comments suppressed due to low confidence (3)

src/coreclr/vm/exceptionhandling.cpp:2138

  • Removing the GCX_PREEMP_NO_DTOR switch back to preemptive GC in the non-unwind (search) path can leave the runtime in cooperative mode and break stack walking. Reintroduce a mode switch before returning ExceptionContinueSearch.
else if (IS_UNWINDING(pExceptionRecord->ExceptionFlags))

src/coreclr/vm/jithelpers.cpp:1357

  • The identifier exceptionFrame is not declared in this scope, leading to a compile error. You need to pass a valid CONTEXT* (e.g., capture it or fetch it from the exception tracker) to DispatchRethrownManagedException.
DispatchRethrownManagedException(exceptionFrame.GetContext());

src/coreclr/vm/interpexec.h:77

  • There are now two overloads of InterpExecMethod (one with default ExceptionClauseArgs and the original three-argument version), which can be confusing. Consider removing the three-argument prototype and relying solely on the default parameter.
void InterpExecMethod(InterpreterFrame *pInterpreterFrame, InterpMethodContextFrame *pFrame, InterpThreadContext *pThreadContext, ExceptionClauseArgs *pExceptionClauseArgs = NULL);

Comment threadsrc/coreclr/vm/eetwain.cpp
Comment threadsrc/coreclr/vm/interpexec.cpp
@janvorli
janvorliforce-pushed the interpreter-eh branch 2 times, most recently from aee764e to 02cb97bCompareMay 27, 2025 22:34
This change implements EH support in the interpreter compiler and
execution parts.
Here is a summary of the changes:
On the compilation side:
* Adds support for CEE_THROW, CEE_RETHROW, CEE_ENDFILTER,
CEE_ENDFINALLY, CEE_LEAVE and CEE_ISINST opcodes
* Adds building of EH info with IR code offsets
* Implements proper funclet handling in the same way the JIT does. All
handlers and filters are moved to the end of the method and out of any
try ranges recursively to enable proper behavior of the EH.
* Fixes a bug related to wrong SVar and and an issue with IL offset of
inserted instructions in AllocOffsets
* Fixes a bug in the CEEOpcodeSize - off by one check for the end of the
code
On the execution side:
* Add funclet start address extraction
* Add calling funclets in the interpreted code
* Removed GCX_PREEMP_NO_DTOR from the
CallDescrWorkerUnwindFrameChainHandler, because it was not correct
* Added new IR opcodes:
* INTOP_LOAD_FRAMEVAR to load parent frame stack pointer in filter
funclets that need to run in the context of the parent, but at the
top of the current interpreter stack.
* INTOP_RETHROW to rethrow an exception
* INTOP_CALL_FINALLY to call finally funclet in non-exceptional cases
* INTOP_LEAVE_FILTER to exit a filter funclet and return the filter result
* INTOP_LEAVE_CATCH to exit a catch handler and return the resume
address
* Added calls to COMPlusThrow for division by zero, stack overflow and
few other exceptions where the interpreter had just TODO and assert
for adding those.
* Modified the InterpExecMethod so that extra information can be passed
in case of a funclet invocation.
It also adds tests to verify various interesting EH scenarios
Here are some more details on moving out the funclets:
For each finally funclet, we create a finally call island that stays in
the code where the original finally was. That island calls the finally and
then branches to the next (outer) finally if any. The last finally call
island branches to the actual leave target.
The interpreter compiler generates a separate sequence of finally call
islands for each leave instruction target. And when the leave is
executed, it jumps to the beginning or into the middle of the chain.
In other words, for example, if all leave instructions in the method go
to the same target, there would be just one call finally island chain.
Comment threadsrc/coreclr/interpreter/compiler.cpp Outdated
Comment threadsrc/coreclr/interpreter/compiler.cpp Outdated
Comment threadsrc/coreclr/interpreter/compiler.cpp Outdated
Comment threadsrc/coreclr/interpreter/compiler.cpp
Comment threadsrc/coreclr/interpreter/compiler.cpp
Comment threadsrc/coreclr/interpreter/compiler.cpp
Comment threadsrc/coreclr/interpreter/compiler.cpp
Comment threadsrc/coreclr/interpreter/compiler.cpp
Comment threadsrc/coreclr/interpreter/compiler.cpp
Comment threadsrc/coreclr/interpreter/compiler.cpp
Comment threadsrc/coreclr/interpreter/compiler.cpp
Comment threadsrc/coreclr/interpreter/compiler.cpp
Comment threadsrc/coreclr/interpreter/compiler.cpp Outdated
Comment threadsrc/coreclr/interpreter/compiler.cpp Outdated
Comment threadsrc/coreclr/interpreter/compiler.cpp
Comment threadsrc/coreclr/interpreter/compiler.cpp
Comment threadsrc/coreclr/interpreter/compiler.cpp
Comment threadsrc/coreclr/interpreter/compiler.cpp
Comment threadsrc/coreclr/interpreter/compiler.cpp Outdated
AddIns(INTOP_LOAD_FRAMEVAR);
PushInterpType(InterpTypeI, NULL);
m_pLastNewIns->SetDVar(m_pStackPointer[-1].var);
EmitStind(interpType, m_pVars[var].clsHnd, m_pVars[var].offset, true /* reverseSVarOrder */);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we need the indirect from LoadVar we should have it here too i think

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am not sure what you mean, can you please explain it in more detail?

Comment threadsrc/coreclr/interpreter/compiler.cpp Outdated
Comment threadsrc/coreclr/interpreter/compiler.cpp
Comment threadsrc/coreclr/vm/interpexec.cpp Outdated
Comment threadsrc/coreclr/interpreter/compiler.cpp
Comment threadsrc/coreclr/interpreter/compiler.cpp
@janvorli

Copy link
Copy Markdown
MemberAuthor

There is some problem with the IL to native map generation when running with checked build of the runtime. Debug build works fine. I am looking into it.

The call finally islands were not classified by the clause they are in.
Their instructions had IL offsets set to the offset of the finally
block, but they need to have no IL offset as they are not generated
from IL.
@janvorli

Copy link
Copy Markdown
MemberAuthor

I've fixed the problem with the checked build of the tests. The problem was that the call finally islands didn't have the clauseType set and so when generating native form of the EH info, they were missing in the try regions ranges.

@kg

kg commented May 29, 2025

Copy link
Copy Markdown
Contributor

I forgot to comment when I finished going over this, it all LGTM. I'm fine with landing it.

kg
kg approved these changes May 29, 2025
Comment threadsrc/coreclr/interpreter/compiler.cpp
Comment threadsrc/coreclr/interpreter/compiler.cpp Outdated
Comment threadsrc/coreclr/interpreter/compiler.cpp Outdated
Comment threadsrc/coreclr/interpreter/compiler.cpp
Comment threadsrc/coreclr/vm/interpexec.cpp Outdated
@BrzVlad

Copy link
Copy Markdown
Member

Had this written down after a call a while ago with @janvorli. Might be useful for visualization.

try {
try {
if
leave end2; // INTOP_LEAVE -> jump to finally_island_1a leave end1; // INTOP_LEAVE -> jump to finally_island_1b
} catch {
leave end1; // INTOP_LEAVE -> jump to finally_island_1b
} finally1 {
endfinally
}
-- finally_island_1a --
call_finally finally1
br finally_island_2
--------------------- -- finally_island_1b --
call_finally finally1
br end1
--------------------- end1;
leave end2; // INTOP_LEAVE -> jump to finally_island_2
} catch {
leave end2; // INTOP_LEAVE -> jump to finally_island_2
} finally2 {
endfinally
}
-- finally_island_2 --
call_finally finally2
br end2;
---------------------
end2;

@BrzVlad

Copy link
Copy Markdown
Member

Looks great! Thanks!

@janvorli
janvorli merged commit 8d59055 into dotnet:mainJun 2, 2025
@janvorli
janvorli deleted the interpreter-eh branch June 2, 2025 16:48
@github-actionsgithub-actionsBot locked and limited conversation to collaborators Jul 3, 2025
Sign up for freeto subscribe to this conversation on GitHub. Already have an account? Sign in.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants

@janvorli@kg@BrzVlad@jkotas@SingleAccretion
, '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

Interpreter EH implementation - #116046

Merged
janvorli merged 5 commits into
dotnet:mainfrom
janvorli:interpreter-eh
Jun 2, 2025
Merged

Interpreter EH implementation#116046
janvorli merged 5 commits into
dotnet:mainfrom
janvorli:interpreter-eh

Conversation

@janvorli

Copy link
Copy Markdown
Member

This change implements EH support in the interpreter compiler and execution parts.

Here is a summary of the changes:

On the compilation side:

  • Adds support for CEE_THROW, CEE_RETHROW, CEE_ENDFILTER, CEE_ENDFINALLY, CEE_LEAVE and CEE_ISINST opcodes
  • Adds building of EH info with IR code offsets
  • Implements proper funclet handling in the same way the JIT does. All handlers and filters are moved to the end of the method and out of any try ranges recursively to enable proper behavior of the EH.
  • Fixes a bug related to wrong SVar and and an issue with IL offset of inserted instructions in AllocOffsets
  • Fixes a bug in the CEEOpcodeSize - off by one check for the end of the code

On the execution side:

  • Add funclet start address extraction
  • Add calling funclets in the interpreted code
  • Removed GCX_PREEMP_NO_DTOR from the CallDescrWorkerUnwindFrameChainHandler, because it was not correct
  • Added new IR opcodes:
    • INTOP_LOAD_FRAMEVAR to load parent frame stack pointer in filter funclets that need to run in the context of the parent, but at the top of the current interpreter stack.
    • INTOP_RETHROW to rethrow an exception
    • INTOP_CALL_FINALLY to call finally funclet in non-exceptional cases
    • INTOP_LEAVE_FILTER to exit a filter funclet and return the filter result
    • INTOP_LEAVE_CATCH to exit a catch handler and return the resume address
  • Added calls to COMPlusThrow for division by zero, stack overflow and few other exceptions where the interpreter had just TODO and assert for adding those.
  • Modified the InterpExecMethod so that extra information can be passed in case of a funclet invocation.

It also adds tests to verify various interesting EH scenarios

Here are some more details on moving out the funclets: For each finally funclet, we create a finally call island that stays in the code where the original finally was. That island calls the finally and then branches to the next (outer) finally if any. The last finally call island branches to the actual leave target.
The interpreter compiler generates a separate sequence of finally call islands for each leave instruction target. And when the leave is executed, it jumps to the beginning or into the middle of the chain. In other words, for example, if all leave instructions in the method go to the same target, there would be just one call finally island chain.

@janvorlijanvorli added this to the 10.0.0 milestone May 27, 2025
@janvorlijanvorli self-assigned this May 27, 2025
CopilotAI review requested due to automatic review settings May 27, 2025 22:28
@janvorli
janvorli requested review from BrzVlad and kg as code ownersMay 27, 2025 22:28
@dotnet-policy-service

Copy link
Copy Markdown
Contributor

Tagging subscribers to this area: @BrzVlad, @janvorli, @kg
See info in area-owners.md if you want to be subscribed.

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull Request Overview

This PR adds full exception-handling (EH) support to the interpreter, covering both compilation and execution phases. It introduces new IR opcodes, builds EH metadata, implements funclet-based EH in the interpreter, and adds tests for various EH scenarios.

  • Added support for EH opcodes (throw, rethrow, leave, filters, finally) and IR-level exception clauses
  • Updated interpreter execution (InterpExecMethod) to dispatch filters, catches, and finally blocks
  • Built and emitted EH information (funclet offsets, EH tables) and wired new EH helpers

Reviewed Changes

Copilot reviewed 16 out of 16 changed files in this pull request and generated 2 comments.

Show a summary per file
FileDescription
src/coreclr/vm/prestub.cppIntroduce retVal storage for interpreter return value
src/coreclr/vm/jithelpers.cppSwap in DispatchRethrownManagedException call
src/coreclr/vm/interpexec.hExtend InterpExecMethod signature with EH args
src/coreclr/vm/interpexec.cppHandle new INTOP_* opcodes and null/overflow checks
src/coreclr/vm/exceptionhandling.hDeclare DispatchRethrownManagedException overloads
src/coreclr/vm/exceptionhandling.cppImplement DispatchRethrownManagedException and adjust dispatch logic
src/coreclr/vm/eetwain.cppFlesh out InterpreterCodeManager::CallFunclet
src/coreclr/vm/codeman.hUpdate GetFuncletStartOffsets declaration
src/coreclr/vm/codeman.cppImplement funclet start address/offset enumeration
src/coreclr/interpreter/intops.defAdd EH-related INTOP definitions
src/coreclr/interpreter/intops.cppFix CEEOpcodeSize boundary condition
src/coreclr/interpreter/eeinterp.cppInvoke BuildEHInfo() during compilation
src/coreclr/interpreter/compileropt.cppCorrect ilOffset propagation and SetSVar call
src/coreclr/interpreter/compiler.hExtend compiler IR and block structures for EH
Comments suppressed due to low confidence (3)

src/coreclr/vm/exceptionhandling.cpp:2138

  • Removing the GCX_PREEMP_NO_DTOR switch back to preemptive GC in the non-unwind (search) path can leave the runtime in cooperative mode and break stack walking. Reintroduce a mode switch before returning ExceptionContinueSearch.
else if (IS_UNWINDING(pExceptionRecord->ExceptionFlags))

src/coreclr/vm/jithelpers.cpp:1357

  • The identifier exceptionFrame is not declared in this scope, leading to a compile error. You need to pass a valid CONTEXT* (e.g., capture it or fetch it from the exception tracker) to DispatchRethrownManagedException.
DispatchRethrownManagedException(exceptionFrame.GetContext());

src/coreclr/vm/interpexec.h:77

  • There are now two overloads of InterpExecMethod (one with default ExceptionClauseArgs and the original three-argument version), which can be confusing. Consider removing the three-argument prototype and relying solely on the default parameter.
void InterpExecMethod(InterpreterFrame *pInterpreterFrame, InterpMethodContextFrame *pFrame, InterpThreadContext *pThreadContext, ExceptionClauseArgs *pExceptionClauseArgs = NULL);

Comment threadsrc/coreclr/vm/eetwain.cpp
Comment threadsrc/coreclr/vm/interpexec.cpp
@janvorli
janvorliforce-pushed the interpreter-eh branch 2 times, most recently from aee764e to 02cb97bCompareMay 27, 2025 22:34
This change implements EH support in the interpreter compiler and
execution parts.
Here is a summary of the changes:
On the compilation side:
* Adds support for CEE_THROW, CEE_RETHROW, CEE_ENDFILTER,
CEE_ENDFINALLY, CEE_LEAVE and CEE_ISINST opcodes
* Adds building of EH info with IR code offsets
* Implements proper funclet handling in the same way the JIT does. All
handlers and filters are moved to the end of the method and out of any
try ranges recursively to enable proper behavior of the EH.
* Fixes a bug related to wrong SVar and and an issue with IL offset of
inserted instructions in AllocOffsets
* Fixes a bug in the CEEOpcodeSize - off by one check for the end of the
code
On the execution side:
* Add funclet start address extraction
* Add calling funclets in the interpreted code
* Removed GCX_PREEMP_NO_DTOR from the
CallDescrWorkerUnwindFrameChainHandler, because it was not correct
* Added new IR opcodes:
* INTOP_LOAD_FRAMEVAR to load parent frame stack pointer in filter
funclets that need to run in the context of the parent, but at the
top of the current interpreter stack.
* INTOP_RETHROW to rethrow an exception
* INTOP_CALL_FINALLY to call finally funclet in non-exceptional cases
* INTOP_LEAVE_FILTER to exit a filter funclet and return the filter result
* INTOP_LEAVE_CATCH to exit a catch handler and return the resume
address
* Added calls to COMPlusThrow for division by zero, stack overflow and
few other exceptions where the interpreter had just TODO and assert
for adding those.
* Modified the InterpExecMethod so that extra information can be passed
in case of a funclet invocation.
It also adds tests to verify various interesting EH scenarios
Here are some more details on moving out the funclets:
For each finally funclet, we create a finally call island that stays in
the code where the original finally was. That island calls the finally and
then branches to the next (outer) finally if any. The last finally call
island branches to the actual leave target.
The interpreter compiler generates a separate sequence of finally call
islands for each leave instruction target. And when the leave is
executed, it jumps to the beginning or into the middle of the chain.
In other words, for example, if all leave instructions in the method go
to the same target, there would be just one call finally island chain.
Comment threadsrc/coreclr/interpreter/compiler.cpp Outdated
Comment threadsrc/coreclr/interpreter/compiler.cpp Outdated
Comment threadsrc/coreclr/interpreter/compiler.cpp Outdated
Comment threadsrc/coreclr/interpreter/compiler.cpp
Comment threadsrc/coreclr/interpreter/compiler.cpp
Comment threadsrc/coreclr/interpreter/compiler.cpp
Comment threadsrc/coreclr/interpreter/compiler.cpp
Comment threadsrc/coreclr/interpreter/compiler.cpp
Comment threadsrc/coreclr/interpreter/compiler.cpp
Comment threadsrc/coreclr/interpreter/compiler.cpp
Comment threadsrc/coreclr/interpreter/compiler.cpp
Comment threadsrc/coreclr/interpreter/compiler.cpp
Comment threadsrc/coreclr/interpreter/compiler.cpp Outdated
Comment threadsrc/coreclr/interpreter/compiler.cpp Outdated
Comment threadsrc/coreclr/interpreter/compiler.cpp
Comment threadsrc/coreclr/interpreter/compiler.cpp
Comment threadsrc/coreclr/interpreter/compiler.cpp
Comment threadsrc/coreclr/interpreter/compiler.cpp
Comment threadsrc/coreclr/interpreter/compiler.cpp Outdated
AddIns(INTOP_LOAD_FRAMEVAR);
PushInterpType(InterpTypeI, NULL);
m_pLastNewIns->SetDVar(m_pStackPointer[-1].var);
EmitStind(interpType, m_pVars[var].clsHnd, m_pVars[var].offset, true /* reverseSVarOrder */);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we need the indirect from LoadVar we should have it here too i think

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am not sure what you mean, can you please explain it in more detail?

Comment threadsrc/coreclr/interpreter/compiler.cpp Outdated
Comment threadsrc/coreclr/interpreter/compiler.cpp
Comment threadsrc/coreclr/vm/interpexec.cpp Outdated
Comment threadsrc/coreclr/interpreter/compiler.cpp
Comment threadsrc/coreclr/interpreter/compiler.cpp
@janvorli

Copy link
Copy Markdown
MemberAuthor

There is some problem with the IL to native map generation when running with checked build of the runtime. Debug build works fine. I am looking into it.

The call finally islands were not classified by the clause they are in.
Their instructions had IL offsets set to the offset of the finally
block, but they need to have no IL offset as they are not generated
from IL.
@janvorli

Copy link
Copy Markdown
MemberAuthor

I've fixed the problem with the checked build of the tests. The problem was that the call finally islands didn't have the clauseType set and so when generating native form of the EH info, they were missing in the try regions ranges.

@kg

kg commented May 29, 2025

Copy link
Copy Markdown
Contributor

I forgot to comment when I finished going over this, it all LGTM. I'm fine with landing it.

kg
kg approved these changes May 29, 2025
Comment threadsrc/coreclr/interpreter/compiler.cpp
Comment threadsrc/coreclr/interpreter/compiler.cpp Outdated
Comment threadsrc/coreclr/interpreter/compiler.cpp Outdated
Comment threadsrc/coreclr/interpreter/compiler.cpp
Comment threadsrc/coreclr/vm/interpexec.cpp Outdated
@BrzVlad

Copy link
Copy Markdown
Member

Had this written down after a call a while ago with @janvorli. Might be useful for visualization.

try {
try {
if
leave end2; // INTOP_LEAVE -> jump to finally_island_1a leave end1; // INTOP_LEAVE -> jump to finally_island_1b
} catch {
leave end1; // INTOP_LEAVE -> jump to finally_island_1b
} finally1 {
endfinally
}
-- finally_island_1a --
call_finally finally1
br finally_island_2
--------------------- -- finally_island_1b --
call_finally finally1
br end1
--------------------- end1;
leave end2; // INTOP_LEAVE -> jump to finally_island_2
} catch {
leave end2; // INTOP_LEAVE -> jump to finally_island_2
} finally2 {
endfinally
}
-- finally_island_2 --
call_finally finally2
br end2;
---------------------
end2;

@BrzVlad

Copy link
Copy Markdown
Member

Looks great! Thanks!

@janvorli
janvorli merged commit 8d59055 into dotnet:mainJun 2, 2025
@janvorli
janvorli deleted the interpreter-eh branch June 2, 2025 16:48
@github-actionsgithub-actionsBot locked and limited conversation to collaborators Jul 3, 2025
Sign up for freeto subscribe to this conversation on GitHub. Already have an account? Sign in.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants

@janvorli@kg@BrzVlad@jkotas@SingleAccretion
, '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

Interpreter EH implementation - #116046

Merged
janvorli merged 5 commits into
dotnet:mainfrom
janvorli:interpreter-eh
Jun 2, 2025
Merged

Interpreter EH implementation#116046
janvorli merged 5 commits into
dotnet:mainfrom
janvorli:interpreter-eh

Conversation

@janvorli

Copy link
Copy Markdown
Member

This change implements EH support in the interpreter compiler and execution parts.

Here is a summary of the changes:

On the compilation side:

  • Adds support for CEE_THROW, CEE_RETHROW, CEE_ENDFILTER, CEE_ENDFINALLY, CEE_LEAVE and CEE_ISINST opcodes
  • Adds building of EH info with IR code offsets
  • Implements proper funclet handling in the same way the JIT does. All handlers and filters are moved to the end of the method and out of any try ranges recursively to enable proper behavior of the EH.
  • Fixes a bug related to wrong SVar and and an issue with IL offset of inserted instructions in AllocOffsets
  • Fixes a bug in the CEEOpcodeSize - off by one check for the end of the code

On the execution side:

  • Add funclet start address extraction
  • Add calling funclets in the interpreted code
  • Removed GCX_PREEMP_NO_DTOR from the CallDescrWorkerUnwindFrameChainHandler, because it was not correct
  • Added new IR opcodes:
    • INTOP_LOAD_FRAMEVAR to load parent frame stack pointer in filter funclets that need to run in the context of the parent, but at the top of the current interpreter stack.
    • INTOP_RETHROW to rethrow an exception
    • INTOP_CALL_FINALLY to call finally funclet in non-exceptional cases
    • INTOP_LEAVE_FILTER to exit a filter funclet and return the filter result
    • INTOP_LEAVE_CATCH to exit a catch handler and return the resume address
  • Added calls to COMPlusThrow for division by zero, stack overflow and few other exceptions where the interpreter had just TODO and assert for adding those.
  • Modified the InterpExecMethod so that extra information can be passed in case of a funclet invocation.

It also adds tests to verify various interesting EH scenarios

Here are some more details on moving out the funclets: For each finally funclet, we create a finally call island that stays in the code where the original finally was. That island calls the finally and then branches to the next (outer) finally if any. The last finally call island branches to the actual leave target.
The interpreter compiler generates a separate sequence of finally call islands for each leave instruction target. And when the leave is executed, it jumps to the beginning or into the middle of the chain. In other words, for example, if all leave instructions in the method go to the same target, there would be just one call finally island chain.

@janvorlijanvorli added this to the 10.0.0 milestone May 27, 2025
@janvorlijanvorli self-assigned this May 27, 2025
CopilotAI review requested due to automatic review settings May 27, 2025 22:28
@janvorli
janvorli requested review from BrzVlad and kg as code ownersMay 27, 2025 22:28
@dotnet-policy-service

Copy link
Copy Markdown
Contributor

Tagging subscribers to this area: @BrzVlad, @janvorli, @kg
See info in area-owners.md if you want to be subscribed.

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull Request Overview

This PR adds full exception-handling (EH) support to the interpreter, covering both compilation and execution phases. It introduces new IR opcodes, builds EH metadata, implements funclet-based EH in the interpreter, and adds tests for various EH scenarios.

  • Added support for EH opcodes (throw, rethrow, leave, filters, finally) and IR-level exception clauses
  • Updated interpreter execution (InterpExecMethod) to dispatch filters, catches, and finally blocks
  • Built and emitted EH information (funclet offsets, EH tables) and wired new EH helpers

Reviewed Changes

Copilot reviewed 16 out of 16 changed files in this pull request and generated 2 comments.

Show a summary per file
FileDescription
src/coreclr/vm/prestub.cppIntroduce retVal storage for interpreter return value
src/coreclr/vm/jithelpers.cppSwap in DispatchRethrownManagedException call
src/coreclr/vm/interpexec.hExtend InterpExecMethod signature with EH args
src/coreclr/vm/interpexec.cppHandle new INTOP_* opcodes and null/overflow checks
src/coreclr/vm/exceptionhandling.hDeclare DispatchRethrownManagedException overloads
src/coreclr/vm/exceptionhandling.cppImplement DispatchRethrownManagedException and adjust dispatch logic
src/coreclr/vm/eetwain.cppFlesh out InterpreterCodeManager::CallFunclet
src/coreclr/vm/codeman.hUpdate GetFuncletStartOffsets declaration
src/coreclr/vm/codeman.cppImplement funclet start address/offset enumeration
src/coreclr/interpreter/intops.defAdd EH-related INTOP definitions
src/coreclr/interpreter/intops.cppFix CEEOpcodeSize boundary condition
src/coreclr/interpreter/eeinterp.cppInvoke BuildEHInfo() during compilation
src/coreclr/interpreter/compileropt.cppCorrect ilOffset propagation and SetSVar call
src/coreclr/interpreter/compiler.hExtend compiler IR and block structures for EH
Comments suppressed due to low confidence (3)

src/coreclr/vm/exceptionhandling.cpp:2138

  • Removing the GCX_PREEMP_NO_DTOR switch back to preemptive GC in the non-unwind (search) path can leave the runtime in cooperative mode and break stack walking. Reintroduce a mode switch before returning ExceptionContinueSearch.
else if (IS_UNWINDING(pExceptionRecord->ExceptionFlags))

src/coreclr/vm/jithelpers.cpp:1357

  • The identifier exceptionFrame is not declared in this scope, leading to a compile error. You need to pass a valid CONTEXT* (e.g., capture it or fetch it from the exception tracker) to DispatchRethrownManagedException.
DispatchRethrownManagedException(exceptionFrame.GetContext());

src/coreclr/vm/interpexec.h:77

  • There are now two overloads of InterpExecMethod (one with default ExceptionClauseArgs and the original three-argument version), which can be confusing. Consider removing the three-argument prototype and relying solely on the default parameter.
void InterpExecMethod(InterpreterFrame *pInterpreterFrame, InterpMethodContextFrame *pFrame, InterpThreadContext *pThreadContext, ExceptionClauseArgs *pExceptionClauseArgs = NULL);

Comment threadsrc/coreclr/vm/eetwain.cpp
Comment threadsrc/coreclr/vm/interpexec.cpp
@janvorli
janvorliforce-pushed the interpreter-eh branch 2 times, most recently from aee764e to 02cb97bCompareMay 27, 2025 22:34
This change implements EH support in the interpreter compiler and
execution parts.
Here is a summary of the changes:
On the compilation side:
* Adds support for CEE_THROW, CEE_RETHROW, CEE_ENDFILTER,
CEE_ENDFINALLY, CEE_LEAVE and CEE_ISINST opcodes
* Adds building of EH info with IR code offsets
* Implements proper funclet handling in the same way the JIT does. All
handlers and filters are moved to the end of the method and out of any
try ranges recursively to enable proper behavior of the EH.
* Fixes a bug related to wrong SVar and and an issue with IL offset of
inserted instructions in AllocOffsets
* Fixes a bug in the CEEOpcodeSize - off by one check for the end of the
code
On the execution side:
* Add funclet start address extraction
* Add calling funclets in the interpreted code
* Removed GCX_PREEMP_NO_DTOR from the
CallDescrWorkerUnwindFrameChainHandler, because it was not correct
* Added new IR opcodes:
* INTOP_LOAD_FRAMEVAR to load parent frame stack pointer in filter
funclets that need to run in the context of the parent, but at the
top of the current interpreter stack.
* INTOP_RETHROW to rethrow an exception
* INTOP_CALL_FINALLY to call finally funclet in non-exceptional cases
* INTOP_LEAVE_FILTER to exit a filter funclet and return the filter result
* INTOP_LEAVE_CATCH to exit a catch handler and return the resume
address
* Added calls to COMPlusThrow for division by zero, stack overflow and
few other exceptions where the interpreter had just TODO and assert
for adding those.
* Modified the InterpExecMethod so that extra information can be passed
in case of a funclet invocation.
It also adds tests to verify various interesting EH scenarios
Here are some more details on moving out the funclets:
For each finally funclet, we create a finally call island that stays in
the code where the original finally was. That island calls the finally and
then branches to the next (outer) finally if any. The last finally call
island branches to the actual leave target.
The interpreter compiler generates a separate sequence of finally call
islands for each leave instruction target. And when the leave is
executed, it jumps to the beginning or into the middle of the chain.
In other words, for example, if all leave instructions in the method go
to the same target, there would be just one call finally island chain.
Comment threadsrc/coreclr/interpreter/compiler.cpp Outdated
Comment threadsrc/coreclr/interpreter/compiler.cpp Outdated
Comment threadsrc/coreclr/interpreter/compiler.cpp Outdated
Comment threadsrc/coreclr/interpreter/compiler.cpp
Comment threadsrc/coreclr/interpreter/compiler.cpp
Comment threadsrc/coreclr/interpreter/compiler.cpp
Comment threadsrc/coreclr/interpreter/compiler.cpp
Comment threadsrc/coreclr/interpreter/compiler.cpp
Comment threadsrc/coreclr/interpreter/compiler.cpp
Comment threadsrc/coreclr/interpreter/compiler.cpp
Comment threadsrc/coreclr/interpreter/compiler.cpp
Comment threadsrc/coreclr/interpreter/compiler.cpp
Comment threadsrc/coreclr/interpreter/compiler.cpp Outdated
Comment threadsrc/coreclr/interpreter/compiler.cpp Outdated
Comment threadsrc/coreclr/interpreter/compiler.cpp
Comment threadsrc/coreclr/interpreter/compiler.cpp
Comment threadsrc/coreclr/interpreter/compiler.cpp
Comment threadsrc/coreclr/interpreter/compiler.cpp
Comment threadsrc/coreclr/interpreter/compiler.cpp Outdated
AddIns(INTOP_LOAD_FRAMEVAR);
PushInterpType(InterpTypeI, NULL);
m_pLastNewIns->SetDVar(m_pStackPointer[-1].var);
EmitStind(interpType, m_pVars[var].clsHnd, m_pVars[var].offset, true /* reverseSVarOrder */);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we need the indirect from LoadVar we should have it here too i think

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am not sure what you mean, can you please explain it in more detail?

Comment threadsrc/coreclr/interpreter/compiler.cpp Outdated
Comment threadsrc/coreclr/interpreter/compiler.cpp
Comment threadsrc/coreclr/vm/interpexec.cpp Outdated
Comment threadsrc/coreclr/interpreter/compiler.cpp
Comment threadsrc/coreclr/interpreter/compiler.cpp
@janvorli

Copy link
Copy Markdown
MemberAuthor

There is some problem with the IL to native map generation when running with checked build of the runtime. Debug build works fine. I am looking into it.

The call finally islands were not classified by the clause they are in.
Their instructions had IL offsets set to the offset of the finally
block, but they need to have no IL offset as they are not generated
from IL.
@janvorli

Copy link
Copy Markdown
MemberAuthor

I've fixed the problem with the checked build of the tests. The problem was that the call finally islands didn't have the clauseType set and so when generating native form of the EH info, they were missing in the try regions ranges.

@kg

kg commented May 29, 2025

Copy link
Copy Markdown
Contributor

I forgot to comment when I finished going over this, it all LGTM. I'm fine with landing it.

kg
kg approved these changes May 29, 2025
Comment threadsrc/coreclr/interpreter/compiler.cpp
Comment threadsrc/coreclr/interpreter/compiler.cpp Outdated
Comment threadsrc/coreclr/interpreter/compiler.cpp Outdated
Comment threadsrc/coreclr/interpreter/compiler.cpp
Comment threadsrc/coreclr/vm/interpexec.cpp Outdated
@BrzVlad

Copy link
Copy Markdown
Member

Had this written down after a call a while ago with @janvorli. Might be useful for visualization.

try {
try {
if
leave end2; // INTOP_LEAVE -> jump to finally_island_1a leave end1; // INTOP_LEAVE -> jump to finally_island_1b
} catch {
leave end1; // INTOP_LEAVE -> jump to finally_island_1b
} finally1 {
endfinally
}
-- finally_island_1a --
call_finally finally1
br finally_island_2
--------------------- -- finally_island_1b --
call_finally finally1
br end1
--------------------- end1;
leave end2; // INTOP_LEAVE -> jump to finally_island_2
} catch {
leave end2; // INTOP_LEAVE -> jump to finally_island_2
} finally2 {
endfinally
}
-- finally_island_2 --
call_finally finally2
br end2;
---------------------
end2;

@BrzVlad

Copy link
Copy Markdown
Member

Looks great! Thanks!

@janvorli
janvorli merged commit 8d59055 into dotnet:mainJun 2, 2025
@janvorli
janvorli deleted the interpreter-eh branch June 2, 2025 16:48
@github-actionsgithub-actionsBot locked and limited conversation to collaborators Jul 3, 2025
Sign up for freeto subscribe to this conversation on GitHub. Already have an account? Sign in.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants

@janvorli@kg@BrzVlad@jkotas@SingleAccretion
, '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

Interpreter EH implementation - #116046

Merged
janvorli merged 5 commits into
dotnet:mainfrom
janvorli:interpreter-eh
Jun 2, 2025
Merged

Interpreter EH implementation#116046
janvorli merged 5 commits into
dotnet:mainfrom
janvorli:interpreter-eh

Conversation

@janvorli

Copy link
Copy Markdown
Member

This change implements EH support in the interpreter compiler and execution parts.

Here is a summary of the changes:

On the compilation side:

  • Adds support for CEE_THROW, CEE_RETHROW, CEE_ENDFILTER, CEE_ENDFINALLY, CEE_LEAVE and CEE_ISINST opcodes
  • Adds building of EH info with IR code offsets
  • Implements proper funclet handling in the same way the JIT does. All handlers and filters are moved to the end of the method and out of any try ranges recursively to enable proper behavior of the EH.
  • Fixes a bug related to wrong SVar and and an issue with IL offset of inserted instructions in AllocOffsets
  • Fixes a bug in the CEEOpcodeSize - off by one check for the end of the code

On the execution side:

  • Add funclet start address extraction
  • Add calling funclets in the interpreted code
  • Removed GCX_PREEMP_NO_DTOR from the CallDescrWorkerUnwindFrameChainHandler, because it was not correct
  • Added new IR opcodes:
    • INTOP_LOAD_FRAMEVAR to load parent frame stack pointer in filter funclets that need to run in the context of the parent, but at the top of the current interpreter stack.
    • INTOP_RETHROW to rethrow an exception
    • INTOP_CALL_FINALLY to call finally funclet in non-exceptional cases
    • INTOP_LEAVE_FILTER to exit a filter funclet and return the filter result
    • INTOP_LEAVE_CATCH to exit a catch handler and return the resume address
  • Added calls to COMPlusThrow for division by zero, stack overflow and few other exceptions where the interpreter had just TODO and assert for adding those.
  • Modified the InterpExecMethod so that extra information can be passed in case of a funclet invocation.

It also adds tests to verify various interesting EH scenarios

Here are some more details on moving out the funclets: For each finally funclet, we create a finally call island that stays in the code where the original finally was. That island calls the finally and then branches to the next (outer) finally if any. The last finally call island branches to the actual leave target.
The interpreter compiler generates a separate sequence of finally call islands for each leave instruction target. And when the leave is executed, it jumps to the beginning or into the middle of the chain. In other words, for example, if all leave instructions in the method go to the same target, there would be just one call finally island chain.

@janvorlijanvorli added this to the 10.0.0 milestone May 27, 2025
@janvorlijanvorli self-assigned this May 27, 2025
CopilotAI review requested due to automatic review settings May 27, 2025 22:28
@janvorli
janvorli requested review from BrzVlad and kg as code ownersMay 27, 2025 22:28
@dotnet-policy-service

Copy link
Copy Markdown
Contributor

Tagging subscribers to this area: @BrzVlad, @janvorli, @kg
See info in area-owners.md if you want to be subscribed.

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull Request Overview

This PR adds full exception-handling (EH) support to the interpreter, covering both compilation and execution phases. It introduces new IR opcodes, builds EH metadata, implements funclet-based EH in the interpreter, and adds tests for various EH scenarios.

  • Added support for EH opcodes (throw, rethrow, leave, filters, finally) and IR-level exception clauses
  • Updated interpreter execution (InterpExecMethod) to dispatch filters, catches, and finally blocks
  • Built and emitted EH information (funclet offsets, EH tables) and wired new EH helpers

Reviewed Changes

Copilot reviewed 16 out of 16 changed files in this pull request and generated 2 comments.

Show a summary per file
FileDescription
src/coreclr/vm/prestub.cppIntroduce retVal storage for interpreter return value
src/coreclr/vm/jithelpers.cppSwap in DispatchRethrownManagedException call
src/coreclr/vm/interpexec.hExtend InterpExecMethod signature with EH args
src/coreclr/vm/interpexec.cppHandle new INTOP_* opcodes and null/overflow checks
src/coreclr/vm/exceptionhandling.hDeclare DispatchRethrownManagedException overloads
src/coreclr/vm/exceptionhandling.cppImplement DispatchRethrownManagedException and adjust dispatch logic
src/coreclr/vm/eetwain.cppFlesh out InterpreterCodeManager::CallFunclet
src/coreclr/vm/codeman.hUpdate GetFuncletStartOffsets declaration
src/coreclr/vm/codeman.cppImplement funclet start address/offset enumeration
src/coreclr/interpreter/intops.defAdd EH-related INTOP definitions
src/coreclr/interpreter/intops.cppFix CEEOpcodeSize boundary condition
src/coreclr/interpreter/eeinterp.cppInvoke BuildEHInfo() during compilation
src/coreclr/interpreter/compileropt.cppCorrect ilOffset propagation and SetSVar call
src/coreclr/interpreter/compiler.hExtend compiler IR and block structures for EH
Comments suppressed due to low confidence (3)

src/coreclr/vm/exceptionhandling.cpp:2138

  • Removing the GCX_PREEMP_NO_DTOR switch back to preemptive GC in the non-unwind (search) path can leave the runtime in cooperative mode and break stack walking. Reintroduce a mode switch before returning ExceptionContinueSearch.
else if (IS_UNWINDING(pExceptionRecord->ExceptionFlags))

src/coreclr/vm/jithelpers.cpp:1357

  • The identifier exceptionFrame is not declared in this scope, leading to a compile error. You need to pass a valid CONTEXT* (e.g., capture it or fetch it from the exception tracker) to DispatchRethrownManagedException.
DispatchRethrownManagedException(exceptionFrame.GetContext());

src/coreclr/vm/interpexec.h:77

  • There are now two overloads of InterpExecMethod (one with default ExceptionClauseArgs and the original three-argument version), which can be confusing. Consider removing the three-argument prototype and relying solely on the default parameter.
void InterpExecMethod(InterpreterFrame *pInterpreterFrame, InterpMethodContextFrame *pFrame, InterpThreadContext *pThreadContext, ExceptionClauseArgs *pExceptionClauseArgs = NULL);

Comment threadsrc/coreclr/vm/eetwain.cpp
Comment threadsrc/coreclr/vm/interpexec.cpp
@janvorli
janvorliforce-pushed the interpreter-eh branch 2 times, most recently from aee764e to 02cb97bCompareMay 27, 2025 22:34
This change implements EH support in the interpreter compiler and
execution parts.
Here is a summary of the changes:
On the compilation side:
* Adds support for CEE_THROW, CEE_RETHROW, CEE_ENDFILTER,
CEE_ENDFINALLY, CEE_LEAVE and CEE_ISINST opcodes
* Adds building of EH info with IR code offsets
* Implements proper funclet handling in the same way the JIT does. All
handlers and filters are moved to the end of the method and out of any
try ranges recursively to enable proper behavior of the EH.
* Fixes a bug related to wrong SVar and and an issue with IL offset of
inserted instructions in AllocOffsets
* Fixes a bug in the CEEOpcodeSize - off by one check for the end of the
code
On the execution side:
* Add funclet start address extraction
* Add calling funclets in the interpreted code
* Removed GCX_PREEMP_NO_DTOR from the
CallDescrWorkerUnwindFrameChainHandler, because it was not correct
* Added new IR opcodes:
* INTOP_LOAD_FRAMEVAR to load parent frame stack pointer in filter
funclets that need to run in the context of the parent, but at the
top of the current interpreter stack.
* INTOP_RETHROW to rethrow an exception
* INTOP_CALL_FINALLY to call finally funclet in non-exceptional cases
* INTOP_LEAVE_FILTER to exit a filter funclet and return the filter result
* INTOP_LEAVE_CATCH to exit a catch handler and return the resume
address
* Added calls to COMPlusThrow for division by zero, stack overflow and
few other exceptions where the interpreter had just TODO and assert
for adding those.
* Modified the InterpExecMethod so that extra information can be passed
in case of a funclet invocation.
It also adds tests to verify various interesting EH scenarios
Here are some more details on moving out the funclets:
For each finally funclet, we create a finally call island that stays in
the code where the original finally was. That island calls the finally and
then branches to the next (outer) finally if any. The last finally call
island branches to the actual leave target.
The interpreter compiler generates a separate sequence of finally call
islands for each leave instruction target. And when the leave is
executed, it jumps to the beginning or into the middle of the chain.
In other words, for example, if all leave instructions in the method go
to the same target, there would be just one call finally island chain.
Comment threadsrc/coreclr/interpreter/compiler.cpp Outdated
Comment threadsrc/coreclr/interpreter/compiler.cpp Outdated
Comment threadsrc/coreclr/interpreter/compiler.cpp Outdated
Comment threadsrc/coreclr/interpreter/compiler.cpp
Comment threadsrc/coreclr/interpreter/compiler.cpp
Comment threadsrc/coreclr/interpreter/compiler.cpp
Comment threadsrc/coreclr/interpreter/compiler.cpp
Comment threadsrc/coreclr/interpreter/compiler.cpp
Comment threadsrc/coreclr/interpreter/compiler.cpp
Comment threadsrc/coreclr/interpreter/compiler.cpp
Comment threadsrc/coreclr/interpreter/compiler.cpp
Comment threadsrc/coreclr/interpreter/compiler.cpp
Comment threadsrc/coreclr/interpreter/compiler.cpp Outdated
Comment threadsrc/coreclr/interpreter/compiler.cpp Outdated
Comment threadsrc/coreclr/interpreter/compiler.cpp
Comment threadsrc/coreclr/interpreter/compiler.cpp
Comment threadsrc/coreclr/interpreter/compiler.cpp
Comment threadsrc/coreclr/interpreter/compiler.cpp
Comment threadsrc/coreclr/interpreter/compiler.cpp Outdated
AddIns(INTOP_LOAD_FRAMEVAR);
PushInterpType(InterpTypeI, NULL);
m_pLastNewIns->SetDVar(m_pStackPointer[-1].var);
EmitStind(interpType, m_pVars[var].clsHnd, m_pVars[var].offset, true /* reverseSVarOrder */);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we need the indirect from LoadVar we should have it here too i think

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am not sure what you mean, can you please explain it in more detail?

Comment threadsrc/coreclr/interpreter/compiler.cpp Outdated
Comment threadsrc/coreclr/interpreter/compiler.cpp
Comment threadsrc/coreclr/vm/interpexec.cpp Outdated
Comment threadsrc/coreclr/interpreter/compiler.cpp
Comment threadsrc/coreclr/interpreter/compiler.cpp
@janvorli

Copy link
Copy Markdown
MemberAuthor

There is some problem with the IL to native map generation when running with checked build of the runtime. Debug build works fine. I am looking into it.

The call finally islands were not classified by the clause they are in.
Their instructions had IL offsets set to the offset of the finally
block, but they need to have no IL offset as they are not generated
from IL.
@janvorli

Copy link
Copy Markdown
MemberAuthor

I've fixed the problem with the checked build of the tests. The problem was that the call finally islands didn't have the clauseType set and so when generating native form of the EH info, they were missing in the try regions ranges.

@kg

kg commented May 29, 2025

Copy link
Copy Markdown
Contributor

I forgot to comment when I finished going over this, it all LGTM. I'm fine with landing it.

kg
kg approved these changes May 29, 2025
Comment threadsrc/coreclr/interpreter/compiler.cpp
Comment threadsrc/coreclr/interpreter/compiler.cpp Outdated
Comment threadsrc/coreclr/interpreter/compiler.cpp Outdated
Comment threadsrc/coreclr/interpreter/compiler.cpp
Comment threadsrc/coreclr/vm/interpexec.cpp Outdated
@BrzVlad

Copy link
Copy Markdown
Member

Had this written down after a call a while ago with @janvorli. Might be useful for visualization.

try {
try {
if
leave end2; // INTOP_LEAVE -> jump to finally_island_1a leave end1; // INTOP_LEAVE -> jump to finally_island_1b
} catch {
leave end1; // INTOP_LEAVE -> jump to finally_island_1b
} finally1 {
endfinally
}
-- finally_island_1a --
call_finally finally1
br finally_island_2
--------------------- -- finally_island_1b --
call_finally finally1
br end1
--------------------- end1;
leave end2; // INTOP_LEAVE -> jump to finally_island_2
} catch {
leave end2; // INTOP_LEAVE -> jump to finally_island_2
} finally2 {
endfinally
}
-- finally_island_2 --
call_finally finally2
br end2;
---------------------
end2;

@BrzVlad

Copy link
Copy Markdown
Member

Looks great! Thanks!

@janvorli
janvorli merged commit 8d59055 into dotnet:mainJun 2, 2025
@janvorli
janvorli deleted the interpreter-eh branch June 2, 2025 16:48
@github-actionsgithub-actionsBot locked and limited conversation to collaborators Jul 3, 2025
Sign up for freeto subscribe to this conversation on GitHub. Already have an account? Sign in.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants

@janvorli@kg@BrzVlad@jkotas@SingleAccretion
, '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

Interpreter EH implementation - #116046

Merged
janvorli merged 5 commits into
dotnet:mainfrom
janvorli:interpreter-eh
Jun 2, 2025
Merged

Interpreter EH implementation#116046
janvorli merged 5 commits into
dotnet:mainfrom
janvorli:interpreter-eh

Conversation

@janvorli

Copy link
Copy Markdown
Member

This change implements EH support in the interpreter compiler and execution parts.

Here is a summary of the changes:

On the compilation side:

  • Adds support for CEE_THROW, CEE_RETHROW, CEE_ENDFILTER, CEE_ENDFINALLY, CEE_LEAVE and CEE_ISINST opcodes
  • Adds building of EH info with IR code offsets
  • Implements proper funclet handling in the same way the JIT does. All handlers and filters are moved to the end of the method and out of any try ranges recursively to enable proper behavior of the EH.
  • Fixes a bug related to wrong SVar and and an issue with IL offset of inserted instructions in AllocOffsets
  • Fixes a bug in the CEEOpcodeSize - off by one check for the end of the code

On the execution side:

  • Add funclet start address extraction
  • Add calling funclets in the interpreted code
  • Removed GCX_PREEMP_NO_DTOR from the CallDescrWorkerUnwindFrameChainHandler, because it was not correct
  • Added new IR opcodes:
    • INTOP_LOAD_FRAMEVAR to load parent frame stack pointer in filter funclets that need to run in the context of the parent, but at the top of the current interpreter stack.
    • INTOP_RETHROW to rethrow an exception
    • INTOP_CALL_FINALLY to call finally funclet in non-exceptional cases
    • INTOP_LEAVE_FILTER to exit a filter funclet and return the filter result
    • INTOP_LEAVE_CATCH to exit a catch handler and return the resume address
  • Added calls to COMPlusThrow for division by zero, stack overflow and few other exceptions where the interpreter had just TODO and assert for adding those.
  • Modified the InterpExecMethod so that extra information can be passed in case of a funclet invocation.

It also adds tests to verify various interesting EH scenarios

Here are some more details on moving out the funclets: For each finally funclet, we create a finally call island that stays in the code where the original finally was. That island calls the finally and then branches to the next (outer) finally if any. The last finally call island branches to the actual leave target.
The interpreter compiler generates a separate sequence of finally call islands for each leave instruction target. And when the leave is executed, it jumps to the beginning or into the middle of the chain. In other words, for example, if all leave instructions in the method go to the same target, there would be just one call finally island chain.

@janvorlijanvorli added this to the 10.0.0 milestone May 27, 2025
@janvorlijanvorli self-assigned this May 27, 2025
CopilotAI review requested due to automatic review settings May 27, 2025 22:28
@janvorli
janvorli requested review from BrzVlad and kg as code ownersMay 27, 2025 22:28
@dotnet-policy-service

Copy link
Copy Markdown
Contributor

Tagging subscribers to this area: @BrzVlad, @janvorli, @kg
See info in area-owners.md if you want to be subscribed.

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull Request Overview

This PR adds full exception-handling (EH) support to the interpreter, covering both compilation and execution phases. It introduces new IR opcodes, builds EH metadata, implements funclet-based EH in the interpreter, and adds tests for various EH scenarios.

  • Added support for EH opcodes (throw, rethrow, leave, filters, finally) and IR-level exception clauses
  • Updated interpreter execution (InterpExecMethod) to dispatch filters, catches, and finally blocks
  • Built and emitted EH information (funclet offsets, EH tables) and wired new EH helpers

Reviewed Changes

Copilot reviewed 16 out of 16 changed files in this pull request and generated 2 comments.

Show a summary per file
FileDescription
src/coreclr/vm/prestub.cppIntroduce retVal storage for interpreter return value
src/coreclr/vm/jithelpers.cppSwap in DispatchRethrownManagedException call
src/coreclr/vm/interpexec.hExtend InterpExecMethod signature with EH args
src/coreclr/vm/interpexec.cppHandle new INTOP_* opcodes and null/overflow checks
src/coreclr/vm/exceptionhandling.hDeclare DispatchRethrownManagedException overloads
src/coreclr/vm/exceptionhandling.cppImplement DispatchRethrownManagedException and adjust dispatch logic
src/coreclr/vm/eetwain.cppFlesh out InterpreterCodeManager::CallFunclet
src/coreclr/vm/codeman.hUpdate GetFuncletStartOffsets declaration
src/coreclr/vm/codeman.cppImplement funclet start address/offset enumeration
src/coreclr/interpreter/intops.defAdd EH-related INTOP definitions
src/coreclr/interpreter/intops.cppFix CEEOpcodeSize boundary condition
src/coreclr/interpreter/eeinterp.cppInvoke BuildEHInfo() during compilation
src/coreclr/interpreter/compileropt.cppCorrect ilOffset propagation and SetSVar call
src/coreclr/interpreter/compiler.hExtend compiler IR and block structures for EH
Comments suppressed due to low confidence (3)

src/coreclr/vm/exceptionhandling.cpp:2138

  • Removing the GCX_PREEMP_NO_DTOR switch back to preemptive GC in the non-unwind (search) path can leave the runtime in cooperative mode and break stack walking. Reintroduce a mode switch before returning ExceptionContinueSearch.
else if (IS_UNWINDING(pExceptionRecord->ExceptionFlags))

src/coreclr/vm/jithelpers.cpp:1357

  • The identifier exceptionFrame is not declared in this scope, leading to a compile error. You need to pass a valid CONTEXT* (e.g., capture it or fetch it from the exception tracker) to DispatchRethrownManagedException.
DispatchRethrownManagedException(exceptionFrame.GetContext());

src/coreclr/vm/interpexec.h:77

  • There are now two overloads of InterpExecMethod (one with default ExceptionClauseArgs and the original three-argument version), which can be confusing. Consider removing the three-argument prototype and relying solely on the default parameter.
void InterpExecMethod(InterpreterFrame *pInterpreterFrame, InterpMethodContextFrame *pFrame, InterpThreadContext *pThreadContext, ExceptionClauseArgs *pExceptionClauseArgs = NULL);

Comment threadsrc/coreclr/vm/eetwain.cpp
Comment threadsrc/coreclr/vm/interpexec.cpp
@janvorli
janvorliforce-pushed the interpreter-eh branch 2 times, most recently from aee764e to 02cb97bCompareMay 27, 2025 22:34
This change implements EH support in the interpreter compiler and
execution parts.
Here is a summary of the changes:
On the compilation side:
* Adds support for CEE_THROW, CEE_RETHROW, CEE_ENDFILTER,
CEE_ENDFINALLY, CEE_LEAVE and CEE_ISINST opcodes
* Adds building of EH info with IR code offsets
* Implements proper funclet handling in the same way the JIT does. All
handlers and filters are moved to the end of the method and out of any
try ranges recursively to enable proper behavior of the EH.
* Fixes a bug related to wrong SVar and and an issue with IL offset of
inserted instructions in AllocOffsets
* Fixes a bug in the CEEOpcodeSize - off by one check for the end of the
code
On the execution side:
* Add funclet start address extraction
* Add calling funclets in the interpreted code
* Removed GCX_PREEMP_NO_DTOR from the
CallDescrWorkerUnwindFrameChainHandler, because it was not correct
* Added new IR opcodes:
* INTOP_LOAD_FRAMEVAR to load parent frame stack pointer in filter
funclets that need to run in the context of the parent, but at the
top of the current interpreter stack.
* INTOP_RETHROW to rethrow an exception
* INTOP_CALL_FINALLY to call finally funclet in non-exceptional cases
* INTOP_LEAVE_FILTER to exit a filter funclet and return the filter result
* INTOP_LEAVE_CATCH to exit a catch handler and return the resume
address
* Added calls to COMPlusThrow for division by zero, stack overflow and
few other exceptions where the interpreter had just TODO and assert
for adding those.
* Modified the InterpExecMethod so that extra information can be passed
in case of a funclet invocation.
It also adds tests to verify various interesting EH scenarios
Here are some more details on moving out the funclets:
For each finally funclet, we create a finally call island that stays in
the code where the original finally was. That island calls the finally and
then branches to the next (outer) finally if any. The last finally call
island branches to the actual leave target.
The interpreter compiler generates a separate sequence of finally call
islands for each leave instruction target. And when the leave is
executed, it jumps to the beginning or into the middle of the chain.
In other words, for example, if all leave instructions in the method go
to the same target, there would be just one call finally island chain.
Comment threadsrc/coreclr/interpreter/compiler.cpp Outdated
Comment threadsrc/coreclr/interpreter/compiler.cpp Outdated
Comment threadsrc/coreclr/interpreter/compiler.cpp Outdated
Comment threadsrc/coreclr/interpreter/compiler.cpp
Comment threadsrc/coreclr/interpreter/compiler.cpp
Comment threadsrc/coreclr/interpreter/compiler.cpp
Comment threadsrc/coreclr/interpreter/compiler.cpp
Comment threadsrc/coreclr/interpreter/compiler.cpp
Comment threadsrc/coreclr/interpreter/compiler.cpp
Comment threadsrc/coreclr/interpreter/compiler.cpp
Comment threadsrc/coreclr/interpreter/compiler.cpp
Comment threadsrc/coreclr/interpreter/compiler.cpp
Comment threadsrc/coreclr/interpreter/compiler.cpp Outdated
Comment threadsrc/coreclr/interpreter/compiler.cpp Outdated
Comment threadsrc/coreclr/interpreter/compiler.cpp
Comment threadsrc/coreclr/interpreter/compiler.cpp
Comment threadsrc/coreclr/interpreter/compiler.cpp
Comment threadsrc/coreclr/interpreter/compiler.cpp
Comment threadsrc/coreclr/interpreter/compiler.cpp Outdated
AddIns(INTOP_LOAD_FRAMEVAR);
PushInterpType(InterpTypeI, NULL);
m_pLastNewIns->SetDVar(m_pStackPointer[-1].var);
EmitStind(interpType, m_pVars[var].clsHnd, m_pVars[var].offset, true /* reverseSVarOrder */);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we need the indirect from LoadVar we should have it here too i think

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am not sure what you mean, can you please explain it in more detail?

Comment threadsrc/coreclr/interpreter/compiler.cpp Outdated
Comment threadsrc/coreclr/interpreter/compiler.cpp
Comment threadsrc/coreclr/vm/interpexec.cpp Outdated
Comment threadsrc/coreclr/interpreter/compiler.cpp
Comment threadsrc/coreclr/interpreter/compiler.cpp
@janvorli

Copy link
Copy Markdown
MemberAuthor

There is some problem with the IL to native map generation when running with checked build of the runtime. Debug build works fine. I am looking into it.

The call finally islands were not classified by the clause they are in.
Their instructions had IL offsets set to the offset of the finally
block, but they need to have no IL offset as they are not generated
from IL.
@janvorli

Copy link
Copy Markdown
MemberAuthor

I've fixed the problem with the checked build of the tests. The problem was that the call finally islands didn't have the clauseType set and so when generating native form of the EH info, they were missing in the try regions ranges.

@kg

kg commented May 29, 2025

Copy link
Copy Markdown
Contributor

I forgot to comment when I finished going over this, it all LGTM. I'm fine with landing it.

kg
kg approved these changes May 29, 2025
Comment threadsrc/coreclr/interpreter/compiler.cpp
Comment threadsrc/coreclr/interpreter/compiler.cpp Outdated
Comment threadsrc/coreclr/interpreter/compiler.cpp Outdated
Comment threadsrc/coreclr/interpreter/compiler.cpp
Comment threadsrc/coreclr/vm/interpexec.cpp Outdated
@BrzVlad

Copy link
Copy Markdown
Member

Had this written down after a call a while ago with @janvorli. Might be useful for visualization.

try {
try {
if
leave end2; // INTOP_LEAVE -> jump to finally_island_1a leave end1; // INTOP_LEAVE -> jump to finally_island_1b
} catch {
leave end1; // INTOP_LEAVE -> jump to finally_island_1b
} finally1 {
endfinally
}
-- finally_island_1a --
call_finally finally1
br finally_island_2
--------------------- -- finally_island_1b --
call_finally finally1
br end1
--------------------- end1;
leave end2; // INTOP_LEAVE -> jump to finally_island_2
} catch {
leave end2; // INTOP_LEAVE -> jump to finally_island_2
} finally2 {
endfinally
}
-- finally_island_2 --
call_finally finally2
br end2;
---------------------
end2;

@BrzVlad

Copy link
Copy Markdown
Member

Looks great! Thanks!

@janvorli
janvorli merged commit 8d59055 into dotnet:mainJun 2, 2025
@janvorli
janvorli deleted the interpreter-eh branch June 2, 2025 16:48
@github-actionsgithub-actionsBot locked and limited conversation to collaborators Jul 3, 2025
Sign up for freeto subscribe to this conversation on GitHub. Already have an account? Sign in.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants

@janvorli@kg@BrzVlad@jkotas@SingleAccretion
, '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

Interpreter EH implementation - #116046

Merged
janvorli merged 5 commits into
dotnet:mainfrom
janvorli:interpreter-eh
Jun 2, 2025
Merged

Interpreter EH implementation#116046
janvorli merged 5 commits into
dotnet:mainfrom
janvorli:interpreter-eh

Conversation

@janvorli

Copy link
Copy Markdown
Member

This change implements EH support in the interpreter compiler and execution parts.

Here is a summary of the changes:

On the compilation side:

  • Adds support for CEE_THROW, CEE_RETHROW, CEE_ENDFILTER, CEE_ENDFINALLY, CEE_LEAVE and CEE_ISINST opcodes
  • Adds building of EH info with IR code offsets
  • Implements proper funclet handling in the same way the JIT does. All handlers and filters are moved to the end of the method and out of any try ranges recursively to enable proper behavior of the EH.
  • Fixes a bug related to wrong SVar and and an issue with IL offset of inserted instructions in AllocOffsets
  • Fixes a bug in the CEEOpcodeSize - off by one check for the end of the code

On the execution side:

  • Add funclet start address extraction
  • Add calling funclets in the interpreted code
  • Removed GCX_PREEMP_NO_DTOR from the CallDescrWorkerUnwindFrameChainHandler, because it was not correct
  • Added new IR opcodes:
    • INTOP_LOAD_FRAMEVAR to load parent frame stack pointer in filter funclets that need to run in the context of the parent, but at the top of the current interpreter stack.
    • INTOP_RETHROW to rethrow an exception
    • INTOP_CALL_FINALLY to call finally funclet in non-exceptional cases
    • INTOP_LEAVE_FILTER to exit a filter funclet and return the filter result
    • INTOP_LEAVE_CATCH to exit a catch handler and return the resume address
  • Added calls to COMPlusThrow for division by zero, stack overflow and few other exceptions where the interpreter had just TODO and assert for adding those.
  • Modified the InterpExecMethod so that extra information can be passed in case of a funclet invocation.

It also adds tests to verify various interesting EH scenarios

Here are some more details on moving out the funclets: For each finally funclet, we create a finally call island that stays in the code where the original finally was. That island calls the finally and then branches to the next (outer) finally if any. The last finally call island branches to the actual leave target.
The interpreter compiler generates a separate sequence of finally call islands for each leave instruction target. And when the leave is executed, it jumps to the beginning or into the middle of the chain. In other words, for example, if all leave instructions in the method go to the same target, there would be just one call finally island chain.

@janvorlijanvorli added this to the 10.0.0 milestone May 27, 2025
@janvorlijanvorli self-assigned this May 27, 2025
CopilotAI review requested due to automatic review settings May 27, 2025 22:28
@janvorli
janvorli requested review from BrzVlad and kg as code ownersMay 27, 2025 22:28
@dotnet-policy-service

Copy link
Copy Markdown
Contributor

Tagging subscribers to this area: @BrzVlad, @janvorli, @kg
See info in area-owners.md if you want to be subscribed.

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull Request Overview

This PR adds full exception-handling (EH) support to the interpreter, covering both compilation and execution phases. It introduces new IR opcodes, builds EH metadata, implements funclet-based EH in the interpreter, and adds tests for various EH scenarios.

  • Added support for EH opcodes (throw, rethrow, leave, filters, finally) and IR-level exception clauses
  • Updated interpreter execution (InterpExecMethod) to dispatch filters, catches, and finally blocks
  • Built and emitted EH information (funclet offsets, EH tables) and wired new EH helpers

Reviewed Changes

Copilot reviewed 16 out of 16 changed files in this pull request and generated 2 comments.

Show a summary per file
FileDescription
src/coreclr/vm/prestub.cppIntroduce retVal storage for interpreter return value
src/coreclr/vm/jithelpers.cppSwap in DispatchRethrownManagedException call
src/coreclr/vm/interpexec.hExtend InterpExecMethod signature with EH args
src/coreclr/vm/interpexec.cppHandle new INTOP_* opcodes and null/overflow checks
src/coreclr/vm/exceptionhandling.hDeclare DispatchRethrownManagedException overloads
src/coreclr/vm/exceptionhandling.cppImplement DispatchRethrownManagedException and adjust dispatch logic
src/coreclr/vm/eetwain.cppFlesh out InterpreterCodeManager::CallFunclet
src/coreclr/vm/codeman.hUpdate GetFuncletStartOffsets declaration
src/coreclr/vm/codeman.cppImplement funclet start address/offset enumeration
src/coreclr/interpreter/intops.defAdd EH-related INTOP definitions
src/coreclr/interpreter/intops.cppFix CEEOpcodeSize boundary condition
src/coreclr/interpreter/eeinterp.cppInvoke BuildEHInfo() during compilation
src/coreclr/interpreter/compileropt.cppCorrect ilOffset propagation and SetSVar call
src/coreclr/interpreter/compiler.hExtend compiler IR and block structures for EH
Comments suppressed due to low confidence (3)

src/coreclr/vm/exceptionhandling.cpp:2138

  • Removing the GCX_PREEMP_NO_DTOR switch back to preemptive GC in the non-unwind (search) path can leave the runtime in cooperative mode and break stack walking. Reintroduce a mode switch before returning ExceptionContinueSearch.
else if (IS_UNWINDING(pExceptionRecord->ExceptionFlags))

src/coreclr/vm/jithelpers.cpp:1357

  • The identifier exceptionFrame is not declared in this scope, leading to a compile error. You need to pass a valid CONTEXT* (e.g., capture it or fetch it from the exception tracker) to DispatchRethrownManagedException.
DispatchRethrownManagedException(exceptionFrame.GetContext());

src/coreclr/vm/interpexec.h:77

  • There are now two overloads of InterpExecMethod (one with default ExceptionClauseArgs and the original three-argument version), which can be confusing. Consider removing the three-argument prototype and relying solely on the default parameter.
void InterpExecMethod(InterpreterFrame *pInterpreterFrame, InterpMethodContextFrame *pFrame, InterpThreadContext *pThreadContext, ExceptionClauseArgs *pExceptionClauseArgs = NULL);

Comment threadsrc/coreclr/vm/eetwain.cpp
Comment threadsrc/coreclr/vm/interpexec.cpp
@janvorli
janvorliforce-pushed the interpreter-eh branch 2 times, most recently from aee764e to 02cb97bCompareMay 27, 2025 22:34
This change implements EH support in the interpreter compiler and
execution parts.
Here is a summary of the changes:
On the compilation side:
* Adds support for CEE_THROW, CEE_RETHROW, CEE_ENDFILTER,
CEE_ENDFINALLY, CEE_LEAVE and CEE_ISINST opcodes
* Adds building of EH info with IR code offsets
* Implements proper funclet handling in the same way the JIT does. All
handlers and filters are moved to the end of the method and out of any
try ranges recursively to enable proper behavior of the EH.
* Fixes a bug related to wrong SVar and and an issue with IL offset of
inserted instructions in AllocOffsets
* Fixes a bug in the CEEOpcodeSize - off by one check for the end of the
code
On the execution side:
* Add funclet start address extraction
* Add calling funclets in the interpreted code
* Removed GCX_PREEMP_NO_DTOR from the
CallDescrWorkerUnwindFrameChainHandler, because it was not correct
* Added new IR opcodes:
* INTOP_LOAD_FRAMEVAR to load parent frame stack pointer in filter
funclets that need to run in the context of the parent, but at the
top of the current interpreter stack.
* INTOP_RETHROW to rethrow an exception
* INTOP_CALL_FINALLY to call finally funclet in non-exceptional cases
* INTOP_LEAVE_FILTER to exit a filter funclet and return the filter result
* INTOP_LEAVE_CATCH to exit a catch handler and return the resume
address
* Added calls to COMPlusThrow for division by zero, stack overflow and
few other exceptions where the interpreter had just TODO and assert
for adding those.
* Modified the InterpExecMethod so that extra information can be passed
in case of a funclet invocation.
It also adds tests to verify various interesting EH scenarios
Here are some more details on moving out the funclets:
For each finally funclet, we create a finally call island that stays in
the code where the original finally was. That island calls the finally and
then branches to the next (outer) finally if any. The last finally call
island branches to the actual leave target.
The interpreter compiler generates a separate sequence of finally call
islands for each leave instruction target. And when the leave is
executed, it jumps to the beginning or into the middle of the chain.
In other words, for example, if all leave instructions in the method go
to the same target, there would be just one call finally island chain.
Comment threadsrc/coreclr/interpreter/compiler.cpp Outdated
Comment threadsrc/coreclr/interpreter/compiler.cpp Outdated
Comment threadsrc/coreclr/interpreter/compiler.cpp Outdated
Comment threadsrc/coreclr/interpreter/compiler.cpp
Comment threadsrc/coreclr/interpreter/compiler.cpp
Comment threadsrc/coreclr/interpreter/compiler.cpp
Comment threadsrc/coreclr/interpreter/compiler.cpp
Comment threadsrc/coreclr/interpreter/compiler.cpp
Comment threadsrc/coreclr/interpreter/compiler.cpp
Comment threadsrc/coreclr/interpreter/compiler.cpp
Comment threadsrc/coreclr/interpreter/compiler.cpp
Comment threadsrc/coreclr/interpreter/compiler.cpp
Comment threadsrc/coreclr/interpreter/compiler.cpp Outdated
Comment threadsrc/coreclr/interpreter/compiler.cpp Outdated
Comment threadsrc/coreclr/interpreter/compiler.cpp
Comment threadsrc/coreclr/interpreter/compiler.cpp
Comment threadsrc/coreclr/interpreter/compiler.cpp
Comment threadsrc/coreclr/interpreter/compiler.cpp
Comment threadsrc/coreclr/interpreter/compiler.cpp Outdated
AddIns(INTOP_LOAD_FRAMEVAR);
PushInterpType(InterpTypeI, NULL);
m_pLastNewIns->SetDVar(m_pStackPointer[-1].var);
EmitStind(interpType, m_pVars[var].clsHnd, m_pVars[var].offset, true /* reverseSVarOrder */);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we need the indirect from LoadVar we should have it here too i think

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am not sure what you mean, can you please explain it in more detail?

Comment threadsrc/coreclr/interpreter/compiler.cpp Outdated
Comment threadsrc/coreclr/interpreter/compiler.cpp
Comment threadsrc/coreclr/vm/interpexec.cpp Outdated
Comment threadsrc/coreclr/interpreter/compiler.cpp
Comment threadsrc/coreclr/interpreter/compiler.cpp
@janvorli

Copy link
Copy Markdown
MemberAuthor

There is some problem with the IL to native map generation when running with checked build of the runtime. Debug build works fine. I am looking into it.

The call finally islands were not classified by the clause they are in.
Their instructions had IL offsets set to the offset of the finally
block, but they need to have no IL offset as they are not generated
from IL.
@janvorli

Copy link
Copy Markdown
MemberAuthor

I've fixed the problem with the checked build of the tests. The problem was that the call finally islands didn't have the clauseType set and so when generating native form of the EH info, they were missing in the try regions ranges.

@kg

kg commented May 29, 2025

Copy link
Copy Markdown
Contributor

I forgot to comment when I finished going over this, it all LGTM. I'm fine with landing it.

kg
kg approved these changes May 29, 2025
Comment threadsrc/coreclr/interpreter/compiler.cpp
Comment threadsrc/coreclr/interpreter/compiler.cpp Outdated
Comment threadsrc/coreclr/interpreter/compiler.cpp Outdated
Comment threadsrc/coreclr/interpreter/compiler.cpp
Comment threadsrc/coreclr/vm/interpexec.cpp Outdated
@BrzVlad

Copy link
Copy Markdown
Member

Had this written down after a call a while ago with @janvorli. Might be useful for visualization.

try {
try {
if
leave end2; // INTOP_LEAVE -> jump to finally_island_1a leave end1; // INTOP_LEAVE -> jump to finally_island_1b
} catch {
leave end1; // INTOP_LEAVE -> jump to finally_island_1b
} finally1 {
endfinally
}
-- finally_island_1a --
call_finally finally1
br finally_island_2
--------------------- -- finally_island_1b --
call_finally finally1
br end1
--------------------- end1;
leave end2; // INTOP_LEAVE -> jump to finally_island_2
} catch {
leave end2; // INTOP_LEAVE -> jump to finally_island_2
} finally2 {
endfinally
}
-- finally_island_2 --
call_finally finally2
br end2;
---------------------
end2;

@BrzVlad

Copy link
Copy Markdown
Member

Looks great! Thanks!

@janvorli
janvorli merged commit 8d59055 into dotnet:mainJun 2, 2025
@janvorli
janvorli deleted the interpreter-eh branch June 2, 2025 16:48
@github-actionsgithub-actionsBot locked and limited conversation to collaborators Jul 3, 2025
Sign up for freeto subscribe to this conversation on GitHub. Already have an account? Sign in.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants

@janvorli@kg@BrzVlad@jkotas@SingleAccretion
, '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

Interpreter EH implementation - #116046

Merged
janvorli merged 5 commits into
dotnet:mainfrom
janvorli:interpreter-eh
Jun 2, 2025
Merged

Interpreter EH implementation#116046
janvorli merged 5 commits into
dotnet:mainfrom
janvorli:interpreter-eh

Conversation

@janvorli

Copy link
Copy Markdown
Member

This change implements EH support in the interpreter compiler and execution parts.

Here is a summary of the changes:

On the compilation side:

  • Adds support for CEE_THROW, CEE_RETHROW, CEE_ENDFILTER, CEE_ENDFINALLY, CEE_LEAVE and CEE_ISINST opcodes
  • Adds building of EH info with IR code offsets
  • Implements proper funclet handling in the same way the JIT does. All handlers and filters are moved to the end of the method and out of any try ranges recursively to enable proper behavior of the EH.
  • Fixes a bug related to wrong SVar and and an issue with IL offset of inserted instructions in AllocOffsets
  • Fixes a bug in the CEEOpcodeSize - off by one check for the end of the code

On the execution side:

  • Add funclet start address extraction
  • Add calling funclets in the interpreted code
  • Removed GCX_PREEMP_NO_DTOR from the CallDescrWorkerUnwindFrameChainHandler, because it was not correct
  • Added new IR opcodes:
    • INTOP_LOAD_FRAMEVAR to load parent frame stack pointer in filter funclets that need to run in the context of the parent, but at the top of the current interpreter stack.
    • INTOP_RETHROW to rethrow an exception
    • INTOP_CALL_FINALLY to call finally funclet in non-exceptional cases
    • INTOP_LEAVE_FILTER to exit a filter funclet and return the filter result
    • INTOP_LEAVE_CATCH to exit a catch handler and return the resume address
  • Added calls to COMPlusThrow for division by zero, stack overflow and few other exceptions where the interpreter had just TODO and assert for adding those.
  • Modified the InterpExecMethod so that extra information can be passed in case of a funclet invocation.

It also adds tests to verify various interesting EH scenarios

Here are some more details on moving out the funclets: For each finally funclet, we create a finally call island that stays in the code where the original finally was. That island calls the finally and then branches to the next (outer) finally if any. The last finally call island branches to the actual leave target.
The interpreter compiler generates a separate sequence of finally call islands for each leave instruction target. And when the leave is executed, it jumps to the beginning or into the middle of the chain. In other words, for example, if all leave instructions in the method go to the same target, there would be just one call finally island chain.

@janvorlijanvorli added this to the 10.0.0 milestone May 27, 2025
@janvorlijanvorli self-assigned this May 27, 2025
CopilotAI review requested due to automatic review settings May 27, 2025 22:28
@janvorli
janvorli requested review from BrzVlad and kg as code ownersMay 27, 2025 22:28
@dotnet-policy-service

Copy link
Copy Markdown
Contributor

Tagging subscribers to this area: @BrzVlad, @janvorli, @kg
See info in area-owners.md if you want to be subscribed.

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull Request Overview

This PR adds full exception-handling (EH) support to the interpreter, covering both compilation and execution phases. It introduces new IR opcodes, builds EH metadata, implements funclet-based EH in the interpreter, and adds tests for various EH scenarios.

  • Added support for EH opcodes (throw, rethrow, leave, filters, finally) and IR-level exception clauses
  • Updated interpreter execution (InterpExecMethod) to dispatch filters, catches, and finally blocks
  • Built and emitted EH information (funclet offsets, EH tables) and wired new EH helpers

Reviewed Changes

Copilot reviewed 16 out of 16 changed files in this pull request and generated 2 comments.

Show a summary per file
FileDescription
src/coreclr/vm/prestub.cppIntroduce retVal storage for interpreter return value
src/coreclr/vm/jithelpers.cppSwap in DispatchRethrownManagedException call
src/coreclr/vm/interpexec.hExtend InterpExecMethod signature with EH args
src/coreclr/vm/interpexec.cppHandle new INTOP_* opcodes and null/overflow checks
src/coreclr/vm/exceptionhandling.hDeclare DispatchRethrownManagedException overloads
src/coreclr/vm/exceptionhandling.cppImplement DispatchRethrownManagedException and adjust dispatch logic
src/coreclr/vm/eetwain.cppFlesh out InterpreterCodeManager::CallFunclet
src/coreclr/vm/codeman.hUpdate GetFuncletStartOffsets declaration
src/coreclr/vm/codeman.cppImplement funclet start address/offset enumeration
src/coreclr/interpreter/intops.defAdd EH-related INTOP definitions
src/coreclr/interpreter/intops.cppFix CEEOpcodeSize boundary condition
src/coreclr/interpreter/eeinterp.cppInvoke BuildEHInfo() during compilation
src/coreclr/interpreter/compileropt.cppCorrect ilOffset propagation and SetSVar call
src/coreclr/interpreter/compiler.hExtend compiler IR and block structures for EH
Comments suppressed due to low confidence (3)

src/coreclr/vm/exceptionhandling.cpp:2138

  • Removing the GCX_PREEMP_NO_DTOR switch back to preemptive GC in the non-unwind (search) path can leave the runtime in cooperative mode and break stack walking. Reintroduce a mode switch before returning ExceptionContinueSearch.
else if (IS_UNWINDING(pExceptionRecord->ExceptionFlags))

src/coreclr/vm/jithelpers.cpp:1357

  • The identifier exceptionFrame is not declared in this scope, leading to a compile error. You need to pass a valid CONTEXT* (e.g., capture it or fetch it from the exception tracker) to DispatchRethrownManagedException.
DispatchRethrownManagedException(exceptionFrame.GetContext());

src/coreclr/vm/interpexec.h:77

  • There are now two overloads of InterpExecMethod (one with default ExceptionClauseArgs and the original three-argument version), which can be confusing. Consider removing the three-argument prototype and relying solely on the default parameter.
void InterpExecMethod(InterpreterFrame *pInterpreterFrame, InterpMethodContextFrame *pFrame, InterpThreadContext *pThreadContext, ExceptionClauseArgs *pExceptionClauseArgs = NULL);

Comment threadsrc/coreclr/vm/eetwain.cpp
Comment threadsrc/coreclr/vm/interpexec.cpp
@janvorli
janvorliforce-pushed the interpreter-eh branch 2 times, most recently from aee764e to 02cb97bCompareMay 27, 2025 22:34
This change implements EH support in the interpreter compiler and
execution parts.
Here is a summary of the changes:
On the compilation side:
* Adds support for CEE_THROW, CEE_RETHROW, CEE_ENDFILTER,
CEE_ENDFINALLY, CEE_LEAVE and CEE_ISINST opcodes
* Adds building of EH info with IR code offsets
* Implements proper funclet handling in the same way the JIT does. All
handlers and filters are moved to the end of the method and out of any
try ranges recursively to enable proper behavior of the EH.
* Fixes a bug related to wrong SVar and and an issue with IL offset of
inserted instructions in AllocOffsets
* Fixes a bug in the CEEOpcodeSize - off by one check for the end of the
code
On the execution side:
* Add funclet start address extraction
* Add calling funclets in the interpreted code
* Removed GCX_PREEMP_NO_DTOR from the
CallDescrWorkerUnwindFrameChainHandler, because it was not correct
* Added new IR opcodes:
* INTOP_LOAD_FRAMEVAR to load parent frame stack pointer in filter
funclets that need to run in the context of the parent, but at the
top of the current interpreter stack.
* INTOP_RETHROW to rethrow an exception
* INTOP_CALL_FINALLY to call finally funclet in non-exceptional cases
* INTOP_LEAVE_FILTER to exit a filter funclet and return the filter result
* INTOP_LEAVE_CATCH to exit a catch handler and return the resume
address
* Added calls to COMPlusThrow for division by zero, stack overflow and
few other exceptions where the interpreter had just TODO and assert
for adding those.
* Modified the InterpExecMethod so that extra information can be passed
in case of a funclet invocation.
It also adds tests to verify various interesting EH scenarios
Here are some more details on moving out the funclets:
For each finally funclet, we create a finally call island that stays in
the code where the original finally was. That island calls the finally and
then branches to the next (outer) finally if any. The last finally call
island branches to the actual leave target.
The interpreter compiler generates a separate sequence of finally call
islands for each leave instruction target. And when the leave is
executed, it jumps to the beginning or into the middle of the chain.
In other words, for example, if all leave instructions in the method go
to the same target, there would be just one call finally island chain.
Comment threadsrc/coreclr/interpreter/compiler.cpp Outdated
Comment threadsrc/coreclr/interpreter/compiler.cpp Outdated
Comment threadsrc/coreclr/interpreter/compiler.cpp Outdated
Comment threadsrc/coreclr/interpreter/compiler.cpp
Comment threadsrc/coreclr/interpreter/compiler.cpp
Comment threadsrc/coreclr/interpreter/compiler.cpp
Comment threadsrc/coreclr/interpreter/compiler.cpp
Comment threadsrc/coreclr/interpreter/compiler.cpp
Comment threadsrc/coreclr/interpreter/compiler.cpp
Comment threadsrc/coreclr/interpreter/compiler.cpp
Comment threadsrc/coreclr/interpreter/compiler.cpp
Comment threadsrc/coreclr/interpreter/compiler.cpp
Comment threadsrc/coreclr/interpreter/compiler.cpp Outdated
Comment threadsrc/coreclr/interpreter/compiler.cpp Outdated
Comment threadsrc/coreclr/interpreter/compiler.cpp
Comment threadsrc/coreclr/interpreter/compiler.cpp
Comment threadsrc/coreclr/interpreter/compiler.cpp
Comment threadsrc/coreclr/interpreter/compiler.cpp
Comment threadsrc/coreclr/interpreter/compiler.cpp Outdated
AddIns(INTOP_LOAD_FRAMEVAR);
PushInterpType(InterpTypeI, NULL);
m_pLastNewIns->SetDVar(m_pStackPointer[-1].var);
EmitStind(interpType, m_pVars[var].clsHnd, m_pVars[var].offset, true /* reverseSVarOrder */);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we need the indirect from LoadVar we should have it here too i think

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am not sure what you mean, can you please explain it in more detail?

Comment threadsrc/coreclr/interpreter/compiler.cpp Outdated
Comment threadsrc/coreclr/interpreter/compiler.cpp
Comment threadsrc/coreclr/vm/interpexec.cpp Outdated
Comment threadsrc/coreclr/interpreter/compiler.cpp
Comment threadsrc/coreclr/interpreter/compiler.cpp
@janvorli

Copy link
Copy Markdown
MemberAuthor

There is some problem with the IL to native map generation when running with checked build of the runtime. Debug build works fine. I am looking into it.

The call finally islands were not classified by the clause they are in.
Their instructions had IL offsets set to the offset of the finally
block, but they need to have no IL offset as they are not generated
from IL.
@janvorli

Copy link
Copy Markdown
MemberAuthor

I've fixed the problem with the checked build of the tests. The problem was that the call finally islands didn't have the clauseType set and so when generating native form of the EH info, they were missing in the try regions ranges.

@kg

kg commented May 29, 2025

Copy link
Copy Markdown
Contributor

I forgot to comment when I finished going over this, it all LGTM. I'm fine with landing it.

kg
kg approved these changes May 29, 2025
Comment threadsrc/coreclr/interpreter/compiler.cpp
Comment threadsrc/coreclr/interpreter/compiler.cpp Outdated
Comment threadsrc/coreclr/interpreter/compiler.cpp Outdated
Comment threadsrc/coreclr/interpreter/compiler.cpp
Comment threadsrc/coreclr/vm/interpexec.cpp Outdated
@BrzVlad

Copy link
Copy Markdown
Member

Had this written down after a call a while ago with @janvorli. Might be useful for visualization.

try {
try {
if
leave end2; // INTOP_LEAVE -> jump to finally_island_1a leave end1; // INTOP_LEAVE -> jump to finally_island_1b
} catch {
leave end1; // INTOP_LEAVE -> jump to finally_island_1b
} finally1 {
endfinally
}
-- finally_island_1a --
call_finally finally1
br finally_island_2
--------------------- -- finally_island_1b --
call_finally finally1
br end1
--------------------- end1;
leave end2; // INTOP_LEAVE -> jump to finally_island_2
} catch {
leave end2; // INTOP_LEAVE -> jump to finally_island_2
} finally2 {
endfinally
}
-- finally_island_2 --
call_finally finally2
br end2;
---------------------
end2;

@BrzVlad

Copy link
Copy Markdown
Member

Looks great! Thanks!

@janvorli
janvorli merged commit 8d59055 into dotnet:mainJun 2, 2025
@janvorli
janvorli deleted the interpreter-eh branch June 2, 2025 16:48
@github-actionsgithub-actionsBot locked and limited conversation to collaborators Jul 3, 2025
Sign up for freeto subscribe to this conversation on GitHub. Already have an account? Sign in.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants

@janvorli@kg@BrzVlad@jkotas@SingleAccretion
, '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

Interpreter EH implementation - #116046

Merged
janvorli merged 5 commits into
dotnet:mainfrom
janvorli:interpreter-eh
Jun 2, 2025
Merged

Interpreter EH implementation#116046
janvorli merged 5 commits into
dotnet:mainfrom
janvorli:interpreter-eh

Conversation

@janvorli

Copy link
Copy Markdown
Member

This change implements EH support in the interpreter compiler and execution parts.

Here is a summary of the changes:

On the compilation side:

  • Adds support for CEE_THROW, CEE_RETHROW, CEE_ENDFILTER, CEE_ENDFINALLY, CEE_LEAVE and CEE_ISINST opcodes
  • Adds building of EH info with IR code offsets
  • Implements proper funclet handling in the same way the JIT does. All handlers and filters are moved to the end of the method and out of any try ranges recursively to enable proper behavior of the EH.
  • Fixes a bug related to wrong SVar and and an issue with IL offset of inserted instructions in AllocOffsets
  • Fixes a bug in the CEEOpcodeSize - off by one check for the end of the code

On the execution side:

  • Add funclet start address extraction
  • Add calling funclets in the interpreted code
  • Removed GCX_PREEMP_NO_DTOR from the CallDescrWorkerUnwindFrameChainHandler, because it was not correct
  • Added new IR opcodes:
    • INTOP_LOAD_FRAMEVAR to load parent frame stack pointer in filter funclets that need to run in the context of the parent, but at the top of the current interpreter stack.
    • INTOP_RETHROW to rethrow an exception
    • INTOP_CALL_FINALLY to call finally funclet in non-exceptional cases
    • INTOP_LEAVE_FILTER to exit a filter funclet and return the filter result
    • INTOP_LEAVE_CATCH to exit a catch handler and return the resume address
  • Added calls to COMPlusThrow for division by zero, stack overflow and few other exceptions where the interpreter had just TODO and assert for adding those.
  • Modified the InterpExecMethod so that extra information can be passed in case of a funclet invocation.

It also adds tests to verify various interesting EH scenarios

Here are some more details on moving out the funclets: For each finally funclet, we create a finally call island that stays in the code where the original finally was. That island calls the finally and then branches to the next (outer) finally if any. The last finally call island branches to the actual leave target.
The interpreter compiler generates a separate sequence of finally call islands for each leave instruction target. And when the leave is executed, it jumps to the beginning or into the middle of the chain. In other words, for example, if all leave instructions in the method go to the same target, there would be just one call finally island chain.

@janvorlijanvorli added this to the 10.0.0 milestone May 27, 2025
@janvorlijanvorli self-assigned this May 27, 2025
CopilotAI review requested due to automatic review settings May 27, 2025 22:28
@janvorli
janvorli requested review from BrzVlad and kg as code ownersMay 27, 2025 22:28
@dotnet-policy-service

Copy link
Copy Markdown
Contributor

Tagging subscribers to this area: @BrzVlad, @janvorli, @kg
See info in area-owners.md if you want to be subscribed.

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull Request Overview

This PR adds full exception-handling (EH) support to the interpreter, covering both compilation and execution phases. It introduces new IR opcodes, builds EH metadata, implements funclet-based EH in the interpreter, and adds tests for various EH scenarios.

  • Added support for EH opcodes (throw, rethrow, leave, filters, finally) and IR-level exception clauses
  • Updated interpreter execution (InterpExecMethod) to dispatch filters, catches, and finally blocks
  • Built and emitted EH information (funclet offsets, EH tables) and wired new EH helpers

Reviewed Changes

Copilot reviewed 16 out of 16 changed files in this pull request and generated 2 comments.

Show a summary per file
FileDescription
src/coreclr/vm/prestub.cppIntroduce retVal storage for interpreter return value
src/coreclr/vm/jithelpers.cppSwap in DispatchRethrownManagedException call
src/coreclr/vm/interpexec.hExtend InterpExecMethod signature with EH args
src/coreclr/vm/interpexec.cppHandle new INTOP_* opcodes and null/overflow checks
src/coreclr/vm/exceptionhandling.hDeclare DispatchRethrownManagedException overloads
src/coreclr/vm/exceptionhandling.cppImplement DispatchRethrownManagedException and adjust dispatch logic
src/coreclr/vm/eetwain.cppFlesh out InterpreterCodeManager::CallFunclet
src/coreclr/vm/codeman.hUpdate GetFuncletStartOffsets declaration
src/coreclr/vm/codeman.cppImplement funclet start address/offset enumeration
src/coreclr/interpreter/intops.defAdd EH-related INTOP definitions
src/coreclr/interpreter/intops.cppFix CEEOpcodeSize boundary condition
src/coreclr/interpreter/eeinterp.cppInvoke BuildEHInfo() during compilation
src/coreclr/interpreter/compileropt.cppCorrect ilOffset propagation and SetSVar call
src/coreclr/interpreter/compiler.hExtend compiler IR and block structures for EH
Comments suppressed due to low confidence (3)

src/coreclr/vm/exceptionhandling.cpp:2138

  • Removing the GCX_PREEMP_NO_DTOR switch back to preemptive GC in the non-unwind (search) path can leave the runtime in cooperative mode and break stack walking. Reintroduce a mode switch before returning ExceptionContinueSearch.
else if (IS_UNWINDING(pExceptionRecord->ExceptionFlags))

src/coreclr/vm/jithelpers.cpp:1357

  • The identifier exceptionFrame is not declared in this scope, leading to a compile error. You need to pass a valid CONTEXT* (e.g., capture it or fetch it from the exception tracker) to DispatchRethrownManagedException.
DispatchRethrownManagedException(exceptionFrame.GetContext());

src/coreclr/vm/interpexec.h:77

  • There are now two overloads of InterpExecMethod (one with default ExceptionClauseArgs and the original three-argument version), which can be confusing. Consider removing the three-argument prototype and relying solely on the default parameter.
void InterpExecMethod(InterpreterFrame *pInterpreterFrame, InterpMethodContextFrame *pFrame, InterpThreadContext *pThreadContext, ExceptionClauseArgs *pExceptionClauseArgs = NULL);

Comment threadsrc/coreclr/vm/eetwain.cpp
Comment threadsrc/coreclr/vm/interpexec.cpp
@janvorli
janvorliforce-pushed the interpreter-eh branch 2 times, most recently from aee764e to 02cb97bCompareMay 27, 2025 22:34
This change implements EH support in the interpreter compiler and
execution parts.
Here is a summary of the changes:
On the compilation side:
* Adds support for CEE_THROW, CEE_RETHROW, CEE_ENDFILTER,
CEE_ENDFINALLY, CEE_LEAVE and CEE_ISINST opcodes
* Adds building of EH info with IR code offsets
* Implements proper funclet handling in the same way the JIT does. All
handlers and filters are moved to the end of the method and out of any
try ranges recursively to enable proper behavior of the EH.
* Fixes a bug related to wrong SVar and and an issue with IL offset of
inserted instructions in AllocOffsets
* Fixes a bug in the CEEOpcodeSize - off by one check for the end of the
code
On the execution side:
* Add funclet start address extraction
* Add calling funclets in the interpreted code
* Removed GCX_PREEMP_NO_DTOR from the
CallDescrWorkerUnwindFrameChainHandler, because it was not correct
* Added new IR opcodes:
* INTOP_LOAD_FRAMEVAR to load parent frame stack pointer in filter
funclets that need to run in the context of the parent, but at the
top of the current interpreter stack.
* INTOP_RETHROW to rethrow an exception
* INTOP_CALL_FINALLY to call finally funclet in non-exceptional cases
* INTOP_LEAVE_FILTER to exit a filter funclet and return the filter result
* INTOP_LEAVE_CATCH to exit a catch handler and return the resume
address
* Added calls to COMPlusThrow for division by zero, stack overflow and
few other exceptions where the interpreter had just TODO and assert
for adding those.
* Modified the InterpExecMethod so that extra information can be passed
in case of a funclet invocation.
It also adds tests to verify various interesting EH scenarios
Here are some more details on moving out the funclets:
For each finally funclet, we create a finally call island that stays in
the code where the original finally was. That island calls the finally and
then branches to the next (outer) finally if any. The last finally call
island branches to the actual leave target.
The interpreter compiler generates a separate sequence of finally call
islands for each leave instruction target. And when the leave is
executed, it jumps to the beginning or into the middle of the chain.
In other words, for example, if all leave instructions in the method go
to the same target, there would be just one call finally island chain.
Comment threadsrc/coreclr/interpreter/compiler.cpp Outdated
Comment threadsrc/coreclr/interpreter/compiler.cpp Outdated
Comment threadsrc/coreclr/interpreter/compiler.cpp Outdated
Comment threadsrc/coreclr/interpreter/compiler.cpp
Comment threadsrc/coreclr/interpreter/compiler.cpp
Comment threadsrc/coreclr/interpreter/compiler.cpp
Comment threadsrc/coreclr/interpreter/compiler.cpp
Comment threadsrc/coreclr/interpreter/compiler.cpp
Comment threadsrc/coreclr/interpreter/compiler.cpp
Comment threadsrc/coreclr/interpreter/compiler.cpp
Comment threadsrc/coreclr/interpreter/compiler.cpp
Comment threadsrc/coreclr/interpreter/compiler.cpp
Comment threadsrc/coreclr/interpreter/compiler.cpp Outdated
Comment threadsrc/coreclr/interpreter/compiler.cpp Outdated
Comment threadsrc/coreclr/interpreter/compiler.cpp
Comment threadsrc/coreclr/interpreter/compiler.cpp
Comment threadsrc/coreclr/interpreter/compiler.cpp
Comment threadsrc/coreclr/interpreter/compiler.cpp
Comment threadsrc/coreclr/interpreter/compiler.cpp Outdated
AddIns(INTOP_LOAD_FRAMEVAR);
PushInterpType(InterpTypeI, NULL);
m_pLastNewIns->SetDVar(m_pStackPointer[-1].var);
EmitStind(interpType, m_pVars[var].clsHnd, m_pVars[var].offset, true /* reverseSVarOrder */);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we need the indirect from LoadVar we should have it here too i think

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am not sure what you mean, can you please explain it in more detail?

Comment threadsrc/coreclr/interpreter/compiler.cpp Outdated
Comment threadsrc/coreclr/interpreter/compiler.cpp
Comment threadsrc/coreclr/vm/interpexec.cpp Outdated
Comment threadsrc/coreclr/interpreter/compiler.cpp
Comment threadsrc/coreclr/interpreter/compiler.cpp
@janvorli

Copy link
Copy Markdown
MemberAuthor

There is some problem with the IL to native map generation when running with checked build of the runtime. Debug build works fine. I am looking into it.

The call finally islands were not classified by the clause they are in.
Their instructions had IL offsets set to the offset of the finally
block, but they need to have no IL offset as they are not generated
from IL.
@janvorli

Copy link
Copy Markdown
MemberAuthor

I've fixed the problem with the checked build of the tests. The problem was that the call finally islands didn't have the clauseType set and so when generating native form of the EH info, they were missing in the try regions ranges.

@kg

kg commented May 29, 2025

Copy link
Copy Markdown
Contributor

I forgot to comment when I finished going over this, it all LGTM. I'm fine with landing it.

kg
kg approved these changes May 29, 2025
Comment threadsrc/coreclr/interpreter/compiler.cpp
Comment threadsrc/coreclr/interpreter/compiler.cpp Outdated
Comment threadsrc/coreclr/interpreter/compiler.cpp Outdated
Comment threadsrc/coreclr/interpreter/compiler.cpp
Comment threadsrc/coreclr/vm/interpexec.cpp Outdated
@BrzVlad

Copy link
Copy Markdown
Member

Had this written down after a call a while ago with @janvorli. Might be useful for visualization.

try {
try {
if
leave end2; // INTOP_LEAVE -> jump to finally_island_1a leave end1; // INTOP_LEAVE -> jump to finally_island_1b
} catch {
leave end1; // INTOP_LEAVE -> jump to finally_island_1b
} finally1 {
endfinally
}
-- finally_island_1a --
call_finally finally1
br finally_island_2
--------------------- -- finally_island_1b --
call_finally finally1
br end1
--------------------- end1;
leave end2; // INTOP_LEAVE -> jump to finally_island_2
} catch {
leave end2; // INTOP_LEAVE -> jump to finally_island_2
} finally2 {
endfinally
}
-- finally_island_2 --
call_finally finally2
br end2;
---------------------
end2;

@BrzVlad

Copy link
Copy Markdown
Member

Looks great! Thanks!

@janvorli
janvorli merged commit 8d59055 into dotnet:mainJun 2, 2025
@janvorli
janvorli deleted the interpreter-eh branch June 2, 2025 16:48
@github-actionsgithub-actionsBot locked and limited conversation to collaborators Jul 3, 2025
Sign up for freeto subscribe to this conversation on GitHub. Already have an account? Sign in.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants

@janvorli@kg@BrzVlad@jkotas@SingleAccretion