Transpilers/Syntax/ConditionalKeyword.psx.ps1

<#
.SYNOPSIS
    Conditional Keyword Expansion
.DESCRIPTION
    Allows for conditional Keywords.

    A coniditional keyword is a continue or break statement, followed by a partial if clause.
    
    The following keywords can be used conditionally:

    |Keyword |Example |
    |---------|----------------- |
    |break |`break if $true` |
    |continue |`continue if $true`|
    |return |`return if $true` |
    |throw |`throw if $true` |
.EXAMPLE
    Invoke-PipeScript {
        $n = 1
        do {
            $n = $n * 2
            $n
            break if (-not ($n % 16))
        } while ($true)
    }
.EXAMPLE
    Import-PipeScript {
        
        function Get-Primes([ValidateRange(2,64kb)][int]$UpTo) {
            $KnownPrimes = new Collections.ArrayList @(2)
            $SieveOfEratosthenes = new Collections.Generic.Dictionary[uint32,bool]
            $n = 2
            :nextNumber for (; $n++;) {
                # Break if past our point of interest
                break if ($n -ge $upTo)
                # Skip if an even number
                continue if (-not ($n -band 1))
                # Check our sieve
                continue if $SieveOfEratosthenes.ContainsKey($n)
                # Determine half of the number
                $halfN = $n /2
                # If this is divisible by the known primes
                foreach ($k in $knownPrimes) {
                    continue nextNumber if (($n % $k) -eq 0) {}
                    break if ($k -ge $halfN)
                }
                foreach ($k in $knownPrimes) {
                    $SieveOfEratosthenes[$n * $k] = $true
                }
                $null = $knownPrimes.Add($n)
            }
            $knownPrimes -le $UpTo
        }
    }
#>

[ValidateScript({
    $ast = $_
    # If the AST is not a break, continue, return, or throw statement
    if ($ast -isnot [Management.Automation.Language.ContinueStatementAst] -and 
        $ast -isnot [Management.Automation.Language.BreakStatementAst] -and
        $ast -isnot [Management.Automation.Language.ReturnStatementAst] -and
        $ast -isnot [Management.Automation.Language.ThrowStatementAst]
    ) {
        return $false # it is not valid for this transpiler.
    }

    # If there is no pipeline
    if (-not $ast.Pipeline) {
        # find the next statement
        $nextStatement = $ast.Parent.Statements[$ast.Parent.Statements.IndexOf($ast) + 1]
        # (If there isn't one, it's invalid)
        if (-not $nextStatement) {  return $false }

        # If the label was 'if', it's valid.
        if ('if' -eq $ast.Label) { return $true }

        # If the label is not if, it could be an actual label
        # If the next statement is an if Statement, we're good
        if ($nextStatement -is [Management.Automation.Language.IfStatementAst]) {
            return $true
        }
        # If it wasn't, it's valid
        return $false    
    }
    else {
        # If there is a pipeline, it must start with 'if'
        return $ast.Pipeline -match '^if'
    }    
})]
param(
# A Continue Statement.
[Parameter(Mandatory,ValueFromPipeline,ParameterSetName='ContinueStatement')]
[Management.Automation.Language.ContinueStatementAst]
$ContinueStatement,

# A Break Statement.
[Parameter(Mandatory,ValueFromPipeline,ParameterSetName='BreakStatement')]
[Management.Automation.Language.BreakStatementAst]
$BreakStatement,

# A Return Statement.
[Parameter(Mandatory,ValueFromPipeline,ParameterSetName='ReturnStatement')]
[Management.Automation.Language.ReturnStatementAst]
$ReturnStatement,

# A Throw Statement.
[Parameter(Mandatory,ValueFromPipeline,ParameterSetName='ThrowStatement')]
[Management.Automation.Language.ThrowStatementAst]
$ThrowStatement
)

process {
    # Get the statement that will be transformed
    $statement = $($PSBoundParameters[$PSBoundParameters.Keys -like '*Statement'])
    # (and return if we do not find one)
    if (-not $statement) { return }

    # Force the statement to be lowercase
    $statementType = ($statement.GetType().Name -replace 'StatementAst').ToLower()
    
    # There are two types of conditional keywords:
    # Labeled statements (continue/break)
    # Pipelined statements (return/throw)
    # For labeled statements, we will need to skip until a point
    $skipUntil = $null
    # For pipelined statements, we need to collect and script expression clauses
    $PipelineClauses = @()

    $nextStatement = 
        # If we are dealing with a labeled statement
        if (-not $statement.Pipeline) {
            # we will always skip the next statement
            $skipUntil = $statement.Parent.Statements[$statement.Parent.Statements.IndexOf($statement) + 1]
            $skipUntil
        } else {
            # Otherwise, go thru all command elements in the pipeline
            $firstElement = $statement.Pipeline.PipelineElements[0]
            # (skipping the first)
            $commandElements = $firstElement.CommandElements[1..($firstElement.CommandElements.Count)]
            foreach ($CommandElement in $commandElements) {
                # any script block expression makes up the closing clause
                if ($commandElement -is [Management.Automation.Language.ScriptBlockExpressionAst]) {
                    $PipelineClauses += $CommandElement.ScriptBlock.AsScriptBlock()
                } else {
                    # and all other command elements make up the condition.
                    $CommandElement
                }
            }
        }
        
    
    $(if ($nextStatement -is [Management.Automation.Language.IfStatementAst]) {
        # If the next statement was an if, recreate it's first clause
        [ScriptBlock]::Create("if ($($nextStatement.Clauses[0].Item1)) {
            $(
                $ReplacedClause = $nextStatement.Clauses[0].Item2 -replace '^\s{0,}\{\s{0,}' -replace '\s{0,}\}\s{0,}$'
                # If the clause was not empty,
                if ($ReplacedClause) {
                    # add continue or break to the label.
                    $ReplacedClause + ';' + "$statementType $($statement.Label)"
                } else {
                    # otherwise, continue or break to the label.
                    "$statementType $($statement.Label)"
                }
            )
        }"
)
    } 
    elseif ($PipelineClauses) {
        # If we had pipeline clauses, include them as is.
        [ScriptBlock]::Create("if ($nextStatement) { $statementType $pipelineClauses}")
    }
    else {
        # Otherwise, make a simple if statement.
        [ScriptBlock]::Create("if ($nextStatement) { $statementType }")
    })
        | Add-Member NoteProperty SkipUntil $skipUntil -PassThru
}