-
Notifications
You must be signed in to change notification settings - Fork 7.8k
Closed
Labels
Resolution-Won't FixThe issue won't be fixed, possibly due to compatibility reason.The issue won't be fixed, possibly due to compatibility reason.WG-Enginecore PowerShell engine, interpreter, and runtimecore PowerShell engine, interpreter, and runtimeWG-ReviewedA Working Group has reviewed this and made a recommendationA Working Group has reviewed this and made a recommendation
Description
Prerequisites
- Write a descriptive title.
- Make sure you are able to repro it on the latest released version
- Search the existing issues.
- Refer to the FAQ.
- Refer to Differences between Windows PowerShell 5.1 and PowerShell.
Steps to reproduce
Save the following script as repro.ps1
, and run with pwsh -NoProfile -Command .\repro.ps1
. Description of the problem in the comments:
<#
.SYNOPSIS
This script demonstrates an unexpected coupling between the order in which you parse a
script with custom types (enums or classes) and execute the resulting Ast's
ScriptBlock: if you parse some other script before executing the type-declaring
script's code, things appear to succeed (no errors generated when executing the parsed
scripts), but attempting to use the custom types fails.
Run with: pwsh.exe -NoProfile -Command ".\repro.ps1"
(Or, for the "No Repro" case: pwsh.exe -NoProfile -Command ".\repro.ps1 -InOrder")
This script generates three files:
Types.ps1: Defines an enum ("MyEnum").
Code.ps1: Defines a function ("Foo") that uses the enum.
ReproModule.psm1: Parses and executes the .ps1 files.
In the repro case, when the module is loaded, the sequence of events is:
1. parse Types.ps1
2. parse Code.ps1
3. execute Types.ps1's Ast.GetScriptBlock()
4. execute Code.ps1's Ast.GetScriptBlock()
Finally, once the ReproModule is loaded, this script attempts to run "Foo -?".
The Expected Result is that we should see a nice help message about how to call Foo.
The actual result is an error, complaining that it can't find the MyEnum type:
InvalidOperation: C:\Users\danthom\OutOfOrderParseProblem\Code.ps1:1
Line |
1 | function Foo { [CmdletBinding()] param( [MyEnum] $ME ) $ME }
| ~~~~~~~~
| Unable to find type [MyEnum].
The -InOrder parameter changes the order of the steps performed by the
ReproModule.psm1:
1. parse Types.ps1
2. execute Types.ps1's Ast.GetScriptBlock()
3. parse Code.ps1
4. execute Code.ps1's Ast.GetScriptBlock()
When performed in this order, "Foo -?" behaves as expected.
Q: Why is this interesting / important?
Parsing a script into an AST should have *no* dependency on any kind of context.
(This Issue demonstrates that unfortunately, there is some kind of hidden dependency.)
If this hidden dependency did not exist, it would allow multi-threaded processing of
many scripts, for significant perf gains (both for first-time loading, as well as
being able to save the ASTs, to be used in other runspaces).
Example C#:
public static void ParseScripts( IList<ParsePsWorkItem> parseWorkItems )
{
Parallel.ForEach(parseWorkItems, parseWorkItem =>
{
parseWorkItem.ParseTask = File.ReadAllTextAsync(parseWorkItem.Path).ContinueWith( t =>
{
Token[] tokens ;
ParseError[] errors;
var ast = Parser.ParseInput( t.Result, parseWorkItem.Path, out tokens, out errors );
return new PsParseResult( errors, ast );
} );
});
}
And corresponding code in a .psm1:
foreach ($pwi in $parseWorkItems)
{
$parseResult = $pwi.ParseTask.GetAwaiter().GetResult()
if( $parseResult.Errors -and $parseResult.Errors.Count )
{
throw "Parse error for $($pwi.Path): $($parseResult.Errors[ 0 ])"
}
$ExecutionContext.InvokeCommand.InvokeScript( $false, # useLocalScope,
$parseResult.Ast.GetScriptBlock(),
@(), # input
@() ) # args
}
This code will *appear* to work (no errors when loading up)... but things will fail at
runtime, if your scripts use enums/classes. :'(
#>
[CmdletBinding()]
param(
[switch] $InOrder
)
'enum MyEnum { None = 0; One = 1 }' > Types.ps1
'function Foo { [CmdletBinding()] param( [MyEnum] $ME ) $ME }' > Code.ps1
# The main .psm1:
@'
$parseWorkItems = @()
foreach( $file in 'Types.ps1', 'Code.ps1' )
{
$parseWorkItems += [PSCustomObject] @{
Path = (Resolve-Path $file).ProviderPath
Errors = $null
Ast = $null
}
}
if( $env:_ParseAndExecInOrder -ne 0 )
{
# THIS IS THE "NO-REPRO" CASE; LOOK AT THE 'ELSE' BLOCK FIRST
Write-Host "We are going to parse and then immediately load each script, in order."
foreach( $pwi in $parseWorkItems )
{
$tokens = @()
$errors = @()
Write-Host "Parsing: $($pwi.Path)"
$ast = [System.Management.Automation.Language.Parser]::ParseInput( (gc -Raw $pwi.Path), $pwi.Path, ([ref] $tokens), ([ref] $errors) )
if( $errors -and $errors.Count )
{
throw "Parse error in $($pwi.Path): $($errors[ 0 ])"
}
Write-Host "Loading: $($pwi.Path)"
$ExecutionContext.InvokeCommand.InvokeScript( $false, # useLocalScope,
$ast.GetScriptBlock(),
@(), # input
@() ) # args
}
}
else
{
Write-Host "We are going to parse all scripts first, then go back and execute the results (`"out of order`")."
foreach( $pwi in $parseWorkItems )
{
$tokens = @()
$errors = @()
Write-Host "Parsing: $($pwi.Path)"
$ast = [System.Management.Automation.Language.Parser]::ParseInput( (gc -Raw $pwi.Path), $pwi.Path, ([ref] $tokens), ([ref] $errors) )
$pwi.Errors = $errors
$pwi.Ast = $ast
}
foreach( $pwi in $parseWorkItems )
{
Write-Host "Loading: $($pwi.Path)"
if( $pwi.Errors -and $pwi.Errors.Count )
{
throw "Parse error in $($pwi.Path): $($pwi.Errors[ 0 ])"
}
$ExecutionContext.InvokeCommand.InvokeScript( $false, # useLocalScope,
$pwi.Ast.GetScriptBlock(),
@(), # input
@() ) # args
}
}
'@ > ReproModule.psm1
if( $InOrder )
{
$env:_ParseAndExecInOrder = 1
}
else
{
$env:_ParseAndExecInOrder = 0
}
# Now we load the module and attempt to get the help for the Foo command.
ipmo .\ReproModule.psm1
Foo -?
# Expected result: We should see a nice help message about how to call Foo.
#
# Actual result (when !$InOrder):
#
# Parsing: C:\Users\danthom\OutOfOrderParseProblem\Types.ps1
# Parsing: C:\Users\danthom\OutOfOrderParseProblem\Code.ps1
# Loading: C:\Users\danthom\OutOfOrderParseProblem\Types.ps1
# Loading: C:\Users\danthom\OutOfOrderParseProblem\Code.ps1
# InvalidOperation: C:\Users\danthom\OutOfOrderParseProblem\Code.ps1:1
# Line |
# 1 | function Foo { [CmdletBinding()] param( [MyEnum] $ME ) $ME }
# | ~~~~~~~~
# | Unable to find type [MyEnum].
Expected behavior
NAME
Foo
SYNTAX
Foo [[-ME] {None | One}] [<CommonParameters>]
ALIASES
None
REMARKS
None
Actual behavior
InvalidOperation: C:\Users\danthom\OutOfOrderParseProblem\Code.ps1:1
Line |
1 | function Foo { [CmdletBinding()] param( [MyEnum] $ME ) $ME }
| ~~~~~~~~
| Unable to find type [MyEnum].
Error details
Exception :
Type : System.Management.Automation.RuntimeException
ErrorRecord :
Exception :
Type : System.Management.Automation.ParentContainsErrorRecordException
Message : Unable to find type [MyEnum].
HResult : -2146233087
TargetObject : MyEnum
CategoryInfo : InvalidOperation: (MyEnum:TypeName) [], ParentContainsErrorRecordException
FullyQualifiedErrorId : TypeNotFound
InvocationInfo :
ScriptLineNumber : 1
OffsetInLine : 41
HistoryId : 1
ScriptName : C:\Users\danthom\OutOfOrderParseProblem\Code.ps1
Line : function Foo { [CmdletBinding()] param( [MyEnum] $ME ) $ME }
Statement : [MyEnum]
PositionMessage : At C:\Users\danthom\OutOfOrderParseProblem\Code.ps1:1 char:41
+ function Foo { [CmdletBinding()] param( [MyEnum] $ME ) $ME }
+ ~~~~~~~~
PSScriptRoot : C:\Users\danthom\OutOfOrderParseProblem
PSCommandPath : C:\Users\danthom\OutOfOrderParseProblem\Code.ps1
CommandOrigin : Internal
ScriptStackTrace : at <ScriptBlock>, C:\Users\danthom\OutOfOrderParseProblem\repro.ps1: line 184
at <ScriptBlock>, <No file>: line 1
TargetSite :
Name : ResolveTypeName
DeclaringType : [System.Management.Automation.TypeOps]
MemberType : Method
Module : System.Management.Automation.dll
Message : Unable to find type [MyEnum].
Data : System.Collections.ListDictionaryInternal
Source : System.Management.Automation
HResult : -2146233087
StackTrace :
at System.Management.Automation.TypeOps.ResolveTypeName(ITypeName typeName, IScriptExtent errorPos)
at System.Management.Automation.Language.Compiler.GetAttribute(TypeConstraintAst typeConstraintAst)
at System.Management.Automation.Language.Compiler.GetRuntimeDefinedParameter(ParameterAst parameterAst, Boolean& customParameterSet, Boolean&
usesCmdletBinding)
at System.Management.Automation.Language.Compiler.GetParameterMetaData(ReadOnlyCollection`1 parameters, Boolean automaticPositions, Boolean&
usesCmdletBinding)
at System.Management.Automation.CompiledScriptBlockData.InitializeMetadata()
at System.Management.Automation.CompiledScriptBlockData.Compile(Boolean optimized)
at System.Management.Automation.PSScriptCmdlet..ctor(ScriptBlock scriptBlock, Boolean useNewScope, Boolean fromScriptFile, ExecutionContext context)
at System.Management.Automation.CommandProcessor.Init(IScriptCommandInfo scriptCommandInfo)
at System.Management.Automation.CommandDiscovery.GetScriptAsCmdletProcessor(IScriptCommandInfo scriptCommandInfo, ExecutionContext context, Boolean
useNewScope, Boolean fromScriptFile, SessionStateInternal sessionState)
at System.Management.Automation.CommandDiscovery.CreateCommandProcessorForScript(FunctionInfo functionInfo, ExecutionContext context, Boolean
useNewScope, SessionStateInternal sessionState)
at System.Management.Automation.CommandDiscovery.LookupCommandProcessor(CommandInfo commandInfo, CommandOrigin commandOrigin, Nullable`1 useLocalScope,
SessionStateInternal sessionState)
at System.Management.Automation.CommandDiscovery.LookupCommandProcessor(String commandName, CommandOrigin commandOrigin, Nullable`1 useLocalScope)
at System.Management.Automation.ExecutionContext.CreateCommand(String command, Boolean dotSource)
at System.Management.Automation.PipelineOps.AddCommand(PipelineProcessor pipe, CommandParameterInternal[] commandElements, CommandBaseAst
commandBaseAst, CommandRedirection[] redirections, ExecutionContext context)
at System.Management.Automation.PipelineOps.InvokePipeline(Object input, Boolean ignoreInput, CommandParameterInternal[][] pipeElements,
CommandBaseAst[] pipeElementAsts, CommandRedirection[][] commandRedirections, FunctionContext funcContext)
at System.Management.Automation.Interpreter.ActionCallInstruction`6.Run(InterpretedFrame frame)
at System.Management.Automation.Interpreter.EnterTryCatchFinallyInstruction.Run(InterpretedFrame frame)
TargetObject : MyEnum
CategoryInfo : InvalidOperation: (MyEnum:TypeName) [], RuntimeException
FullyQualifiedErrorId : TypeNotFound
InvocationInfo :
ScriptLineNumber : 1
OffsetInLine : 41
HistoryId : 1
ScriptName : C:\Users\danthom\OutOfOrderParseProblem\Code.ps1
Line : function Foo { [CmdletBinding()] param( [MyEnum] $ME ) $ME }
Statement : [MyEnum]
PositionMessage : At C:\Users\danthom\OutOfOrderParseProblem\Code.ps1:1 char:41
+ function Foo { [CmdletBinding()] param( [MyEnum] $ME ) $ME }
+ ~~~~~~~~
PSScriptRoot : C:\Users\danthom\OutOfOrderParseProblem
PSCommandPath : C:\Users\danthom\OutOfOrderParseProblem\Code.ps1
CommandOrigin : Internal
ScriptStackTrace : at <ScriptBlock>, C:\Users\danthom\OutOfOrderParseProblem\repro.ps1: line 184
at <ScriptBlock>, <No file>: line 1
Environment data
Name Value
---- -----
PSVersion 7.5.1
PSEdition Core
GitCommitId 7.5.1
OS Microsoft Windows 10.0.26422
Platform Win32NT
PSCompatibleVersions {1.0, 2.0, 3.0, 4.0…}
PSRemotingProtocolVersion 2.3
SerializationVersion 1.1.0.1
WSManStackVersion 3.0
Visuals
No response
Metadata
Metadata
Assignees
Labels
Resolution-Won't FixThe issue won't be fixed, possibly due to compatibility reason.The issue won't be fixed, possibly due to compatibility reason.WG-Enginecore PowerShell engine, interpreter, and runtimecore PowerShell engine, interpreter, and runtimeWG-ReviewedA Working Group has reviewed this and made a recommendationA Working Group has reviewed this and made a recommendation