Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 7 additions & 2 deletions IMPLEMENTATION_PLAN.md
Original file line number Diff line number Diff line change
Expand Up @@ -402,8 +402,13 @@ priorities.
pin current-scope mutation, empty input, explicit input, phase order, and
the differing no-input behavior of the two cmdlets. Ordinary assignment
state transfer remains an atomic task 7.5b follow-up; task 7.5b is not
complete. Continue with in-process `Invoke-Command`; child
process/runspace jobs and parallel
complete. In-process `Invoke-Command` now distinguishes default child
variable/command scope from `-NoNewScope` current-scope flow while
propagating shared location and retaining synchronous/once region facts.
Pipelines with any supported synchronous execution region plus another
stateful stage withhold body facts that downstream initialization or
per-object interleaving can invalidate.
Child process/runspace jobs and parallel
blocks; deferred breakpoint/event/completion actions; then unknown
receiver and nested/adversarial matrices. Preserve script blocks proved
to be data as opaque values, expose ambiguous bodies with incomplete
Expand Down
105 changes: 85 additions & 20 deletions src/ShellSyntaxTree/Internal/Pwsh/Parsing/PwshForEachValueAnalysis.cs
Original file line number Diff line number Diff line change
Expand Up @@ -988,7 +988,7 @@ private readonly Dictionary<Clause, IReadOnlyList<ExecutionRegionSyntax>>
private long _locationStateMutationCount;
private long _childScopeEscapeRiskCount;
private bool _pipelineStageMayReceiveInput;
private bool _pipelineCallbacksMayInterleave;
private bool _pipelineStageEffectsMayReachRegionBodies;

private PwshForEachValueAnalyzer(
PwshParserOptions options,
Expand Down Expand Up @@ -1220,6 +1220,15 @@ flow.OnFailure is AnalysisContext failure
flow);
}

if (binding.ParameterSet == PwshExecutionRegionParameterSet.InvokeInProcess)
{
return AnalyzeInProcessInvokeCommand(
binding,
regions[0],
regionInput,
flow);
}

var executionRegionEffectCount = _executionRegionEffectCount;
var nonRegionStateMutationCount = _nonRegionStateMutationCount;
var bodyFlow = AnalyzeExecutionRegionBody(regions[0].Body, regionInput);
Expand All @@ -1230,6 +1239,48 @@ flow.OnFailure is AnalysisContext failure
: flow;
}

private PwshFlowResult AnalyzeInProcessInvokeCommand(
PwshExecutionRegionBindingResult binding,
ExecutionRegionSyntax region,
AnalysisContext input,
PwshFlowResult fallback)
{
var executionRegionEffectCount = _executionRegionEffectCount;
var nonRegionStateMutationCount = _nonRegionStateMutationCount;
var locationStateMutationCount = _locationStateMutationCount;
var childScopeEscapeRiskCount = _childScopeEscapeRiskCount;
var body = AnalyzeExecutionRegionBody(region.Body, input);
var locationMutated = _locationStateMutationCount > locationStateMutationCount;
var childScopeMayEscape =
_childScopeEscapeRiskCount > childScopeEscapeRiskCount;
_executionRegionEffectCount = executionRegionEffectCount + 1;
_nonRegionStateMutationCount = nonRegionStateMutationCount;

if (binding.HasNoNewScope)
{
return body.JoinedState is AnalysisContext bodyExit
? PwshFlowResult.Both(bodyExit)
: fallback;
}

_childScopeEscapeRiskCount = childScopeEscapeRiskCount +
(childScopeMayEscape ? 1 : 0);
var restoredExit = AnalysisContext.JoinNullable(
RestoreChildScopeExit(
body.OnSuccess,
input,
locationMutated,
childScopeMayEscape),
RestoreChildScopeExit(
body.OnFailure,
input,
locationMutated,
childScopeMayEscape));
return restoredExit is AnalysisContext joined
? PwshFlowResult.Both(joined)
: fallback;
}

