Private/ExecTracer.psm1

#!/usr/bin/env pwsh
using namespace System.Diagnostics
using namespace System.Collections.Generic
using namespace System.Management.Automation.Language

class TimeLine {
  [List[TimeSpan]]$TimeSpans
  hidden [TimeSpan]$Total

  TimeLine() {
    $this.TimeSpans = [List[TimeSpan]]::new()
  }

  [void] Add([TimeSpan]$TimeSpan) {
    $this.TimeSpans.Add($TimeSpan)
    $this.Total = $this.Total.Add($TimeSpan)
  }

  [TimeSpan] GetTotal() {
    return $this.Total
  }

  [TimeSpan] GetAverage() {
    if ($this.GetCount() -eq 0) {
      return [TimeSpan]::Zero
    }

    return [TimeSpan]::FromTicks($this.GetTotal().Ticks / $this.GetCount())
  }

  [int] GetCount() {
    return $this.TimeSpans.Count
  }
  [string] ToString() {
    return "Total: $([Math]::Round($this.GetTotal().TotalMilliseconds, 2))ms Average: $([Math]::Round($this.GetAverage().TotalMilliseconds, 2))ms Count: $($this.GetCount())"
  }
}

class ExecMeasurement {
  [int]$LineNo
  [TimeSpan]$ExecutionTime
  [string] $Line
  [TimeLine] $TimeLine
  hidden [bool]$Top
  hidden [string]$SourceScript

  [string] ToString() {
    $output = "LineNo: $($this.LineNo.ToString()) ExecTime: $($this.ExecutionTime.ToString()) Line: $($this.Line.ToString()) TimeLine: $($this.TimeLine.ToString()) Top: $($this.Top.ToString()) Script: $($this.SourceScript.ToString())"
    if ($this.Top) {
      return "$([char]27)[91m$output$([char]27)[0m"
    }
    return $output
  }
}


#region AstVisitor
class PSPVisitor : ICustomAstVisitor, ICustomAstVisitor2 {
  [ExecTracer]$tracer = $null
  PSPVisitor([ExecTracer]$tracer) {
    $this.tracer = $tracer
  }
  [Object] VisitElement([object]$element) {
    if ($null -eq $element) {
      return $null
    }
    $res = $element.Visit($this)
    return $res
  }
  [Object] VisitElements([Object]$elements) {
    if ($null -eq $elements -or $elements.Count -eq 0) {
      return $null
    }
    $typeName = $elements.gettype().GenericTypeArguments.Fullname

    $newElements = New-Object -TypeName "System.Collections.Generic.List[$typeName]"
    foreach ($element in $elements) {
      $visitedResult = $element.Visit($this)
      $newElements.add($visitedResult)
    }
    return $newElements
  }
  [StatementAst[]] VisitStatements([object]$Statements) {
    $newStatements = [List[StatementAst]]::new()
    foreach ($statement in $statements) {
      [bool]$instrument = $statement -is [PipelineBaseAst]
      $extent = $statement.Extent
      if ($instrument) {
        $expressionAstCollection = [List[ExpressionAst]]::new()
        $constantExpression = [ConstantExpressionAst]::new($extent, $extent.StartLineNumber - 1)
        $expressionAstCollection.Add($constantExpression)
        $constantProfiler = [ConstantExpressionAst]::new($extent, $this.tracer)
        $constantStartline = [StringConstantExpressionAst]::new($extent, "StartLine", [StringConstantType]::BareWord)
        $invokeMember = [InvokeMemberExpressionAst]::new(
          $extent,
          $constantProfiler,
          $constantStartline,
          $expressionAstCollection,
          $false
        )
        $startLine = [CommandExpressionAst]::new(
          $extent,
          $invokeMember,
          $null
        )
        $pipe = [PipelineAst]::new($extent, $startLine);
        $newStatements.Add($pipe)
      }
      $newStatements.Add($this.VisitElement($statement))
      if ($instrument) {
        $expressionAstCollection = [List[ExpressionAst]]::new()
        $expressionAstCollection.Add([ConstantExpressionAst]::new($extent, $extent.StartLineNumber - 1))
        $endLine = [CommandExpressionAst]::new(
          $extent,
          [InvokeMemberExpressionAst]::new(
            $extent,
            [ConstantExpressionAst]::new($extent, $this.tracer),
            [StringConstantExpressionAst]::new($extent, "EndLine", [StringConstantType]::BareWord),
            $expressionAstCollection,
            $false
          ),
          $null
        )
        $pipe = [PipelineAst]::new($extent, $endLine)
        $newStatements.add($pipe)
      }
    }
    return $newStatements
  }

  [object] VisitScriptBlock([ScriptBlockAst] $scriptBlockAst) {
    $newParamBlock = $this.VisitElement($scriptBlockAst.ParamBlock)
    $newBeginBlock = $this.VisitElement($scriptBlockAst.BeginBlock)
    $newProcessBlock = $this.VisitElement($scriptBlockAst.ProcessBlock)
    $newEndBlock = $this.VisitElement($scriptBlockAst.EndBlock)
    $newdynamicParamBlock = $this.VisitElement($scriptBlockAst.dynamicParamBlock)
    return [ScriptBlockAst]::new($scriptBlockAst.Extent, $newParamBlock, $newBeginBlock, $newProcessBlock, $newEndBlock, $newdynamicParamBlock)
  }


