Transpilers/Core/Pipescript.psx.ps1

<#
.Synopsis
    The Core PipeScript Transpiler
.Description
    The Core PipeScript Transpiler.

    This will convert various portions in the PowerShell Abstract Syntax Tree from their PipeScript syntax into regular PowerShell.
    
    It will run other converters as directed by the source code.
.Link
    .>Pipescript.Function
.Link
    .>Pipescript.AttributedExpression
.EXAMPLE
    {
        function [explicit]ExplicitOutput {
            "whoops"
            return 1
        }
    } | .>PipeScript
.Example
    {
        [minify]{
            # Requires PSMinifier (this comment will be minified away)
            "blah"
                "de"
                    "blah"
        }
    } | .>PipeScript
.Example
    .\PipeScript.psx.ps1 -ScriptBlock {
        [bash]{param([string]$Message) $message}
    }
.Example
    .\PipeScript.psx.ps1 -ScriptBlock {
        [explicit]{1;2;3;echo 4}
    }
#>

<#
.EXAMPLE
    {
        function [ProxyCommand<'Get-Process'>]GetProcessProxy {}
    } | .>PipeScript
#>

param(
# A ScriptBlock that will be transpiled.
[Parameter(Mandatory,ParameterSetName='ScriptBlock',ValueFromPipeline)]
[ScriptBlock]
$ScriptBlock,

# One or more transpilation expressions that apply to the script block.
[Parameter(ParameterSetName='ScriptBlock')]
[string[]]
$Transpiler
)

begin {
    # First off, we declare a few inner functions that we will use within only this script.
    $myCmd = $MyInvocation.MyCommand
}

