Transpilers/Syntax/ArrowOperator.psx.ps1

<#
.SYNOPSIS
    Arrow Operator
.DESCRIPTION
    Many languages support an arrow operator natively. PowerShell does not.

    PipeScript's arrow operator works similarly to lambda expressions in C# or arrow operators in JavaScript:
.EXAMPLE
    $allTypes =
        Invoke-PipeScript {
            [AppDomain]::CurrentDomain.GetAssemblies() => $_.GetTypes()
        }

    $allTypes.Count # Should -BeGreaterThan 1kb
    $allTypes # Should -BeOfType ([Type])
.EXAMPLE
    Invoke-PipeScript {
        Get-Process -ID $PID => ($Name, $Id, $StartTime) => { "$Name [$ID] $StartTime"}
    } # Should -Match "$pid"
.EXAMPLE
    Invoke-PipeScript {
        func => ($Name, $Id) { $Name, $Id}
    } # Should -BeOfType ([ScriptBlock])
#>

[ValidatePattern("=>")]
[ValidateScript({
    $ToValidate = $_        
    if ($ToValidate -isnot [Management.Automation.Language.AssignmentStatementAst]) {
        return (
            $ToValidate -is [Management.Automation.Language.CommandAst]
        ) -and ($ToValidate.CommandElements.Value -eq '=>')        
    } else {
        return (
            $ToValidate.Right -is [Management.Automation.Language.PipelineAst]
        ) -and
        $ToValidate.Right.PipelineElements[0].CommandElements -and
        ('>' -eq $ToValidate.Right.PipelineElements[0].CommandElements[0])    
    }    
})]
param(
<#
The Arrow Operator can be part of a statement, for example:

~~~PowerShell
Invoke-PipeScript { [AppDomain]::CurrentDomain.GetAssemblies() => $_.GetTypes() }
~~~

The -ArrowStatementAst is the assignment statement that uses the arrow operator.
#>
 
[Parameter(Mandatory,ValueFromPipeline,ParameterSetName='AssignmentStatementAst')]
[Management.Automation.Language.AssignmentStatementAst]
$ArrowStatementAst,

<#
The Arrow Operator can occur within a command, for example:

~~~PowerShell
Invoke-PipeScript {
    Get-Process -Id $pid => ($Name,$ID,$StartTime) => { "$Name [$ID] @ $StartTime" }
}
~~~
#>

[Parameter(Mandatory,ValueFromPipeline,ParameterSetName='CommandAst')]
[Management.Automation.Language.CommandAst]
$ArrowCommandAst
)

begin {
    # Declare a little script to help us turn things into parameters
    $ConvertParensAndVariablesExpressionToParameters = {
        # If we have a parenthesis
        @(if ($currentParenthesis -and $currentParenthesis.Pipeline.PipelineElements) {
            $currentPipelineElement = $currentParenthesis.Pipeline.PipelineElements[0]
            if ($currentPipelineElement.Expression -is 
                [Management.Automation.Language.ArrayLiteralAst]) {
                # unroll array elements
                foreach ($arrayElement in $currentPipelineElement.Expression.Elements) {
                    if ($arrayElement -is [Management.Automation.Language.VariableExpressionAST]) {
                        "[Parameter(ValueFromPipelineByPropertyName)]$arrayElement"
                    } else {
                        "$arrayElement"
                    }
                }
            } elseif ($currentPipelineElement.Expression -is 
                [Management.Automation.Language.VariableExpressionAST]
            ) {
                # Put variables in directly if there was only one.
                "[Parameter(ValueFromPipelineByPropertyName,ValueFromPipeline)]$currentPipelineElement"
            } elseif ($currentPipelineElement.Expression -is 
                [Management.Automation.Language.AttributedExpressionAst]) {
                # If the variable had attributes, make sure it has ValueFromPipelineByPropertyName
                if ($currentPipelineElement.Expression.Extent -notmatch '\[Parameter') {
                    "[Parameter(ValueFromPipelineByPropertyName)]$currentPipelineElement"
                } else {
                    "$currentPipelineElement"
                }
            } else {
                # Otherwise something unexpected came thru. Do nothing.
                $null = $null
            }
            
        } elseif ($currentVariable) {
            # If there was a current single variable, bind it to the pipeline
            "[Parameter(ValueFromPipeline)]$currentVariable"
        }) -join ("," + [Environment]::Newline + (' ' * 8))
    }
}