  [object] VisitNamedBlock([NamedBlockAst] $namedBlockAst) {
    $newTraps = $this.VisitElements($namedBlockAst.Traps)
    $newStatements = $this.VisitStatements($namedBlockAst.Statements)
    $statementBlock = [StatementBlockAst]::new($namedBlockAst.Extent, $newStatements, $newTraps)
    return [NamedBlockAst]::new($namedBlockAst.Extent, $namedBlockAst.BlockKind, $statementBlock, $namedBlockAst.Unnamed)
  }

  [object] VisitFunctionDefinition([FunctionDefinitionAst] $functionDefinitionAst) {
    $newBody = $this.VisitElement($functionDefinitionAst.Body)
    return [FunctionDefinitionAst]::new($functionDefinitionAst.Extent, $functionDefinitionAst.IsFilter, $functionDefinitionAst.IsWorkflow, $functionDefinitionAst.Name, $this.VisitElements($functionDefinitionAst.Parameters), $newBody);
  }

  [object] VisitStatementBlock([StatementBlockAst] $statementBlockAst) {
    $newStatements = $this.VisitStatements($statementBlockAst.Statements)
    $newTraps = $this.VisitElements($statementBlockAst.Traps)
    return [StatementBlockAst]::new($statementBlockAst.Extent, $newStatements, $newTraps)
  }

  [object] VisitIfStatement([IfStatementAst] $ifStmtAst) {
    [Tuple[PipelineBaseAst, StatementBlockAst][]]$newClauses = @(foreach ($clause in $ifStmtAst.Clauses) {
        $newClauseTest = [PipelineBaseAst]$this.VisitElement($clause.Item1)
        $newStatementBlock = [StatementBlockAst]$this.VisitElement($clause.Item2)
        [Tuple[PipelineBaseAst, StatementBlockAst]]::new($newClauseTest, $newStatementBlock)
      }
    )
    $newElseClause = $this.VisitElement($ifStmtAst.ElseClause)
    return [IfStatementAst]::new($ifStmtAst.Extent, $newClauses, $newElseClause)
  }

  [object] VisitTrap([TrapStatementAst] $trapStatementAst) {
    return [TrapStatementAst]::new($trapStatementAst.Extent, $this.VisitElement($trapStatementAst.TrapType), $this.VisitElement($trapStatementAst.Body))
  }

  [object] VisitSwitchStatement([SwitchStatementAst] $switchStatementAst) {
    $newCondition = $this.VisitElement($switchStatementAst.Condition)
    $newClauses = [List[Tuple[ExpressionAst, StatementBlockAst]]]::new()
    $switchStatementAst.Clauses | ForEach-Object {
      $newClauseTest = $this.VisitElement($_.Item1)
      $newStatementBlock = $this.VisitElement($_.Item2)
      $newClauses.Add([Tuple[ExpressionAst, StatementBlockAst]]::new($newClauseTest, $newStatementBlock))
    }
    $newDefault = $this.VisitElement($switchStatementAst.Default)
    return [SwitchStatementAst]::new($switchStatementAst.Extent, $switchStatementAst.Label, $newCondition, $switchStatementAst.Flags, $newClauses, $newDefault)
  }

  [object] VisitDataStatement([DataStatementAst] $dataStatementAst) {
    $newBody = $this.VisitElement($dataStatementAst.Body)
    $newCommandsAllowed = $this.VisitElements($dataStatementAst.CommandsAllowed)
    return [DataStatementAst]::new($dataStatementAst.Extent, $dataStatementAst.Variable, $newCommandsAllowed, $newBody)
  }

  [object] VisitForEachStatement([ForEachStatementAst] $forEachStatementAst) {
    $newVariable = $this.VisitElement($forEachStatementAst.Variable)
    $newCondition = $this.VisitElement($forEachStatementAst.Condition)
    $newBody = $this.VisitElement($forEachStatementAst.Body)
    return [ForEachStatementAst]::new($forEachStatementAst.Extent, $forEachStatementAst.Label, [ForEachFlags]::None, $newVariable, $newCondition, $newBody)
  }

  [object] VisitDoWhileStatement([DoWhileStatementAst] $doWhileStatementAst) {
    $newCondition = $this.VisitElement($doWhileStatementAst.Condition)
    $newBody = $this.VisitElement($doWhileStatementAst.Body)
    return [DoWhileStatementAst]::new($doWhileStatementAst.Extent, $doWhileStatementAst.Label, $newCondition, $newBody)
  }

  [object] VisitForStatement([ForStatementAst] $forStatementAst) {
    $newInitializer = $this.VisitElement($forStatementAst.Initializer)
    $newCondition = $this.VisitElement($forStatementAst.Condition)
    $newIterator = $this.VisitElement($forStatementAst.Iterator)
    $newBody = $this.VisitElement($forStatementAst.Body)
    return [ForStatementAst]::new($forStatementAst.Extent, $forStatementAst.Label, $newInitializer, $newCondition, $newIterator, $newBody)
  }

  [object] VisitWhileStatement([WhileStatementAst] $whileStatementAst) {
    $newCondition = $this.VisitElement($whileStatementAst.Condition)
    $newBody = $this.VisitElement($whileStatementAst.Body)
    return [WhileStatementAst]::new($whileStatementAst.Extent, $whileStatementAst.Label, $newCondition, $newBody)
  }

  [object] VisitCatchClause([CatchClauseAst] $catchClauseAst) {
    $newBody = $this.VisitElement($catchClauseAst.Body)
    $newCatchTypes = $this.VisitElements($catchClauseAst.CatchTypes)
    return [CatchClauseAst]::new($catchClauseAst.Extent, $newCatchTypes, $newBody)
  }