private PwshFlowResult AnalyzePipelineCallbackRegions(
PwshExecutionRegionBindingResult binding,
IReadOnlyList<ExecutionRegionSyntax> regions,
Expand Down Expand Up @@ -1362,7 +1413,7 @@ private PwshFlowResult AnalyzeExecutionRegionBody(
{
var enclosingStageMayReceiveInput = _pipelineStageMayReceiveInput;
_pipelineStageMayReceiveInput = false;
var bodyInput = _pipelineCallbacksMayInterleave
var bodyInput = _pipelineStageEffectsMayReachRegionBodies
? input.Invalidate(unknownCwd: true)
: input;
var flow = AnalyzeBlock(body, bodyInput);
Expand Down Expand Up @@ -1445,6 +1496,7 @@ private static bool IsSupportedSynchronousReceiver(
binding.Status == PwshExecutionRegionBindingStatus.ProvedExecution &&
(binding.ParameterSet is PwshExecutionRegionParameterSet.MeasureExpression or
PwshExecutionRegionParameterSet.TraceExpression or
PwshExecutionRegionParameterSet.InvokeInProcess or
PwshExecutionRegionParameterSet.ForEachScriptBlock or
PwshExecutionRegionParameterSet.WhereScriptBlock) &&
AllBindingsAreCompleteAndSynchronous(binding.Bindings);
Expand Down Expand Up @@ -1606,8 +1658,10 @@ private PwshFlowResult AnalyzeList(CommandListSyntax list, AnalysisContext input
private PwshFlowResult AnalyzePipeline(PipelineSyntax pipeline, AnalysisContext input)
{
var stageInput = input;
var enclosingCallbacksMayInterleave = _pipelineCallbacksMayInterleave;
_pipelineCallbacksMayInterleave |= PipelineCallbacksMayInterleave(pipeline);
var enclosingStageEffectsMayReachBodies =
_pipelineStageEffectsMayReachRegionBodies;
_pipelineStageEffectsMayReachRegionBodies |=
PipelineStageEffectsMayReachRegionBodies(pipeline);
try
{
for (var stageIndex = 0; stageIndex < pipeline.Stages.Count; stageIndex++)
Expand Down Expand Up @@ -1644,35 +1698,43 @@ private PwshFlowResult AnalyzePipeline(PipelineSyntax pipeline, AnalysisContext
}
finally
{
_pipelineCallbacksMayInterleave = enclosingCallbacksMayInterleave;
_pipelineStageEffectsMayReachRegionBodies =
enclosingStageEffectsMayReachBodies;
}
}

private static bool PipelineCallbacksMayInterleave(PipelineSyntax pipeline)
private static bool PipelineStageEffectsMayReachRegionBodies(PipelineSyntax pipeline)
{
var hasCallback = false;
var hasPipelineSensitiveRegion = false;
var statefulStageCount = 0;
for (var index = 0; index < pipeline.Stages.Count; index++)
{
var stage = pipeline.Stages[index];
hasCallback |= ContainsPipelineCallback(stage);
hasPipelineSensitiveRegion |= ContainsPipelineSensitiveExecutionRegion(stage);
if (MayMutatePipelineState(stage))
{
statefulStageCount++;
}
}

return hasCallback && statefulStageCount > 1;
return hasPipelineSensitiveRegion && statefulStageCount > 1;
}

private static bool ContainsPipelineCallback(ShellSyntaxNode node) =>
private static bool ContainsPipelineSensitiveExecutionRegion(ShellSyntaxNode node) =>
node switch
{
SimpleCommandSyntax simple => IsPipelineCallback(simple),
ShellBlockSyntax block => BlockContains(block, ContainsPipelineCallback),
GroupSyntax group => ContainsPipelineCallback(group.Body),
CommandListSyntax list => ListContains(list, ContainsPipelineCallback),
PipelineSyntax pipeline => PipelineContains(pipeline, ContainsPipelineCallback),
SimpleCommandSyntax simple => IsPipelineSensitiveExecutionRegionHost(simple),
ShellBlockSyntax block => BlockContains(
block,
ContainsPipelineSensitiveExecutionRegion),
GroupSyntax group => ContainsPipelineSensitiveExecutionRegion(group.Body),
CommandListSyntax list => ListContains(
list,
ContainsPipelineSensitiveExecutionRegion),
PipelineSyntax pipeline => PipelineContains(
pipeline,
ContainsPipelineSensitiveExecutionRegion),
ExecutionRegionSyntax => true,
_ => false,
};

Expand All @@ -1690,14 +1752,17 @@ private static bool MayMutatePipelineState(ShellSyntaxNode node) =>
_ => false,
};

private static bool IsPipelineCallback(SimpleCommandSyntax simple)
private static bool IsPipelineSensitiveExecutionRegionHost(SimpleCommandSyntax simple)
{
var binding = PwshExecutionRegionBindingCatalog.Bind(
simple.Clause,
commandIdentityProven: true);
return binding.Status == PwshExecutionRegionBindingStatus.ProvedExecution &&
binding.ParameterSet is PwshExecutionRegionParameterSet.ForEachScriptBlock or
PwshExecutionRegionParameterSet.WhereScriptBlock;
PwshExecutionRegionParameterSet.WhereScriptBlock or
PwshExecutionRegionParameterSet.InvokeInProcess or
PwshExecutionRegionParameterSet.MeasureExpression or
PwshExecutionRegionParameterSet.TraceExpression;
}

private static bool SimpleMayMutatePipelineState(SimpleCommandSyntax simple)
Expand Down Expand Up @@ -1818,19 +1883,19 @@ private PwshFlowResult AnalyzeExecutionRegion(
}

return new PwshFlowResult(
RestoreDirectCallExit(
RestoreChildScopeExit(
body.OnSuccess,
input,
locationMutated,
childScopeMayEscape),
RestoreDirectCallExit(
RestoreChildScopeExit(
body.OnFailure,
input,
locationMutated,
childScopeMayEscape));
}

private static AnalysisContext? RestoreDirectCallExit(
private static AnalysisContext? RestoreChildScopeExit(
AnalysisContext? bodyExit,
AnalysisContext input,
bool locationMutated,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,8 @@ internal sealed record PwshExecutionRegionBindingResult

internal bool HasExplicitInputObject { get; init; }

internal bool HasNoNewScope { get; init; }

internal IReadOnlyList<PwshExecutionRegionBinding> Bindings { get; init; } =
Array.Empty<PwshExecutionRegionBinding>();
}
Expand Down Expand Up @@ -443,6 +445,8 @@ private static PwshExecutionRegionBindingResult BindReceiver(
ParameterSet = parameterSet,
CanonicalCommandName = canonicalName,
HasExplicitInputObject = arguments.HasNamed("InputObject"),
HasNoNewScope = receiver == PwshExecutionRegionReceiver.InvokeCommand &&
arguments.IsSwitchEnabled("NoNewScope"),
Bindings = bindings.OrderBy(binding => binding.HostClauseElementIndex).ToArray(),
};
}
Expand Down Expand Up @@ -1572,6 +1576,33 @@ internal void AddNamedParameter(string name)

internal bool HasNamed(string name) => NamedParameters.Contains(name);

internal bool IsSwitchEnabled(string name)
{
if (!HasNamed(name))
{
return false;
}

foreach (var argument in NamedArguments)
{
if (!string.Equals(
argument.ParameterName,
name,
StringComparison.OrdinalIgnoreCase))
{
continue;
}

return string.Equals(
argument.Value,
"$true",
StringComparison.OrdinalIgnoreCase) ||
argument.Value == "1";
}

return true;
}

internal bool HasAnyNamed(params string[] names) => names.Any(HasNamed);

internal int CountNamed(params string[] names) => names.Count(HasNamed);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -297,6 +297,20 @@ public void Invoke_command_distinguishes_in_process_and_remote_parameter_sets()

Assert.Equal(PwshExecutionRegionParameterSet.InvokeInProcess, local.ParameterSet);
Assert.True(Assert.Single(local.Bindings).IsComplete);
Assert.True(local.HasNoNewScope);
var positionalLocal = Bind("Invoke-Command { Get-Date }");
Assert.Equal(
PwshExecutionRegionParameterSet.InvokeInProcess,
positionalLocal.ParameterSet);
Assert.False(positionalLocal.HasNoNewScope);
Assert.False(Bind(
"Invoke-Command -NoNewScope:$false { Get-Date }").HasNoNewScope);
Assert.True(Bind(
"Invoke-Command -NoNewScope:$true { Get-Date }").HasNoNewScope);
Assert.False(Bind(
"Invoke-Command -NoNewScope:0 { Get-Date }").HasNoNewScope);
Assert.True(Bind(
"Invoke-Command -NoNewScope:1 { Get-Date }").HasNoNewScope);
Assert.Equal(PwshExecutionRegionParameterSet.InvokeRemote, remote.ParameterSet);
var remoteBinding = Assert.Single(remote.Bindings);
Assert.Equal(ExecutionRegionTiming.Unknown, remoteBinding.Timing);
Expand Down
Loading