powershelltype-conversion

Assigning null to a variable in a PowerShell function


I'm trying to understand null variable assignment in PowerShell, please see the below code example:

[Bool]$result = $null
Cannot convert value "" to type "System.Boolean". Boolean parameters accept only Boolean values and numbers, such as $True, $False, 1 or 0.

This makes sense. Now when using a function:

function Test-Function {

    [CmdletBinding()]
    Param (
    )
    [Bool]$result = $null
    Write-Output -InputObject "Value of 'result' is: $result"
    Write-Output -InputObject "Type is: $($result.GetType())"
}

When running the above function with Test-Function -ErrorAction Stop the below output is returned:

Value of 'result' is: False
Type is: bool

Why is the behaviour different when assigning $null to $result inside the function?

Using PowerShell 5.1 on Windows 11


Solution

  • The issue isn't easy to explain and requires diving deep into PowerShell source code. We can assert this issue only happens on strongly typed conversions a.k.a. variable constraints instead of casting ([bool] $var = $null vs $var = [bool] $null).

    The simple answer is that there is a clear distinction between how PowerShell invokes a script block when optimized vs unoptimized and this isn't documented or it isn't well documented at least. This is proven by the following test:

    $expression = { [bool] $var = $null }
    & $expression # Runs optimized, succeeds conversion
    . $expression # Runs unoptimized, fails conversion
    

    When the conversion fails, if we inspect $Error[0].Exception.StackTrace, in the first line we can see that the code path taken is thru ArgumentTypeConverterAttribute.Transform which attempts a lot of conversions before failing. Trace-Command can help you see all conversion attempts performed by PowerShell:

    Trace-Command TypeConversion { [bool] $var = $null } -PSHost
    

    As for when the conversion succeeds, when running optimized, we can see that a different code path is taken. There is a great cmdlet from the ScriptBlockDisassembler Module that can help us see what method is called:

    { [bool] $var = $null } | Get-ScriptBlockDisassembly -Minimal
    

    The following outputs:

    // ScriptBlock.EndBlock
    try
    {
        locals.Item009 = Fake.Dynamic<Func<CallSite, object, bool>>(
            PSConvertBinder.Get(typeof(bool)))(null);
    }
    catch (FlowControlException)
    {
        throw;
    }
    catch (Exception exception)
    {
        ExceptionHandlingOps.CheckActionPreference(funcContext, exception);
    }
    

    Then from here you can see that Powershell uses PSConvertBinder.Get to perform the optimized conversion. The code for this class isn't easy to follow but what is likely to happen in this case is that the conversion ends up running thru a delegate of LanguagePrimitives. To put it simple, something like this ends up happening:

    [System.Management.Automation.LanguagePrimitives]::ConvertTo($null, [bool])
    # doesn't fail, outputs `false`