  [object] VisitTryStatement([TryStatementAst] $tryStatementAst) {
    $newBody = $this.VisitElement($tryStatementAst.Body)
    $newCatchClauses = $this.VisitElements($tryStatementAst.CatchClauses)
    $newFinally = $this.VisitElement($tryStatementAst.Finally)
    return [TryStatementAst]::new($tryStatementAst.Extent, $newBody, $newCatchClauses, $newFinally)
  }

  [object] VisitDoUntilStatement([DoUntilStatementAst] $doUntilStatementAst) {
    $newCondition = $this.VisitElement($doUntilStatementAst.Condition)
    $newBody = $this.VisitElement($doUntilStatementAst.Body)
    return [DoUntilStatementAst]::new($doUntilStatementAst.Extent, $doUntilStatementAst.Label, $newCondition, $newBody)
  }

  [object] VisitParamBlock([ParamBlockAst] $paramBlockAst) {
    $newAttributes = $this.VisitElements($paramBlockAst.Attributes)
    $newParameters = $this.VisitElements($paramBlockAst.Parameters)
    return [ParamBlockAst]::new($paramBlockAst.Extent, $newAttributes, $newParameters)
  }

  [object] VisitErrorStatement([ErrorStatementAst] $errorStatementAst) {
    return $errorStatementAst
  }

  [object] VisitErrorExpression([ErrorExpressionAst] $errorExpressionAst) {
    return $errorExpressionAst
  }

  [object] VisitTypeConstraint([TypeConstraintAst] $typeConstraintAst) {
    return [TypeConstraintAst]::new($typeConstraintAst.Extent, $typeConstraintAst.TypeName)
  }

  [object] VisitAttribute([AttributeAst] $attributeAst) {
    $newPositionalArguments = $this.VisitElements($attributeAst.PositionalArguments)
    $newNamedArguments = $this.VisitElements($attributeAst.NamedArguments)
    return [AttributeAst]::new($attributeAst.Extent, $attributeAst.TypeName, $newPositionalArguments, $newNamedArguments)
  }

  [object] VisitNamedAttributeArgument([NamedAttributeArgumentAst] $namedAttributeArgumentAst) {
    $newArgument = $this.VisitElement($namedAttributeArgumentAst.Argument)
    return [NamedAttributeArgumentAst]::new($namedAttributeArgumentAst.Extent, $namedAttributeArgumentAst.ArgumentName, $newArgument, $namedAttributeArgumentAst.ExpressionOmitted)
  }

  [object] VisitParameter([ParameterAst] $parameterAst) {
    $newName = $this.VisitElement($parameterAst.Name)
    $newAttributes = $this.VisitElements($parameterAst.Attributes)
    $newDefaultValue = $this.VisitElement($parameterAst.DefaultValue)
    return [ParameterAst]::new($parameterAst.Extent, $newName, $newAttributes, $newDefaultValue)
  }

  [object] VisitBreakStatement([BreakStatementAst] $breakStatementAst) {
    $newLabel = $this.VisitElement($breakStatementAst.Label)
    return [BreakStatementAst]::new($breakStatementAst.Extent, $newLabel)
  }

  [object] VisitContinueStatement([ContinueStatementAst] $continueStatementAst) {
    $newLabel = $this.VisitElement($continueStatementAst.Label)
    return [ContinueStatementAst]::new($continueStatementAst.Extent, $newLabel)
  }

  [object] VisitReturnStatement([ReturnStatementAst] $returnStatementAst) {
    $newPipeline = $this.VisitElement($returnStatementAst.Pipeline)
    return [ReturnStatementAst]::new($returnStatementAst.Extent, $newPipeline)
  }

  [object] VisitExitStatement([ExitStatementAst] $exitStatementAst) {
    $newPipeline = $this.VisitElement($exitStatementAst.Pipeline)
    return [ExitStatementAst]::new($exitStatementAst.Extent, $newPipeline)
  }

  [object] VisitThrowStatement([ThrowStatementAst] $throwStatementAst) {
    $newPipeline = $this.VisitElement($throwStatementAst.Pipeline)
    return [ThrowStatementAst]::new($throwStatementAst.Extent, $newPipeline)
  }

  [object] VisitAssignmentStatement([AssignmentStatementAst] $assignmentStatementAst) {
    $newLeft = $this.VisitElement($assignmentStatementAst.Left)
    $newRight = $this.VisitElement($assignmentStatementAst.Right)
    return [AssignmentStatementAst]::new($assignmentStatementAst.Extent, $newLeft, $assignmentStatementAst.Operator, $newRight, $assignmentStatementAst.ErrorPosition)
  }

  [object] VisitPipeline([PipelineAst] $pipelineAst) {
    $newPipeElements = $this.VisitElements($pipelineAst.PipelineElements)
    return [PipelineAst]::new($pipelineAst.Extent, $newPipeElements)
  }

  [object] VisitCommand([CommandAst] $commandAst) {
    $newCommandElements = $this.VisitElements($commandAst.CommandElements)
    $newRedirections = $this.VisitElements($commandAst.Redirections)
    return [CommandAst]::new($commandAst.Extent, $newCommandElements, $commandAst.InvocationOperator, $newRedirections)
  }

  [object] VisitCommandExpression([CommandExpressionAst] $commandExpressionAst) {
    $newExpression = $this.VisitElement($commandExpressionAst.Expression)
    $newRedirections = $this.VisitElements($commandExpressionAst.Redirections)
    return [CommandExpressionAst]::new($commandExpressionAst.Extent, $newExpression, $newRedirections)
  }

