Skip to content

Commit fa1d441

Browse files
Analyze local Invoke-Command regions (#109)
* Analyze local Invoke-Command regions * Make PowerShell oracle cross-platform
1 parent 4251496 commit fa1d441

6 files changed

Lines changed: 384 additions & 22 deletions

File tree

IMPLEMENTATION_PLAN.md

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -402,8 +402,13 @@ priorities.
402402
pin current-scope mutation, empty input, explicit input, phase order, and
403403
the differing no-input behavior of the two cmdlets. Ordinary assignment
404404
state transfer remains an atomic task 7.5b follow-up; task 7.5b is not
405-
complete. Continue with in-process `Invoke-Command`; child
406-
process/runspace jobs and parallel
405+
complete. In-process `Invoke-Command` now distinguishes default child
406+
variable/command scope from `-NoNewScope` current-scope flow while
407+
propagating shared location and retaining synchronous/once region facts.
408+
Pipelines with any supported synchronous execution region plus another
409+
stateful stage withhold body facts that downstream initialization or
410+
per-object interleaving can invalidate.
411+
Child process/runspace jobs and parallel
407412
blocks; deferred breakpoint/event/completion actions; then unknown
408413
receiver and nested/adversarial matrices. Preserve script blocks proved
409414
to be data as opaque values, expose ambiguous bodies with incomplete

src/ShellSyntaxTree/Internal/Pwsh/Parsing/PwshForEachValueAnalysis.cs

Lines changed: 85 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -988,7 +988,7 @@ private readonly Dictionary<Clause, IReadOnlyList<ExecutionRegionSyntax>>
988988
private long _locationStateMutationCount;
989989
private long _childScopeEscapeRiskCount;
990990
private bool _pipelineStageMayReceiveInput;
991-
private bool _pipelineCallbacksMayInterleave;
991+
private bool _pipelineStageEffectsMayReachRegionBodies;
992992

993993
private PwshForEachValueAnalyzer(
994994
PwshParserOptions options,
@@ -1220,6 +1220,15 @@ flow.OnFailure is AnalysisContext failure
12201220
flow);
12211221
}
12221222

1223+
if (binding.ParameterSet == PwshExecutionRegionParameterSet.InvokeInProcess)
1224+
{
1225+
return AnalyzeInProcessInvokeCommand(
1226+
binding,
1227+
regions[0],
1228+
regionInput,
1229+
flow);
1230+
}
1231+
12231232
var executionRegionEffectCount = _executionRegionEffectCount;
12241233
var nonRegionStateMutationCount = _nonRegionStateMutationCount;
12251234
var bodyFlow = AnalyzeExecutionRegionBody(regions[0].Body, regionInput);
@@ -1230,6 +1239,48 @@ flow.OnFailure is AnalysisContext failure
12301239
: flow;
12311240
}
12321241