process {
    # First, make a copy of our input parameters.
    $invocationParams = [Ordered]@{} + $PSBoundParameters

    # Then, check on which parameter set is being used
    switch ($PSCmdlet.ParameterSetName) {
        #region Transpile ScriptBlocks
        ScriptBlock {
            # Walk thru any provided transpilers
            $TranspilerAttributes = @()
            $RemainingTranspilers = @()
            $scriptText = "$scriptBlock"
            foreach ($transpilerExpression in $invocationParams['transpiler']) {
                # If the transpiled expression doesn't use variables (except for true or false),
                if ($transpiledExpression -notmatch '(?>\@|\$(?!(?>true|false)))') {
                    # we will make the expression into a psuedo-attribute.
                    $TranspilerAttributes += $transpilerExpression -replace 
                        '\<', '(' -replace '\>', ')' -replace # To do this, replace <> with ()
                        '^([^\[])', '[$1' -replace '([^\]])$', '$1]' -replace # then ensure the attribute begins and ends with brackets
                        '(?<=[^\)])\]$', '()]' # and then ensure that it has a set of empty parenthesis at the end.
                } else {
                    $RemainingTranspilers += $transpilerExpression # Keep track of any other transpilers (for future use).
                }
            }
            
            # If the ScriptBlock had attributes, we'll add them to a special list of things that will be transpiled first.
            if ($ScriptBlock.Ast.ParamBlock.Attributes) {
                # To determine what we transpile first,
                $moreToPreTranspile =
                    @(
                        # check each ScriptBlock attribute
                        foreach ($attrAst in $ScriptBlock.Ast.ParamBlock.Attributes) {
                            $attrRealType = $attrAst.TypeName.ToString() -as [type]
                            if (-not $attrRealType) {
                                $attrAst
                            }
                        }
                    )
                
                $replacements = [Ordered]@{}
                $myOffset     = 0
                foreach ($moreTo in $moreToPreTranspile) {
                    $TranspilerAttributes += $moreTo.Extent.ToString()
                    $start = $scriptText.IndexOf($moreTo.extent.text, $myOffset)
                    $end   = $start + $moreTo.Extent.Text.Length
                    $replacements["$start,$end"] = ''
                }

                $UpdatedScript = Update-PipeScript -ScriptReplacement $replacements -ScriptBlock $ScriptBlock

                $scriptBlock = $UpdatedScript
            }

            $scriptText = "$scriptBlock"
            

            $PreTranspile = 
                if ($transpilerAttributes) {
                    [ScriptBlock]::Create(
                        ($TranspilerAttributes -join [Environment]::NewLine) + $(
                            if (-not $ScriptBlock.Ast.ParamBlock) {
                                "param()" + [Environment]::NewLine
                            }
                        ) + $ScriptBlock
                    )
                }

            if (-not $ScriptBlock) { return }

            # If there were any attributes that can be pre-transpiled, convert them.
            if ($PreTranspile.Ast.ParamBlock.Attributes) {
                # Get the list of transpilation attributes
                $attrList = @($PreTranspile.Ast.ParamBlock.Attributes)
                $currentInput = $ScriptBlock  # and set the current input to this script block.
                # Then walk backwards thru the attribute list
                for ($attrIndex = $attrList.Length - 1; $attrIndex -ge 0; $attrIndex--) {
                    # and invoke the transpiler by name
                    $currentOutput = $currentInput | Invoke-PipeScript -AttributeSyntaxTree $attrList[$attrIndex]
                    if ($currentOutput -and $currentOutput -ne $attrList[$attrIndex]) {
                        $currentInput  = $currentOutput
                    }                    
                }

                
                # If the current output is a [ScriptBlock]
                if ($currentOutput -is [scriptblock]) {
                    # reset the script block.
                    $ScriptBlock = $currentOutput
                } elseif ( # Otherwise, if we can convert the string to a ScriptBlock without error
                    $currentOutput -is [string] -and $(
                    $CurrentOutputAsScriptBlock = try { [scriptblock]::Create($currentOutput)} catch { $null }
                    $CurrentOutputAsScriptBlock
                )) {
                    # reset the script block.
                    $ScriptBlock = $CurrentOutputAsScriptBlock
                }
            }
            
            # Find all AST elements within the script block.
            $astList = @($scriptBlock.Ast.FindAll({$true}, $false))
            # Prepare to replace code by stringifying the -ScriptBlcok,
            $scriptText = "$scriptBlock"
            # creating an ordered dictionary of replacements,
            $replacements = [Ordered]@{}
            $AstReplacements = [Ordered]@{}
            # and a list of Update-PipeScript splats
            $updateSplats = @()
            
            # At various points within transpilation, we will be skipping processing until a known end pointer. For now, set this to null.
            $skipUntil  = 0
            # Keep track of the offset from a starting position as well, for the same reason.
            $myOffset   = 0

            # Walk over each item in the abstract syntax tree.
            :NextAstItem foreach ($item in $astList) {
                # If skipUntil was set,
                if ($skipUntil) {
                    # find the space between now and the last known offset.
                    try {
                        $newOffset = $scriptText.IndexOf($item.Extent.Text, $myOffset)
                        if ($newOffset -eq -1) { continue }
                        $myOffset  = $newOffset
                    } catch {
                        $ex =$_
                        $null = $null
                    }
                    if ($myOffset -lt $skipUntil) { # If this is before our skipUntil point
                        continue # ignore this AST element.
                    }
                    $skipUntil = $null # If we have reached our skipUntil point, let's stop skipping.
                }
                # otherwise, find if any pipescripts match this AST

                # We can cache transpilers by type
                if (-not $script:TranspilerAstTypes) {
                    $script:TranspilerAstTypes = @{}
                }

                $itemTypeName = $item.GetType().Fullname
                if (-not $script:TranspilerAstTypes[$itemTypeName]) {
                    $script:TranspilerAstTypes[$itemTypeName] = Get-Transpiler -CouldPipe $item |
                        Where-Object { 
                            $_.ExtensionCommand.CouldRun(@{} + $_.ExtensionParameter)
                        }
                }

                # But we still need to determine if an extension is valid for a given type's context
                $pipescripts =  foreach ($potentialPipeScript in $script:TranspilerAstTypes[$itemTypeName]) {                    
                        if (
                        $potentialPipeScript.ExtensionCommand.CouldRun(@{} + $potentialPipeScript.ExtensionParameter) -and
                        $(
                            $eap = $ErrorActionPreference
                            $ErrorActionPreference = 'ignore'
                            $potentialPipeScript.ExtensionCommand.Validate($item, $true)
                            $ErrorActionPreference = $eap
                        )
                        ) {
                            $potentialPipeScript
                        }
                    }

                # If we found any matching pipescripts
                if ($pipescripts) {
                    $pipeScriptOutput = # run them
                        :NextPipeScript foreach ($ps in $pipescripts) {
                            Invoke-PipeScript -InputObject $item -CommandInfo $ps.ExtensionCommand
                        }

                    foreach ($pso in $pipeScriptOutput) {
                        if ($pso -is [Collections.IDictionary]) {
                            $psoCopy = [Ordered]@{} + $pso                             
                            foreach ($kv in @($psoCopy.GetEnumerator())) {
                                if ($kv.Key -is [Management.Automation.Language.Ast]) {
                                    $astReplacements[$kv.Key] = $kv.Value
                                    $psoCopy.Remove($kv.Key)
                                } elseif ($kv.Key -match '^\d,\d$') {
                                    $textReplacements["$($kv.Key)"] = $kv.Value
                                    $psoCopy.Remove($kv.Key)
                                }
                            }
                            if ($psoCopy.Count) {
                                $updateSplats += $psoCopy
                            }                            
                        } else {
                            if ($pso) { # If we have ouput from any of the scripts
                                $start = $scriptText.IndexOf($item.Extent.Text, $myOffset) # determine the end of this AST element
                                $end   = $start + $item.Extent.Text.Length
                                $skipUntil = $end # set SkipUntil
                                $AstReplacements[$item] = $pso # and store the replacement.
                            }
                        }
                    }

                } 
            }
            
            $newScript =
                if ($AstReplacements.Count) {            
                    Update-PipeScript -ScriptBlock $ScriptBlock -ScriptReplacement $replacements -AstReplacement $AstReplacements
                } elseif ($updateSplats) {
                    foreach ($upSplat in $updateSplats) {
                        $newScript = Update-PipeScript -ScriptBlock $ScriptBlock @upSplat
                        if ($newScript -is [ScriptBlock]) {
                            $scriptBlock = $newScript
                        }
                    }
                    $scriptBlock
                } else {
                    $scriptBlock
                }
            
            $transpiledScriptBlock =
                [ScriptBlock]::Create($newScript)
            
            $transpiledScriptBlock
            # output the new script.
        }
        #endregion Transpile ScriptBlocks
    }
}