  [object] VisitCommandParameter([CommandParameterAst] $commandParameterAst) {
    $newArgument = $this.VisitElement($commandParameterAst.Argument)
    return [CommandParameterAst]::new($commandParameterAst.Extent, $commandParameterAst.ParameterName, $newArgument, $commandParameterAst.ErrorPosition)
  }

  [object] VisitFileRedirection([FileRedirectionAst] $fileRedirectionAst) {
    $newFile = $this.VisitElement($fileRedirectionAst.Location)
    return [FileRedirectionAst]::new($fileRedirectionAst.Extent, $fileRedirectionAst.FromStream, $newFile, $fileRedirectionAst.Append)
  }

  [object] VisitMergingRedirection([MergingRedirectionAst] $mergingRedirectionAst) {
    return [MergingRedirectionAst]::new($mergingRedirectionAst.Extent, $mergingRedirectionAst.FromStream, $mergingRedirectionAst.ToStream)
  }

  [object] VisitBinaryExpression([BinaryExpressionAst] $binaryExpressionAst) {
    $newLeft = $this.VisitElement($binaryExpressionAst.Left)
    $newRight = $this.VisitElement($binaryExpressionAst.Right)
    return [BinaryExpressionAst]::new($binaryExpressionAst.Extent, $newLeft, $binaryExpressionAst.Operator, $newRight, $binaryExpressionAst.ErrorPosition)
  }

  [object] VisitUnaryExpression([UnaryExpressionAst] $unaryExpressionAst) {
    $newChild = $this.VisitElement($unaryExpressionAst.Child)
    return [UnaryExpressionAst]::new($unaryExpressionAst.Extent, $unaryExpressionAst.TokenKind, $newChild)
  }

  [object] VisitConvertExpression([ConvertExpressionAst] $convertExpressionAst) {
    $newChild = $this.VisitElement($convertExpressionAst.Child)
    $newTypeConstraint = $this.VisitElement($convertExpressionAst.Type)
    return [ConvertExpressionAst]::new($convertExpressionAst.Extent, $newTypeConstraint, $newChild)
  }

  [object] VisitTypeExpression([TypeExpressionAst] $typeExpressionAst) {
    return [TypeExpressionAst]::new($typeExpressionAst.Extent, $typeExpressionAst.TypeName)
  }

  [object] VisitConstantExpression([ConstantExpressionAst] $constantExpressionAst) {
    return [ConstantExpressionAst]::new($constantExpressionAst.Extent, $constantExpressionAst.Value)
  }

  [object] VisitStringConstantExpression([StringConstantExpressionAst] $stringConstantExpressionAst) {
    return [StringConstantExpressionAst]::new($stringConstantExpressionAst.Extent, $stringConstantExpressionAst.Value, $stringConstantExpressionAst.StringConstantType)
  }

  [object] VisitSubExpression([SubExpressionAst] $subExpressionAst) {
    $newStatementBlock = $this.VisitElement($subExpressionAst.SubExpression)
    return [SubExpressionAst]::new($subExpressionAst.Extent, $newStatementBlock)
  }

  [object] VisitUsingExpression([UsingExpressionAst] $usingExpressionAst) {
    $newUsingExpr = $this.VisitElement($usingExpressionAst.SubExpression)
    return [UsingExpressionAst]::new($usingExpressionAst.Extent, $newUsingExpr)
  }

  [object] VisitVariableExpression([VariableExpressionAst] $variableExpressionAst) {
    return [VariableExpressionAst]::new($variableExpressionAst.Extent, $variableExpressionAst.VariablePath.UserPath, $variableExpressionAst.Splatted)
  }

  [object] VisitMemberExpression([MemberExpressionAst] $memberExpressionAst) {
    $newExpr = $this.VisitElement($memberExpressionAst.Expression)
    $newMember = $this.VisitElement($memberExpressionAst.Member)
    return [MemberExpressionAst]::new($memberExpressionAst.Extent, $newExpr, $newMember, $memberExpressionAst.Static)
  }

  [object] VisitInvokeMemberExpression([InvokeMemberExpressionAst] $invokeMemberExpressionAst) {
    $newExpression = $this.VisitElement($invokeMemberExpressionAst.Expression)
    $newMethod = $this.VisitElement($invokeMemberExpressionAst.Member)
    $newArguments = $this.VisitElements($invokeMemberExpressionAst.Arguments)
    return [InvokeMemberExpressionAst]::new($invokeMemberExpressionAst.Extent, $newExpression, $newMethod, $newArguments, $invokeMemberExpressionAst.Static)
  }

  [object] VisitArrayExpression([ArrayExpressionAst] $arrayExpressionAst) {
    $newStatementBlock = $this.VisitElement($arrayExpressionAst.SubExpression)
    return [ArrayExpressionAst]::new($arrayExpressionAst.Extent, $newStatementBlock)
  }

  [object] VisitArrayLiteral([ArrayLiteralAst] $arrayLiteralAst) {
    $newArrayElements = $this.VisitElements($arrayLiteralAst.Elements)
    return [ArrayLiteralAst]::new($arrayLiteralAst.Extent, $newArrayElements)
  }