process {
    $IsGeneratingALambda = $false
    $functionNames       = @()
    
    # Start generating our pipeline:
    $generatedPipeline = @(        
    
    # If we're in an assignment
    if ($ArrowStatementAst) {        
        if ($ArrowStatementAst.Left.VariablePath.DriveName -eq 'function' -or
            $ArrowStatementAst.Left.VariablePath.UserPath -eq 'function') {
            $IsGeneratingALambda = $true
        }

        "$($ArrowStatementAst.Left)" # the pipeline starts with the left

        $arrowPipeline = $ArrowStatementAst.Right
        
        # and we have a pipeline command
        $arrowPipelineCommand = $arrowPipeline.PipelineElements[0]
        # whose first index we can skip (because it will be `>`).
        $arrowIndex = 1
    } elseif ($ArrowCommandAst) {
        # If we're part of a command ast
        $arrowPipelineCommand = $ArrowCommandAst
        $arrowCommandElements = # get all elements before this point
            @(for ($arrowIndex = 0; $arrowIndex -lt $arrowPipelineCommand.CommandElements.Count; $arrowIndex++) {
                $arrowElement = $arrowPipelineCommand.CommandElements[$arrowIndex]
                if ($arrowElement.Value -and $arrowElement.Value -eq 'func') {
                    $IsGeneratingALambda = $true
                    break
                }
                if ($arrowElement.Value -and $arrowElement.Value -eq '=>') {
                    break
                }
                $arrowElement            
            }) -join ' '

        # If there were any, start off our pipeline with this
        if ($arrowCommandElements) {
            $arrowCommandElements
        }
    }
    )

    # Now we need to go thru everything in the command after =>

    # Parenethesis, variables, and conditions are special, so nullify each.
    $currentParenthesis   = $null
    $currentVariable      = $null
    $IsCondition          = $false
    # Walk thru each command element.
    for (; $arrowIndex -lt $arrowPipelineCommand.CommandElements.Count; $arrowIndex++) {
        $arrowElement = $arrowPipelineCommand.CommandElements[$arrowIndex]
        # Track paranethesis
        if ($arrowElement -is [Management.Automation.Language.ParenExpressionAst]) {
            $currentParenthesis = $arrowElement
            continue # and keep moving if we find one.
        }

        # Do the same thing if we found a variable, unless it's `$_`.
        if ($arrowElement -is [Management.Automation.Language.VariableExpressionAST] -and
            '$_' -ne $arrowElement) {
            $currentVariable = $arrowElement
            continue
        }
        
        
        if ($arrowElement -is [Management.Automation.Language.StringConstantExpressionAst]) {
            # If we find another arrow element, skip
            if ($arrowElement.Value -eq '=>') {
                continue
            }
            # If the elements value was ? or ?=>
            if ($arrowElement.Value -eq '?' -or 
                $arrowElement.Value -eq '?=>') {
                # treat it as a condition.
                $IsCondition = $true
                continue
            }

            # If we're generating a lambda
            if ($IsGeneratingALambda) {
                # a bareword will become the function name.
                $functionNames += $arrowElement.Value
            }
        }
            
        # Construct our parameter block (using our little script block)
        $paramBlock = . $ConvertParensAndVariablesExpressionToParameters

        $expr = # Make all expressions script like
            if ($arrowElement -is [Management.Automation.Language.ScriptBlockExpressionAst]) {
                "$arrowElement"
            } else {
                "{ $arrowElement}"
            }

        # If the current step is a condition
        if ($IsCondition) {
            # make the expression pass thru if the condition is true.
            $expr = "{ `$in = `$_; `$out = & $expr; if (`$out) { `$in } }"
        }
        

        # Set up the parameter block
        $paramBlock = 
            if ($paramBlock) {
                (
                    # (indentation and all)
                    " param(",
                    ((' ' * 8) + $paramBlock),
                    ((' ' * 8) + ')') -join 
                        [Environment]::NewLine
                ) + 
                    [Environment]::NewLine + 
                    (' ' * 8)
            } else {
                ' '
            }

        $ClosingBrace = # If we had a parameter block
            if ($paramBlock -eq ' ') {
                '}'
            } else {
                # drop the closing brace.
                [Environment]::NewLine + (' ' * 4) + '}'
            }
        
        # Add this step to the generated pipeline.
        # If the current element is a ScriptBlockExpression
        if ($arrowElement -is [Management.Automation.Language.ScriptBlockExpressionAst] -and 
            $(
                # and it has named blocks
                $arrowScriptAst = $arrowElement.ScriptBlock
                ($arrowScriptAst.ProcessBlock -or 
                $arrowScriptAst.ParamBlock -or 
                $arrowScriptAst.BeginBlock -or 
                $arrowScriptAst.CleanBlock -or
                ($arrowScriptAst.EndBlock -and -not $arrowScriptAst.EndBlock.Unnamed))
            )
        ) {
            # Then embed it directly
            $generatedPipeline += ". $arrowElement"
        } else {
            # Otherwise throw in the parameter block.
            $generatedPipeline += ". {${paramBlock}process $expr $ClosingBrace"
        }
        
        # If we were processing a condition
        if ($IsCondition) {
            # just flip that bit back and keep our paranethesis/variables.
            $IsCondition = $false
        } else {
            # Otherwise, reset our variables for the next one.
            $currentParenthesis = $null
            $currentVariable = $null
        }            
    }

    # If the arrow operator ends with a variable, consider it an assignment of the entire pipeline.
    $assignResultTo = $null
    if ($currentVariable) {
        $assignResultTo = $currentVariable
        $currentVariable = $null
    }
    
    # If the arrow operator ends with parenthesis, consider it something like a select
    if ($currentParenthesis) {
        $paramBlock = . $ConvertParensAndVariablesExpressionToParameters
    }

    # If we have current parenthesis or we're assigning, make sure our final step outputs.
    if ($currentParenthesis -or $assignResultTo) {
        $generatedPipeline += "& { $(if ($paramBlock) { "param($ParamBlock)" }) process { $(if ($paramBlock) { '[PSCustomObject]([Ordered]@{} + $PSBoundParameters)'} else { '$_'})} }"
    }


    $generatedScript =
        # If we're generating a lambda,
        if ($IsGeneratingALambda) {
            # always omit `func`
            $(if ($generatedPipeline[0] -notmatch '^func') {
                # and turn `$function` into the name of functions, if provided.
                if ($generatedPipeline[0] -match '^\$\{?function\}?' -and $functionNames) {
                    # (we can do this by using the function drive)
                    (@(foreach ($fn in $functionNames) {
                        "`${function:$fn}"
                    }) -join ' = ') + ' ='
                } else {
                    # (any path already in the function drive is handled normally)
                    ($GeneratedPipeline[0] + ' =')
                }                
            } else { '' }) + 
            [Environment]::NewLine +
            (
                # String together the rest of the pipeline
                $GeneratedPipeline[1..($generatedPipeline.Length - 1)] -join (
                    '|' + [Environment]::NewLine + (' ' * 4)
                ) -replace '^[\&\.]\s{0,}' # and strip our leading invocation operator
                # (because we want to return a [ScriptBlock], not run it)
            )
        } else {
            # If we're not generating a lambda, just join the pipeline parts together.
            $GeneratedPipeline -join (
                '|' + [Environment]::NewLine + (' ' * 4)
            )
        }
    # Join all of the parts to construct the script
    

    # If we're assigning results, the script gets a little more complex:
    if ($assignResultTo) {
        # If we're being piped to,
        if ($ArrowCommandAst -and $ArrowCommandAst.IsPipedTo) {
            # dot source, then collect inputs in a queue, then pipe to the generated script and assign the result.
            $generatedScript = ". { begin { `$q = [Collections.Queue]::new() } process { `$q.Enqueue(`$_) } end { $assignResultTo = `$q | $generatedScript }} "
        } else {
            # If we're not being piped to, just make this the result of the pipeline.
            $generatedScript = "$assignResultTo = $generatedScript"
        }        
    }

    # Output the generated script, and our arrow operator should work.
    # Pipe the generated output to Use-PipeScript to transpile any remaining syntax.
    [ScriptBlock]::Create($generatedScript) | 
        Use-PipeScript
}