@@ -543,6 +543,7 @@ void InterpExecMethod(InterpreterFrame *pInterpreterFrame, InterpMethodContextFr
543543 }
544544
545545 int32_t returnOffset, callArgsOffset, methodSlot;
546+ bool isTailcall = false ;
546547 MethodDesc* targetMethod;
547548
548549MAIN_LOOP:
@@ -1895,8 +1896,10 @@ void InterpExecMethod(InterpreterFrame *pInterpreterFrame, InterpMethodContextFr
18951896 break ;
18961897 }
18971898
1899+ case INTOP_CALLVIRT_TAIL:
18981900 case INTOP_CALLVIRT:
18991901 {
1902+ isTailcall = (*ip == INTOP_CALLVIRT_TAIL);
19001903 returnOffset = ip[1 ];
19011904 callArgsOffset = ip[2 ];
19021905 methodSlot = ip[3 ];
@@ -1914,8 +1917,10 @@ void InterpExecMethod(InterpreterFrame *pInterpreterFrame, InterpMethodContextFr
19141917 goto CALL_INTERP_METHOD;
19151918 }
19161919
1920+ case INTOP_CALLI_TAIL:
19171921 case INTOP_CALLI:
19181922 {
1923+ isTailcall = (*ip == INTOP_CALLI_TAIL);
19191924 returnOffset = ip[1 ];
19201925 callArgsOffset = ip[2 ];
19211926 int32_t calliFunctionPointerVar = ip[3 ];
@@ -1927,6 +1932,7 @@ void InterpExecMethod(InterpreterFrame *pInterpreterFrame, InterpMethodContextFr
19271932 // Save current execution state for when we return from called method
19281933 pFrame->ip = ip;
19291934
1935+ // Interpreter-FIXME: isTailcall
19301936 InvokeCalliStub (LOCAL_VAR (calliFunctionPointerVar, PCODE), pCallStub, stack + callArgsOffset, stack + returnOffset);
19311937 break ;
19321938 }
@@ -1936,6 +1942,7 @@ void InterpExecMethod(InterpreterFrame *pInterpreterFrame, InterpMethodContextFr
19361942 // This opcode handles p/invokes that don't use a managed wrapper for marshaling. These
19371943 // calls are special in that they need an InlinedCallFrame in order for proper EH to happen
19381944
1945+ isTailcall = false ;
19391946 returnOffset = ip[1 ];
19401947 callArgsOffset = ip[2 ];
19411948 methodSlot = ip[3 ];
@@ -1971,6 +1978,7 @@ void InterpExecMethod(InterpreterFrame *pInterpreterFrame, InterpMethodContextFr
19711978
19721979 case INTOP_CALLDELEGATE:
19731980 {
1981+ isTailcall = false ;
19741982 returnOffset = ip[1 ];
19751983 callArgsOffset = ip[2 ];
19761984 methodSlot = ip[3 ];
@@ -1996,8 +2004,10 @@ void InterpExecMethod(InterpreterFrame *pInterpreterFrame, InterpMethodContextFr
19962004 break ;
19972005 }
19982006
2007+ case INTOP_CALL_TAIL:
19992008 case INTOP_CALL:
20002009 {
2010+ isTailcall = (*ip == INTOP_CALL_TAIL);
20012011 returnOffset = ip[1 ];
20022012 callArgsOffset = ip[2 ];
20032013 methodSlot = ip[3 ];
@@ -2032,24 +2042,44 @@ void InterpExecMethod(InterpreterFrame *pInterpreterFrame, InterpMethodContextFr
20322042 if (targetIp == NULL )
20332043 {
20342044 // If we didn't get the interpreter code pointer setup, then this is a method we need to invoke as a compiled method.
2045+ // Interpreter-FIXME: Implement tailcall via helpers, see https://github.com/dotnet/runtime/blob/main/docs/design/features/tailcalls-with-helpers.md
20352046 InvokeCompiledMethod (targetMethod, stack + callArgsOffset, stack + returnOffset, targetMethod->GetMultiCallableAddrOfCode (CORINFO_ACCESS_ANY));
20362047 break ;
20372048 }
20382049 }
20392050
2040- // Allocate child frame.
2051+ if (isTailcall)
20412052 {
2042- InterpMethodContextFrame *pChildFrame = pFrame->pNext ;
2043- if (!pChildFrame)
2053+ // Move args from callArgsOffset to start of stack frame.
2054+ InterpMethod* pTargetMethod = targetIp->Method ;
2055+ assert (pTargetMethod->CheckIntegrity ());
2056+ // It is safe to use memcpy because the source and destination are both on the interp stack, not in the GC heap.
2057+ // We need to use the target method's argsSize, not our argsSize, because tail calls (unlike CEE_JMP) can have a
2058+ // different signature from the caller.
2059+ memcpy (pFrame->pStack , stack + callArgsOffset, pTargetMethod->argsSize );
2060+ // Reuse current stack frame. We discard the call insn's returnOffset because it's not important and tail calls are
2061+ // required to be followed by a ret, so we know nothing is going to read from stack[returnOffset] after the call.
2062+ pFrame->ReInit (pFrame->pParent , targetIp, pFrame->pRetVal , pFrame->pStack );
2063+ }
2064+ else
2065+ {
2066+ // Save current execution state for when we return from called method
2067+ pFrame->ip = ip;
2068+
2069+ // Allocate child frame.
20442070 {
2045- pChildFrame = (InterpMethodContextFrame*)alloca (sizeof (InterpMethodContextFrame));
2046- pChildFrame->pNext = NULL ;
2047- pFrame->pNext = pChildFrame;
2071+ InterpMethodContextFrame *pChildFrame = pFrame->pNext ;
2072+ if (!pChildFrame)
2073+ {
2074+ pChildFrame = (InterpMethodContextFrame*)alloca (sizeof (InterpMethodContextFrame));
2075+ pChildFrame->pNext = NULL ;
2076+ pFrame->pNext = pChildFrame;
2077+ }
2078+ pChildFrame->ReInit (pFrame, targetIp, stack + returnOffset, stack + callArgsOffset);
2079+ pFrame = pChildFrame;
20482080 }
2049- pChildFrame->ReInit (pFrame, targetIp, stack + returnOffset, stack + callArgsOffset);
2050- pFrame = pChildFrame;
2081+ assert (((size_t )pFrame->pStack % INTERP_STACK_ALIGNMENT) == 0 );
20512082 }
2052- assert (((size_t )pFrame->pStack % INTERP_STACK_ALIGNMENT) == 0 );
20532083
20542084 // Set execution state for the new frame
20552085 pMethod = pFrame->startIp ->Method ;
@@ -2061,6 +2091,7 @@ void InterpExecMethod(InterpreterFrame *pInterpreterFrame, InterpMethodContextFr
20612091 }
20622092 case INTOP_NEWOBJ_GENERIC:
20632093 {
2094+ isTailcall = false ;
20642095 returnOffset = ip[1 ];
20652096 callArgsOffset = ip[2 ];
20662097 methodSlot = ip[4 ];
@@ -2079,6 +2110,7 @@ void InterpExecMethod(InterpreterFrame *pInterpreterFrame, InterpMethodContextFr
20792110 }
20802111 case INTOP_NEWOBJ:
20812112 {
2113+ isTailcall = false ;
20822114 returnOffset = ip[1 ];
20832115 callArgsOffset = ip[2 ];
20842116 methodSlot = ip[3 ];
@@ -2110,6 +2142,7 @@ void InterpExecMethod(InterpreterFrame *pInterpreterFrame, InterpMethodContextFr
21102142 }
21112143 case INTOP_NEWOBJ_VT:
21122144 {
2145+ isTailcall = false ;
21132146 returnOffset = ip[1 ];
21142147 callArgsOffset = ip[2 ];
21152148 methodSlot = ip[3 ];
0 commit comments