  [object] VisitHashtable([HashtableAst] $hashtableAst) {
    $newKeyValuePairs = [List[Tuple[ExpressionAst, StatementAst]]]::new()
    foreach ($keyValuePair in $hashtableAst.KeyValuePairs) {
      $newKey = $this.VisitElement($keyValuePair.Item1);
      $newValue = $this.VisitElement($keyValuePair.Item2);
      $newKeyValuePairs.Add([Tuple[ExpressionAst, StatementAst]]::new($newKey, $newValue))
    }
    return [HashtableAst]::new($hashtableAst.Extent, $newKeyValuePairs)
  }

  [object] VisitScriptBlockExpression([ScriptBlockExpressionAst] $scriptBlockExpressionAst) {
    $newScriptBlock = $this.VisitElement($scriptBlockExpressionAst.ScriptBlock)
    return [ScriptBlockExpressionAst]::new($scriptBlockExpressionAst.Extent, $newScriptBlock)
  }

  [object] VisitParenExpression([ParenExpressionAst] $parenExpressionAst) {
    $newPipeline = $this.VisitElement($parenExpressionAst.Pipeline)
    return [ParenExpressionAst]::new($parenExpressionAst.Extent, $newPipeline)
  }

  [object] VisitExpandableStringExpression([ExpandableStringExpressionAst] $expandableStringExpressionAst) {
    return [ExpandableStringExpressionAst]::new($expandableStringExpressionAst.Extent, $expandableStringExpressionAst.Value, $expandableStringExpressionAst.StringConstantType)
  }

  [object] VisitIndexExpression([IndexExpressionAst] $indexExpressionAst) {
    $newTargetExpression = $this.VisitElement($indexExpressionAst.Target)
    $newIndexExpression = $this.VisitElement($indexExpressionAst.Index)
    return [IndexExpressionAst]::new($indexExpressionAst.Extent, $newTargetExpression, $newIndexExpression)
  }

  [object] VisitAttributedExpression([AttributedExpressionAst] $attributedExpressionAst) {
    $newAttribute = $this.VisitElement($attributedExpressionAst.Attribute)
    $newChild = $this.VisitElement($attributedExpressionAst.Child)
    return [AttributedExpressionAst]::new($attributedExpressionAst.Extent, $newAttribute, $newChild)
  }

  [object] VisitBlockStatement([BlockStatementAst] $blockStatementAst) {
    $newBody = $this.VisitElement($blockStatementAst.Body)
    return [BlockStatementAst]::new($blockStatementAst.Extent, $blockStatementAst.Kind, $newBody)
  }

  [object] VisitTypeDefinition([TypeDefinitionAst] $typeDefinitionAst) {
    $newAttributes = $this.VisitElements($typeDefinitionAst.Attributes)
    $newBaseTypes = $this.VisitElements($typeDefinitionAst.BaseTypes)
    $newMembers = $this.VisitElements($typeDefinitionAst.Members)

    return [TypeDefinitionAst]::new($typeDefinitionAst.Extent, $typeDefinitionAst.Name, $newAttributes, $newMembers, $typeDefinitionAst.TypeAttributes, $newBaseTypes)
  }

  [object] VisitPropertyMember([PropertyMemberAst] $propertyMemberAst) {
    $newPropertyType = $this.VisitElement($propertyMemberAst.PropertyType)
    $newAttributes = $this.VisitElements($propertyMemberAst.Attributes)
    $newInitValue = $this.VisitElement($propertyMemberAst.InitialValue)
    return [PropertyMemberAst]::new($propertyMemberAst.Extent, $propertyMemberAst.Name, $newPropertyType, $newAttributes, $propertyMemberAst.PropertyAttributes, $newInitValue)
  }

  [object] VisitFunctionMember([FunctionMemberAst] $functionMemberAst) {
    $newBody = $this.VisitElement($functionMemberAst.Body)
    $newParameters = $this.VisitElements($functionMemberAst.Parameters)
    $newFunctionDefinition = $this.VisitElement($functionMemberAst.Extent, $false, $false, $functionMemberAst.Name, $newParameters, $newBody)

    $newReturnType = $this.VisitElement($functionMemberAst.ReturnType)
    $newAttributes = $this.VisitElements($functionMemberAst.Attributes)
    return [FunctionMemberAst]::new($functionMemberAst.Extent, $newFunctionDefinition, $newReturnType, $newAttributes, $functionMemberAst.MethodAttributes)
  }

  [object] VisitBaseCtorInvokeMemberExpression([BaseCtorInvokeMemberExpressionAst] $baseCtorAst) {
    $newInvokeMemberExpression = $this.VisitInvokeMemberExpression($baseCtorAst)
    return [BaseCtorInvokeMemberExpressionAst]::new($baseCtorAst.Expression.Extent, $newInvokeMemberExpression.Extent, $newInvokeMemberExpression)
  }

  [object] VisitUsingStatement([UsingStatementAst] $usingStatementAst) {
    $newName = $this.VisitElement($usingStatementAst.Name)
    $newAlias = if ($usingStatementAst.Alias -is [StringConstantExpressionAst]) {
      $this.VisitElement($usingStatementAst.Alias)
    }

    switch ($usingStatementAst.UsingStatementKind) {
      'Module' {
        if ($usingStatementAst.ModuleSpecification -is [HashtableAst]) {
          $newModuleSpec = $this.VisitElement($usingStatementAst.ModuleSpecification)
          if ($newAlias) {
            return [UsingStatementAst]::new($usingStatementAst.Extent, $newName, $newModuleSpec)
          }
          return [UsingStatementAst]::new($usingStatementAst.Extent, $newModuleSpec)
        }
        if ($newAlias) {
          return [UsingStatementAst]::new($usingStatementAst.Extent, $_, $newName, $newAlias)
        }
        return [UsingStatementAst]::new($usingStatementAst.Extent, $_, $newName)
      }

      default {
        if ($newAlias) {
          return [UsingStatementAst]::new($usingStatementAst.Extent, $_, $newName, $newAlias)
        }
        return [UsingStatementAst]::new($usingStatementAst.Extent, $_, $newName)
      }
    }

    throw [System.NotImplementedException]::new()
  }