1242+
private PwshFlowResult AnalyzeInProcessInvokeCommand(
1243+
PwshExecutionRegionBindingResult binding,
1244+
ExecutionRegionSyntax region,
1245+
AnalysisContext input,
1246+
PwshFlowResult fallback)
1247+
{
1248+
var executionRegionEffectCount = _executionRegionEffectCount;
1249+
var nonRegionStateMutationCount = _nonRegionStateMutationCount;
1250+
var locationStateMutationCount = _locationStateMutationCount;
1251+
var childScopeEscapeRiskCount = _childScopeEscapeRiskCount;
1252+
var body = AnalyzeExecutionRegionBody(region.Body, input);
1253+
var locationMutated = _locationStateMutationCount > locationStateMutationCount;
1254+
var childScopeMayEscape =
1255+
_childScopeEscapeRiskCount > childScopeEscapeRiskCount;
1256+
_executionRegionEffectCount = executionRegionEffectCount + 1;
1257+
_nonRegionStateMutationCount = nonRegionStateMutationCount;
1258+
1259+
if (binding.HasNoNewScope)
1260+
{
1261+
return body.JoinedState is AnalysisContext bodyExit
1262+
? PwshFlowResult.Both(bodyExit)
1263+
: fallback;
1264+
}
1265+
1266+
_childScopeEscapeRiskCount = childScopeEscapeRiskCount +
1267+
(childScopeMayEscape ? 1 : 0);
1268+
var restoredExit = AnalysisContext.JoinNullable(
1269+
RestoreChildScopeExit(
1270+
body.OnSuccess,
1271+
input,
1272+
locationMutated,
1273+
childScopeMayEscape),
1274+
RestoreChildScopeExit(
1275+
body.OnFailure,
1276+
input,
1277+
locationMutated,
1278+
childScopeMayEscape));
1279+
return restoredExit is AnalysisContext joined
1280+
? PwshFlowResult.Both(joined)
1281+
: fallback;
1282+
}
1283+
12331284
private PwshFlowResult AnalyzePipelineCallbackRegions(
12341285
PwshExecutionRegionBindingResult binding,
12351286
IReadOnlyList<ExecutionRegionSyntax> regions,
@@ -1362,7 +1413,7 @@ private PwshFlowResult AnalyzeExecutionRegionBody(
13621413
{
13631414
var enclosingStageMayReceiveInput = _pipelineStageMayReceiveInput;
13641415
_pipelineStageMayReceiveInput = false;
1365-
var bodyInput = _pipelineCallbacksMayInterleave
1416+
var bodyInput = _pipelineStageEffectsMayReachRegionBodies
13661417
? input.Invalidate(unknownCwd: true)
13671418
: input;
13681419
var flow = AnalyzeBlock(body, bodyInput);
@@ -1445,6 +1496,7 @@ private static bool IsSupportedSynchronousReceiver(
14451496
binding.Status == PwshExecutionRegionBindingStatus.ProvedExecution &&
14461497
(binding.ParameterSet is PwshExecutionRegionParameterSet.MeasureExpression or
14471498
PwshExecutionRegionParameterSet.TraceExpression or
1499+
PwshExecutionRegionParameterSet.InvokeInProcess or
14481500
PwshExecutionRegionParameterSet.ForEachScriptBlock or
14491501
PwshExecutionRegionParameterSet.WhereScriptBlock) &&
14501502
AllBindingsAreCompleteAndSynchronous(binding.Bindings);
@@ -1606,8 +1658,10 @@ private PwshFlowResult AnalyzeList(CommandListSyntax list, AnalysisContext input
16061658
private PwshFlowResult AnalyzePipeline(PipelineSyntax pipeline, AnalysisContext input)
16071659
{
16081660
var stageInput = input;
1609-
var enclosingCallbacksMayInterleave = _pipelineCallbacksMayInterleave;
1610-
_pipelineCallbacksMayInterleave |= PipelineCallbacksMayInterleave(pipeline);
1661+
var enclosingStageEffectsMayReachBodies =
1662+
_pipelineStageEffectsMayReachRegionBodies;
1663+
_pipelineStageEffectsMayReachRegionBodies |=
1664+
PipelineStageEffectsMayReachRegionBodies(pipeline);
16111665
try
16121666
{
16131667
for (var stageIndex = 0; stageIndex < pipeline.Stages.Count; stageIndex++)
@@ -1644,35 +1698,43 @@ private PwshFlowResult AnalyzePipeline(PipelineSyntax pipeline, AnalysisContext
16441698
}
16451699
finally
16461700
{
1647-
_pipelineCallbacksMayInterleave = enclosingCallbacksMayInterleave;
1701+
_pipelineStageEffectsMayReachRegionBodies =
1702+
enclosingStageEffectsMayReachBodies;
16481703
}
16491704
}
16501705

1651-
private static bool PipelineCallbacksMayInterleave(PipelineSyntax pipeline)
1706+
private static bool PipelineStageEffectsMayReachRegionBodies(PipelineSyntax pipeline)
16521707
{
1653-
var hasCallback = false;
1708+
var hasPipelineSensitiveRegion = false;
16541709
var statefulStageCount = 0;
16551710
for (var index = 0; index < pipeline.Stages.Count; index++)
16561711
{
16571712
var stage = pipeline.Stages[index];
1658-
hasCallback |= ContainsPipelineCallback(stage);
1713+
hasPipelineSensitiveRegion |= ContainsPipelineSensitiveExecutionRegion(stage);
16591714
if (MayMutatePipelineState(stage))
16601715
{
16611716
statefulStageCount++;
16621717
}
16631718
}
16641719

1665-
return hasCallback && statefulStageCount > 1;
1720+
return hasPipelineSensitiveRegion && statefulStageCount > 1;
16661721
}
16671722

1668-
private static bool ContainsPipelineCallback(ShellSyntaxNode node) =>
1723+
private static bool ContainsPipelineSensitiveExecutionRegion(ShellSyntaxNode node) =>
16691724
node switch
16701725
{
1671-
SimpleCommandSyntax simple => IsPipelineCallback(simple),
1672-
ShellBlockSyntax block => BlockContains(block, ContainsPipelineCallback),
1673-
GroupSyntax group => ContainsPipelineCallback(group.Body),
1674-
CommandListSyntax list => ListContains(list, ContainsPipelineCallback),
1675-
PipelineSyntax pipeline => PipelineContains(pipeline, ContainsPipelineCallback),
1726+
SimpleCommandSyntax simple => IsPipelineSensitiveExecutionRegionHost(simple),
1727+
ShellBlockSyntax block => BlockContains(
1728+
block,
1729+
ContainsPipelineSensitiveExecutionRegion),
1730+
GroupSyntax group => ContainsPipelineSensitiveExecutionRegion(group.Body),
1731+
CommandListSyntax list => ListContains(
1732+
list,
1733+
ContainsPipelineSensitiveExecutionRegion),
1734+
PipelineSyntax pipeline => PipelineContains(
1735+
pipeline,
1736+
ContainsPipelineSensitiveExecutionRegion),
1737+
ExecutionRegionSyntax => true,
16761738
_ => false,
16771739
};
16781740

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

1693-
private static bool IsPipelineCallback(SimpleCommandSyntax simple)
1755+
private static bool IsPipelineSensitiveExecutionRegionHost(SimpleCommandSyntax simple)
16941756
{
16951757
var binding = PwshExecutionRegionBindingCatalog.Bind(
16961758
simple.Clause,
16971759
commandIdentityProven: true);
16981760
return binding.Status == PwshExecutionRegionBindingStatus.ProvedExecution &&
16991761
binding.ParameterSet is PwshExecutionRegionParameterSet.ForEachScriptBlock or
1700-
PwshExecutionRegionParameterSet.WhereScriptBlock;
1762+
PwshExecutionRegionParameterSet.WhereScriptBlock or
1763+
PwshExecutionRegionParameterSet.InvokeInProcess or
1764+
PwshExecutionRegionParameterSet.MeasureExpression or
1765+
PwshExecutionRegionParameterSet.TraceExpression;
17011766
}
17021767

17031768
private static bool SimpleMayMutatePipelineState(SimpleCommandSyntax simple)
@@ -1818,19 +1883,19 @@ private PwshFlowResult AnalyzeExecutionRegion(
18181883
}
18191884

18201885
return new PwshFlowResult(
1821-
RestoreDirectCallExit(
1886+
RestoreChildScopeExit(
18221887
body.OnSuccess,
18231888
input,
18241889
locationMutated,
18251890
childScopeMayEscape),
1826-
RestoreDirectCallExit(
1891+
RestoreChildScopeExit(
18271892
body.OnFailure,
18281893
input,
18291894
locationMutated,
18301895
childScopeMayEscape));
18311896
}
18321897

1833-
private static AnalysisContext? RestoreDirectCallExit(
1898+
private static AnalysisContext? RestoreChildScopeExit(
18341899
AnalysisContext? bodyExit,
18351900
AnalysisContext input,
18361901
bool locationMutated,

src/ShellSyntaxTree/Internal/Pwsh/Verbs/PwshExecutionRegionBindingCatalog.cs

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,8 @@ internal sealed record PwshExecutionRegionBindingResult
9292

9393
internal bool HasExplicitInputObject { get; init; }
9494

95+
internal bool HasNoNewScope { get; init; }
96+
9597
internal IReadOnlyList<PwshExecutionRegionBinding> Bindings { get; init; } =
9698
Array.Empty<PwshExecutionRegionBinding>();
9799
}
@@ -443,6 +445,8 @@ private static PwshExecutionRegionBindingResult BindReceiver(
443445
ParameterSet = parameterSet,
444446
CanonicalCommandName = canonicalName,
445447
HasExplicitInputObject = arguments.HasNamed("InputObject"),
448+
HasNoNewScope = receiver == PwshExecutionRegionReceiver.InvokeCommand &&
449+
arguments.IsSwitchEnabled("NoNewScope"),
446450
Bindings = bindings.OrderBy(binding => binding.HostClauseElementIndex).ToArray(),
447451
};
448452
}
@@ -1572,6 +1576,33 @@ internal void AddNamedParameter(string name)
15721576

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

1579+
internal bool IsSwitchEnabled(string name)
1580+
{
1581+
if (!HasNamed(name))
1582+
{
1583+
return false;
1584+
}
1585+
1586+
foreach (var argument in NamedArguments)
1587+
{
1588+
if (!string.Equals(
1589+
argument.ParameterName,
1590+
name,
1591+
StringComparison.OrdinalIgnoreCase))
1592+
{
1593+
continue;
1594+
}
1595+
1596+
return string.Equals(
1597+
argument.Value,
1598+
"$true",
1599+
StringComparison.OrdinalIgnoreCase) ||
1600+
argument.Value == "1";
1601+
}
1602+
1603+
return true;
1604+
}
1605+
15751606
internal bool HasAnyNamed(params string[] names) => names.Any(HasNamed);
15761607

15771608
internal int CountNamed(params string[] names) => names.Count(HasNamed);

tests/ShellSyntaxTree.Tests/Parsing/PwshExecutionRegionBindingCatalogTests.cs

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -297,6 +297,20 @@ public void Invoke_command_distinguishes_in_process_and_remote_parameter_sets()
297297

298298
Assert.Equal(PwshExecutionRegionParameterSet.InvokeInProcess, local.ParameterSet);
299299
Assert.True(Assert.Single(local.Bindings).IsComplete);
300+
Assert.True(local.HasNoNewScope);
301+
var positionalLocal = Bind("Invoke-Command { Get-Date }");
302+
Assert.Equal(
303+
PwshExecutionRegionParameterSet.InvokeInProcess,
304+
positionalLocal.ParameterSet);
305+
Assert.False(positionalLocal.HasNoNewScope);
306+
Assert.False(Bind(
307+
"Invoke-Command -NoNewScope:$false { Get-Date }").HasNoNewScope);
308+
Assert.True(Bind(
309+
"Invoke-Command -NoNewScope:$true { Get-Date }").HasNoNewScope);
310+
Assert.False(Bind(
311+
"Invoke-Command -NoNewScope:0 { Get-Date }").HasNoNewScope);
312+
Assert.True(Bind(
313+
"Invoke-Command -NoNewScope:1 { Get-Date }").HasNoNewScope);
300314
Assert.Equal(PwshExecutionRegionParameterSet.InvokeRemote, remote.ParameterSet);
301315
var remoteBinding = Assert.Single(remote.Bindings);
302316
Assert.Equal(ExecutionRegionTiming.Unknown, remoteBinding.Timing);

0 commit comments

Comments
 (0)