text: //-------------------------------------------------------------------------------------------------------
  // Copyright (C) Microsoft. All rights reserved.
  // Licensed under the MIT license. See LICENSE.txt file in the project root for full license information.
  //-------------------------------------------------------------------------------------------------------
  #include "RuntimeDebugPch.h"
  
  #ifdef ENABLE_SCRIPT_DEBUGGING
  #include "Language/InterpreterStackFrame.h"
  
  #define InvalidScriptId 0xFFFFFFFF
  
  namespace Js
  {
  
      InterpreterHaltState::InterpreterHaltState(StopType _stopType, const FunctionBody* _executingFunction, MutationBreakpoint* _activeMutationBP/*= nullptr*/) :
          stopType(_stopType),
          executingFunction(_executingFunction),
          topFrame(nullptr),
          framePointers(nullptr),
          referencedDiagnosticArena(nullptr),
          exceptionObject(nullptr),
          stringBuilder(nullptr),
          activeMutationBP(_activeMutationBP)
      {
          Assert(executingFunction || (stopType == STOP_EXCEPTIONTHROW || stopType == STOP_MUTATIONBREAKPOINT));
      }
  
      FunctionBody* InterpreterHaltState::GetFunction()
      {
          Assert(IsValid());
          return this->topFrame->GetFunction();
      }
  
      int InterpreterHaltState::GetCurrentOffset()
      {
          Assert(IsValid());
          return this->topFrame->GetByteCodeOffset();
      }
  
      void InterpreterHaltState::SetCurrentOffset(int offset)
      {
          Assert(IsValid());
          if (this->topFrame->IsInterpreterFrame())
          {
              // For interpreter frames, actual scenarios we need changed offset are: set next in topmost frame, ignore exception.
              // For throw exception we don't need it, but it doesn't hurt because interpreter will ignore the offset
              // and rather just throw the exception.
              this->topFrame->AsInterpreterFrame()->GetReader()->SetCurrentOffset(offset);
          }
          else
          {
              // For native frames, the only scenario we need to record changed offset is when we ignore exception.
              if (this->exceptionObject && this->exceptionObject->IsDebuggerSkip())
              {
                  this->exceptionObject->SetByteCodeOffsetAfterDebuggerSkip(offset);
              }
          }
      }
  
      bool InterpreterHaltState::IsValid() const
      {
          // "executingFunction == nullptr" when dispatching exception or mutation bp.
          return topFrame && (topFrame->GetFunction() == executingFunction || executingFunction == nullptr);
      }
  
  
      StepController::StepController()
          : stepType(STEP_NONE),
          byteOffset(0),
          statementMap(NULL),
          frameCountWhenSet(0),
          frameAddrWhenSet((size_t)-1),
          stepCompleteOnInlineBreakpoint(false),
          pActivatedContext(NULL),
          scriptIdWhenSet(InvalidScriptId),
          returnedValueRecordingDepth(0),
          returnedValueList(nullptr)
      {
      }
  
      bool StepController::IsActive()
      {
          return stepType != STEP_NONE;
      }
  
      void StepController::Activate(StepType stepType, InterpreterHaltState* haltState)
      {
          this->stepType = stepType;
          this->byteOffset = haltState->GetCurrentOffset();
          this->pActivatedContext = haltState->framePointers->Peek()->GetScriptContext();
          Assert(this->pActivatedContext);
  
          Js::FunctionBody* functionBody = haltState->GetFunction();
  
          this->body.Root(functionBody, this->pActivatedContext->GetRecycler());
          this->statementMap = body->GetMatchingStatementMapFromByteCode(byteOffset, false);
          this->frameCountWhenSet = haltState->framePointers->Count();
  
          if (stepType != STEP_DOCUMENT)
          {
              this->frameAddrWhenSet = (size_t)haltState->framePointers->Peek(0)->GetStackAddress();
          }
          else
          {
              // for doc mode, do not bail out automatically on frame changes
              this->frameAddrWhenSet = (size_t)-1;
          }
  
          this->scriptIdWhenSet = GetScriptId(functionBody);
  
          if (this->returnedValueList == nullptr)
          {
              this->returnedValueList = JsUtil::List<ReturnedValue*>::New(this->pActivatedContext->GetRecycler());
              this->pActivatedContext->GetThreadContext()->SetReturnedValueList(this->returnedValueList);
          }
      }
  
      void StepController::AddToReturnedValueContainer(Js::Var returnValue, Js::JavascriptFunction * function, bool isValueOfReturnStatement)
      {
          if (this->pActivatedContext != nullptr) // This will be null when we execute scripts when on break.
          {
              ReturnedValue *valuePair = RecyclerNew(pActivatedContext->GetRecycler(), ReturnedValue, returnValue, function, isValueOfReturnStatement);
              this->returnedValueList->Add(valuePair);
          }
      }
  
      void StepController::AddReturnToReturnedValueContainer()
      {
          AddToReturnedValueContainer(nullptr/*returnValue*/, nullptr/*function*/, true/*isValueOfReturnStatement*/);
      }
  
      void StepController::StartRecordingCall()
      {
          returnedValueRecordingDepth++;
      }
  
      void StepController::EndRecordingCall(Js::Var returnValue, Js::JavascriptFunction * function)
      {
          if (IsActive() && this->pActivatedContext != nullptr && returnValue != nullptr)
          {
              if (this->pActivatedContext->GetThreadContext()->GetDebugManager()->IsAtDispatchHalt())
              {
                  // OS bug 3050302 - Keeping this FatalError for finding other issues where we can record when we are at break
                  Js::Throw::FatalInternalError();
              }
              bool isStepOut = stepType == STEP_OUT || stepType == STEP_DOCUMENT;
  
              // Record when :
              // If step-out/document : we need to record calls only which are already on the stack, that means the recording-depth is zero or negative.
              // if not step-out (step-in and step-over). only for those, which are called from the current call-site or the ones as if we step-out
              if ((!isStepOut && returnedValueRecordingDepth <= 1) || (isStepOut && returnedValueRecordingDepth <= 0))
              {
                  // if we are step_document, we should be removing whatever we have collected so-far,
                  // since they belong to the current document which is a library code
                  if (stepType == STEP_DOCUMENT)
                  {
                      this->returnedValueList->ClearAndZero();
                  }
  
                  AddToReturnedValueContainer(returnValue, function, false/*isValueOfReturnStatement*/);
              }
          }
          returnedValueRecordingDepth--;
      }
  
      void StepController::ResetReturnedValueList()
      {
          returnedValueRecordingDepth = 0;
          if (this->returnedValueList != nullptr)
          {
              this->returnedValueList->ClearAndZero();
          }
      }
  
      void StepController::HandleResumeAction(Js::InterpreterHaltState* haltState, BREAKRESUMEACTION resumeAction)
      {
          ResetReturnedValueList();
          switch (resumeAction)
          {
          case BREAKRESUMEACTION_STEP_INTO:
              Activate(Js::STEP_IN, haltState);
              break;
          case BREAKRESUMEACTION_STEP_OVER:
              Activate(Js::STEP_OVER, haltState);
              break;
          case BREAKRESUMEACTION_STEP_OUT:
              Activate(Js::STEP_OUT, haltState);
              break;
          case BREAKRESUMEACTION_STEP_DOCUMENT:
              Activate(Js::STEP_DOCUMENT, haltState);
              break;
          }
      }
  
  
      void StepController::Deactivate(InterpreterHaltState* haltState /*=nullptr*/)
      {
          // If we are deactivating the step controller during ProbeContainer close or attach/detach we should clear return value list
          // If we break other than step -> clear the list.
          // If we step in and we land on different function (we are in recording phase the current function) -> clear the list
          if ((haltState == nullptr) || (haltState->stopType != Js::STOP_STEPCOMPLETE || (this->stepType == STEP_IN && this->returnedValueRecordingDepth > 0)))
          {
              ResetReturnedValueList();
          }
  
          if (this->body)
          {
              Assert(this->pActivatedContext);
              this->body.Unroot(this->pActivatedContext->GetRecycler());
          }
          this->pActivatedContext = NULL;
          stepType = STEP_NONE;
          byteOffset = Js::Constants::NoByteCodeOffset;
          statementMap = NULL;
  
          frameCountWhenSet = 0;
          scriptIdWhenSet = InvalidScriptId;
          frameAddrWhenSet = (size_t)-1;
      }
  
      bool StepController::IsStepComplete_AllowingFalsePositives(InterpreterStackFrame * stackFrame)
      {
          Assert(stackFrame);
          if (stepType == STEP_IN)
          {
              return true;
          }
          else if (stepType == STEP_DOCUMENT)
          {
              Assert(stackFrame->GetFunctionBody());
              return GetScriptId(stackFrame->GetFunctionBody()) != this->scriptIdWhenSet;
          }
  
          // A STEP_OUT or a STEP_OVER has not completed if we are currently deeper on the callstack.
          return this->frameAddrWhenSet <= stackFrame->GetStackAddress();
      }
  
      bool StepController::IsStepComplete(InterpreterHaltState* haltState, HaltCallback * haltCallback, OpCode originalOpcode)
      {
          int currentFrameCount = haltState->framePointers->Count();
          AssertMsg(currentFrameCount > 0, "In IsStepComplete we must have at least one frame.");
  
          FunctionBody* body = haltState->framePointers->Peek()->GetJavascriptFunction()->GetFunctionBody();
          bool canPossiblyHalt = haltCallback->CanHalt(haltState);
  
          OUTPUT_TRACE(Js::DebuggerPhase, _u("StepController::IsStepComplete(): stepType = %d "), stepType);
  
          uint scriptId = GetScriptId(body);
          AssertMsg(scriptId != InvalidScriptId, "scriptId cannot be 'invalid-reserved'");
  
          int byteOffset = haltState->GetCurrentOffset();
          bool fCanHalt = false;
  
          if (this->frameCountWhenSet > currentFrameCount && STEP_DOCUMENT != stepType)
          {
              // all steps match once the frame they started on has popped.
              fCanHalt = canPossiblyHalt;
          }
          else if (STEP_DOCUMENT == stepType)
          {
              OUTPUT_TRACE(Js::DebuggerPhase, _u("StepController::IsStepComplete(): docId when set=%d, currentDocId = %d, can Halt = %d, will halt = %d "), this->scriptIdWhenSet, scriptId, canPossiblyHalt, fCanHalt);
              fCanHalt = (scriptId != this->scriptIdWhenSet) && canPossiblyHalt;
          }
          else if (STEP_IN != stepType && this->frameCountWhenSet < currentFrameCount)
          {
              // Only step into allows the stack to be deeper
              OUTPUT_TRACE(Js::DebuggerPhase, _u("StepController::IsStepComplete(stepType = %d) returning false "), stepType);
              return false;
          }
          else if (STEP_OUT == stepType)
          {
              fCanHalt = this->frameCountWhenSet > currentFrameCount && canPossiblyHalt;
          }
          else if (nullptr != this->statementMap && this->statementMap->isSubexpression && STEP_IN != stepType)
          {
              // Only step into started from subexpression is allowed to stop on another subexpression
              Js::FunctionBody* pCurrentFuncBody = haltState->GetFunction();
              Js::FunctionBody::StatementMap* map = pCurrentFuncBody->GetMatchingStatementMapFromByteCode(byteOffset, false);
              if (nullptr != map && map->isSubexpression)    // Execute remaining Subexpressions
              {
                  fCanHalt = false;
              }
              else
              {
                  Js::FunctionBody::StatementMap* outerMap = pCurrentFuncBody->GetMatchingStatementMapFromByteCode(this->statementMap->byteCodeSpan.begin, true);
                  if (nullptr != outerMap && map == outerMap) // Execute the rest of current regular statement
                  {
                      fCanHalt = false;
                  }
                  else
                  {
                      fCanHalt = canPossiblyHalt;
                  }
              }
          }
          else
          {
              // Match if we are no longer on the original statement.  Stepping means move off current statement.
              if (body != this->body || NULL == this->statementMap ||
                  !this->statementMap->byteCodeSpan.Includes(byteOffset))
              {
                  fCanHalt = canPossiblyHalt;
              }
          }
          // At this point we are verifying of global return opcode.
          // The global returns are alway added as a zero range begin with zero.
  
          if (fCanHalt && originalOpcode == OpCode::Ret)
          {
              Js::FunctionBody* pCurrentFuncBody = haltState->GetFunction();
              Js::FunctionBody::StatementMap* map = pCurrentFuncBody->GetMatchingStatementMapFromByteCode(byteOffset, true);
  
              fCanHalt = !FunctionBody::IsDummyGlobalRetStatement(&map->sourceSpan);
              if (fCanHalt)
              {
                  // We are breaking at last line of function, imagine '}'
                  AddReturnToReturnedValueContainer();
              }
          }
  
          OUTPUT_TRACE(Js::DebuggerPhase, _u("StepController::IsStepComplete(stepType = %d) returning %d "), stepType, fCanHalt);
          return fCanHalt;
      }
  
      bool StepController::ContinueFromInlineBreakpoint()
      {
          bool ret = stepCompleteOnInlineBreakpoint;
          stepCompleteOnInlineBreakpoint = false;
          return ret;
      }
  
      uint StepController::GetScriptId(_In_ FunctionBody* body)
      {
          // safe value
          uint retValue = BuiltInFunctionsScriptId;
  
          if (body != nullptr)
          {
              // FYI - Different script blocks within a HTML page will have different source Info ids even though they have the same backing file.
              // It might imply we notify the debugger a bit more than needed - thus can be TODO for performance improvements of the Just-My-Code
              // or step to next document boundary mode.
              AssertMsg(body->GetUtf8SourceInfo() != nullptr, "body->GetUtf8SourceInfo() == nullptr");
              retValue = body->GetUtf8SourceInfo()->GetSourceInfoId();
          }
  
          return retValue;
      }
  
      AsyncBreakController::AsyncBreakController()
          : haltCallback(NULL)
      {
      }
  
      void AsyncBreakController::Activate(HaltCallback* haltCallback)
      {
          InterlockedExchangePointer((PVOID*)&this->haltCallback, haltCallback);
      }
  
      void AsyncBreakController::Deactivate()
      {
          InterlockedExchangePointer((PVOID*)&this->haltCallback, NULL);
      }
  
      bool AsyncBreakController::IsBreak()
      {
          return haltCallback != NULL;
      }
  
      bool AsyncBreakController::IsAtStoppingLocation(InterpreterHaltState* haltState)
      {
          HaltCallback* callback = this->haltCallback;
          if (callback)
          {
              return callback->CanHalt(haltState);
          }
          return false;
      }
  
      void AsyncBreakController::DispatchAndReset(InterpreterHaltState* haltState)
      {
          HaltCallback* callback = this->haltCallback;
          Deactivate();
          if (callback)
          {
              callback->DispatchHalt(haltState);
          }
      }
  }
  #endif
  