  [object] VisitConfigurationDefinition([ConfigurationDefinitionAst]$configDefinitionAst) {
    $newConfig = $this.VisitElement($configDefinitionAst.Body)
    $newInstanceName = $this.VisitElement($configDefinitionAst.InstanceName)
    return [ConfigurationDefinitionAst]::new($configDefinitionAst.Extent, $newConfig, $configDefinitionAst.ConfigurationType, $newInstanceName)
  }

  [object] VisitDynamicKeywordStatement([DynamicKeywordStatementAst]$dynamicKeywordAst) {
    $newElements = $this.VisitElements($dynamicKeywordAst.CommandElements)
    return [DynamicKeywordStatementAst]::new($dynamicKeywordAst.Extent, $newElements)
  }

  # V7 nodes
  [object] VisitTernaryExpression([TernaryExpressionAst]$ternaryExpressionAst) {
    $newCondition = $this.VisitElement($ternaryExpressionAst.Condition)
    $newIfTrue = $this.VisitElement($ternaryExpressionAst.IfTrue)
    $newIfFalse = $this.VisitElement($ternaryExpressionAst.IfFalse)
    return [TernaryExpressionAst]::new($ternaryExpressionAst.Extent, $newCondition, $newIfTrue, $newIfFalse)
  }

  [object] VisitPipelineChain([PipelineChainAst]$pipelineChainAst) {
    $newLhsPipeline = $this.VisitElement($pipelineChainAst.LhsPipelineChain)
    $newRhsPipeline = $this.VisitElement($pipelineChainAst.RhsPipeline)
    return [PipelineChainAst]::new($pipelineChainAst.Extent, $newLhsPipeline, $newRhsPipeline, $pipelineChainAst.Operator, $pipelineChainAst.Background)
  }
}
#endregion

<#
.SYNOPSIS
  The ExecTracer class
.DESCRIPTION
  The ExecTracer class tracks the exact execution time of every individual statement.
  By exclusively relying on static methods and internal class structures, it removes the performance overhead and scope-binding constraints of standard Cmdlet functions.
 
.EXAMPLE
  $measurements = [ExecTracer]::TraceCommand({
      Get-Service | ForEach-Object {
          $_.name + " is " + $_.Status
      }
  }, $null, $null, "ServiceTrace")
 
  # To print as table : $measurements | Format-Table
  # To print as string:
  [ExecTracer]::HighlightTop($measurements)
 
 
  Measures the anonymous script block and returns the times executed for each line, formatting it into a viewable table.
 
.EXAMPLE
  $slowestLine = [ExecTracer]::WhyScriptNotFast("c:\scripts\GenerateUsername.ps1", "ExecutionResult", @{ GivenName = "Joe"; Surname = "Smith" }, $null)
 
  Executes the specified script file with hashtable arguments and stores the result in $ExecutionResult.
  It returns the single slowest [ExecMeasurement] line of execution for performance troubleshooting.
.EXAMPLE
  try {
    [ExecTracer]::TraceCommand({
        Get-Process | ForEach-Object {
            throw "Intentional Error"
        }
    }, $null, $null, "ErrorTraceTest")
  } catch {
    # The command will fail.
    # Retrieve the backwards trace from where it broke:
    [ExecTracer]::GetErrorTrace()
  }
#>

class ExecTracer {
  [int]$Offset
  [TimeLine[]]$TimeLines
  [Stopwatch[]]$StopWatches
  static [System.Collections.Generic.List[hashtable]]$CallLog = @()
  static [System.Collections.Generic.List[string]]$LastErrorStack = [System.Collections.Generic.List[string]]::new()
  static [System.Collections.Concurrent.ConcurrentStack[string]]$Stack = [System.Collections.Concurrent.ConcurrentStack[string]]::new()

  static [string[]] GetErrorTrace() {
    return [ExecTracer]::LastErrorStack.ToArray()
  }

  ExecTracer([IScriptExtent]$extent) {
    $lines = $extent.EndLineNumber
    $this.Offset = $extent.StartLineNumber - 1
    $this.StopWatches = [Stopwatch[]]::new($lines)
    $this.TimeLines = [TimeLine[]]::new($lines)

    for ($i = 0; $i -lt $lines; $i++) {
      $this.StopWatches[$i] = [Stopwatch]::new()
      $this.TimeLines[$i] = [TimeLine]::new()
    }
  }

  static [ExecMeasurement[]] TraceCommand([string]$Path, [string]$ExecutionResultVariable, [hashtable]$Arguments, [string]$Name) {
    return [ExecTracer]::TraceCommand($Path, $ExecutionResultVariable, $Arguments, $Name, 5)
  }

  static [ExecMeasurement[]] TraceCommand([string]$Path, [string]$ExecutionResultVariable, [hashtable]$Arguments, [string]$Name, [int]$Top) {
    if (!(Test-Path $Path)) {
      throw "No such file: '$Path'"
    }

    $Errors = [System.Collections.ObjectModel.Collection[System.Management.Automation.Language.ParseError]]::new()
    $Ast = [System.Management.Automation.Language.Parser]::ParseFile((Get-Item $Path).FullName, [ref]$null, [ref]$Errors)
    if ($Errors.Count -gt 0) {
      Write-Error -Message "Encountered errors while parsing '$Path'"
    }

    $Source = $Path
    if (![string]::IsNullOrEmpty($Name)) {
      $Source = "{0}: {1}$Name" -f $Source, [System.Environment]::NewLine
    }

    $ExecTracer = [ExecTracer]::new($Ast.Extent)
    $visitor = [PSPVisitor]::new($ExecTracer)
    $newAst = $Ast.Visit($visitor)

    $MeasureScriptblock = $newAst.GetScriptBlock()

    $errCountBefore = (Get-Variable -Name Error -ValueOnly -Scope Global).Count
    try {
      if ([string]::IsNullOrEmpty($ExecutionResultVariable)) {
        if ($null -ne $Arguments) {
          $null = & $MeasureScriptblock @Arguments
        } else {
          $null = & $MeasureScriptblock
        }
      } else {
        if ($null -ne $Arguments) {
          $executionResult = . $MeasureScriptblock @Arguments
        } else {
          $executionResult = . $MeasureScriptblock
        }
        Set-Variable -Name $ExecutionResultVariable -Value $executionResult -Scope 1 -ErrorAction SilentlyContinue
      }
    } catch {
      [ExecTracer]::LastErrorStack.Clear()
      $globalError = Get-Variable -Name Error -ValueOnly -Scope Global
      $newErrCount = $globalError.Count - $errCountBefore
      for ($i = 0; $i -lt $newErrCount; $i++) {
        $e = $globalError[$i]
        if ($null -ne $e.ScriptStackTrace) {
          [ExecTracer]::LastErrorStack.Add($e.ScriptStackTrace)
        } elseif ($null -ne $e.Exception -and $null -ne $e.Exception.StackTrace) {
          [ExecTracer]::LastErrorStack.Add($e.Exception.StackTrace)
        }
      }
      Write-Warning "ExecTracer caught an error. Evaluation stack traces saved to [ExecTracer]::LastErrorStack."
      throw $_
    }

    return [ExecTracer]::BuildMeasurements($ExecTracer, $Source, $Ast, $Top)
  }

  static [ExecMeasurement[]] TraceCommand([scriptblock]$ScriptBlock, [string]$ExecutionResultVariable, [hashtable]$Arguments, [string]$Name) {
    return [ExecTracer]::TraceCommand($ScriptBlock, $ExecutionResultVariable, $Arguments, $Name, 5)
  }

  static [ExecMeasurement[]] TraceCommand([scriptblock]$ScriptBlock, [string]$ExecutionResultVariable, [hashtable]$Arguments, [string]$Name, [int]$Top) {
    $Ast = $ScriptBlock.Ast
    $Source = '{{{0}}}' -f [guid]::NewGuid().ToString().Replace('-', '')

    $ssiPropertyInfo = [scriptblock].GetProperty('SessionStateInternal', [System.Reflection.BindingFlags]'Instance,NonPublic')
    $callerSessionState = $null
    if ($null -ne $ssiPropertyInfo) {
      $callerSessionState = $ssiPropertyInfo.GetValue($ScriptBlock)
    }

    if (![string]::IsNullOrEmpty($Name)) {
      $Source = "{0}: {1}$Name" -f $Source, [System.Environment]::NewLine
    }

    $ExecTracer = [ExecTracer]::new($Ast.Extent)
    $visitor = [PSPVisitor]::new($ExecTracer)
    $newAst = $Ast.Visit($visitor)

    $MeasureScriptblock = $newAst.GetScriptBlock()
    if ($null -ne $callerSessionState -and $null -ne $ssiPropertyInfo) {
      $ssiPropertyInfo.SetValue($MeasureScriptblock, $callerSessionState)
    }
    try {
      if ([string]::IsNullOrEmpty($ExecutionResultVariable)) {
        if ($null -ne $Arguments) {
          $null = & $MeasureScriptblock @Arguments
        } else {
          $null = & $MeasureScriptblock
        }
      } else {
        if ($null -ne $Arguments) {
          $executionResult = . $MeasureScriptblock @Arguments
        } else {
          $executionResult = . $MeasureScriptblock
        }
        Set-Variable -Name $ExecutionResultVariable -Value $executionResult -Scope 1 -ErrorAction SilentlyContinue
      }
    } catch {
      [ExecTracer]::LastErrorStack.Clear()

      $internalStack = [ExecTracer]::Stack.ToArray()
      if ($null -ne $internalStack -and $internalStack.Length -gt 0) {
        [ExecTracer]::LastErrorStack.Add("--- ExecTracer Execution Stack ---")
        [ExecTracer]::LastErrorStack.AddRange($internalStack)
        [ExecTracer]::LastErrorStack.Add("")
      }

      [ExecTracer]::LastErrorStack.Add("--- Exception Trace ---")
      if (![string]::IsNullOrEmpty($_.ScriptStackTrace)) {
        [ExecTracer]::LastErrorStack.Add($_.ScriptStackTrace)
      }

      $ex = $_.Exception
      while ($null -ne $ex) {
        if (![string]::IsNullOrEmpty($ex.StackTrace)) {
          [ExecTracer]::LastErrorStack.Add($ex.StackTrace)
        }
        $ex = $ex.InnerException
      }

      $traceOutput = [ExecTracer]::LastErrorStack.ToArray() -join "`n"
      Write-Error "ExecTracer caught an engine error. Traceback recorded:`n$traceOutput"
      throw $_
    }

    return [ExecTracer]::BuildMeasurements($ExecTracer, $Source, $Ast, $Top)
  }

  static [ExecMeasurement[]] BuildMeasurements([ExecTracer]$Tracer, [string]$Source, [System.Management.Automation.Language.Ast]$Ast, [int]$Top) {
    [string[]]$lines = $Ast.Extent.ToString() -split '\r?\n' | ForEach-Object TrimEnd

    $executionTimes = [System.Collections.Generic.List[TimeSpan]]::new()
    for ($i = 0; $i -lt $lines.Count; $i++) {
      $executionTimes.Add($Tracer.TimeLines[$i].GetTotal())
    }

    $topLimit = [long]::MaxValue
    if ($Top -gt 0) {
      $topTimeSpan = $executionTimes.ToArray() | Where-Object { $_.Ticks -gt 0 } | Sort-Object -Descending | Select-Object -First $Top | Select-Object -Last 1
      if ($null -ne $topTimeSpan) {
        $topLimit = $topTimeSpan.Ticks
      }
    }

    $results = [System.Collections.Generic.List[ExecMeasurement]]::new()
    for ($i = 0; $i -lt $lines.Count; $i++) {
      $measurement = [ExecMeasurement]::new()
      $measurement.LineNo = $i + 1
      $measurement.ExecutionTime = $executionTimes[$i]
      $measurement.TimeLine = $Tracer.TimeLines[$i]
      $measurement.Line = $lines[$i]
      $measurement.SourceScript = $Source
      $measurement.Top = ($executionTimes[$i].Ticks -gt 0 -and $executionTimes[$i].Ticks -ge $topLimit)
      $results.Add($measurement)
    }

    return $results.ToArray()
  }

  static [ExecMeasurement] WhyScriptNotFast([string]$Path, [string]$ExecutionResultVariable, [hashtable]$Arguments, [string]$Name) {
    $measurements = [ExecTracer]::TraceCommand($Path, $ExecutionResultVariable, $Arguments, $Name, 5)
    return $measurements | Sort-Object ExecutionTime | Select-Object -Last 1
  }
  static [ExecMeasurement] WhyScriptNotFast([scriptblock]$ScriptBlock, [string]$ExecutionResultVariable, [hashtable]$Arguments, [string]$Name) {
    $measurements = [ExecTracer]::TraceCommand($ScriptBlock, $ExecutionResultVariable, $Arguments, $Name, 5)
    return $measurements | Sort-Object ExecutionTime | Select-Object -Last 1
  }

  static [string[]] HighlightTop([ExecMeasurement[]]$Measurements) {
    if ($null -eq $Measurements) { return @() }

    $results = [System.Collections.Generic.List[string]]::new()
    $results.Add("LineNo".PadLeft(6) + " " + "ExecutionTime".PadRight(15) + " " + "Line".PadRight(40) + " " + "TimeLine")
    $results.Add("------".PadLeft(6) + " " + "-------------".PadRight(15) + " " + "----".PadRight(40) + " " + "--------")

    foreach ($m in $Measurements) {
      $lineNoStr = $m.LineNo.ToString().PadLeft(6)
      $execStr = $m.ExecutionTime.ToString("hh\:mm\:ss\.fffffff").PadRight(15)

      $lineText = $m.Line
      if ($null -eq $lineText) { $lineText = "" }
      if ($lineText.Length -gt 38) { $lineText = $lineText.Substring(0, 38) + ".." }

      $output = "$lineNoStr $execStr $($lineText.PadRight(40)) $($m.TimeLine.ToString())"

      if ($m.Top) {
        $results.Add("$([char]27)[91m$output$([char]27)[0m")
      } else {
        $results.Add($output)
      }
    }
    return $results.ToArray()
  }

  [void] StartLine([int] $lineNo) {
    $this.StopWatches[$lineNo - $this.Offset].Start()
  }

  [void] EndLine([int] $lineNo) {
    $lineNo -= $this.Offset
    $this.StopWatches[$lineNo].Stop()
    $this.TimeLines[$lineNo].Add($this.StopWatches[$lineNo].Elapsed)
    $this.StopWatches[$lineNo].Reset()
  }
  static [void] Push([string]$class) {
    $str = "[{0}]" -f $class
    if ([ExecTracer]::Peek() -ne "$class") {
      [ExecTracer]::stack.Push($str)
      $LAST_ERROR = $(Get-Variable -Name Error -ValueOnly)[0]
      [ExecTracer]::CallLog.Add(@{ ($str + ' @ ' + [datetime]::Now.ToShortTimeString()) = $(if ($null -ne $LAST_ERROR) { $LAST_ERROR.ScriptStackTrace } else { [System.Environment]::StackTrace }).Split("`n").Replace("at ", "# ").Trim() })
    }
  }
  static [type] Pop() {
    $result = $null
    if ([ExecTracer]::stack.TryPop([ref]$result)) {
      return $result
    } else {
      throw [System.InvalidOperationException]::new("Stack is empty!")
    }
  }
  static [string] Peek() {
    $result = $null
    if ([ExecTracer]::stack.TryPeek([ref]$result)) {
      return $result
    } else {
      return [string]::Empty
    }
  }
  static [int] GetSize() {
    return [ExecTracer]::stack.Count
  }
  static [bool] IsEmpty() {
    return [ExecTracer]::stack.IsEmpty
  }
}