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
19 changes: 13 additions & 6 deletions IMPLEMENTATION_PLAN.md
Original file line number Diff line number Diff line change
Expand Up @@ -378,17 +378,24 @@ priorities.
incomplete unless its separate module-baseline proof is supplied. Local
`Invoke-Command -AsJob`, ambiguous prefixes, malformed value binding,
unproved identities, and unknown receivers retain unknown/incomplete
facts. Module-qualified identities are catalogued, but the parser keeps
rejecting those forms atomically until the region-emission slice can
expose every body command. The first direct-operator sub-slice now handles
facts. Supported catalog-owned module qualifications now pass structural
admission because every possible body remains visible; the occurrence
analyzer still withholds typed receiver facts after an observed command-
resolution mutation unless the authored module qualification proves the
identity independently. The first direct-operator sub-slice now handles
currently supported command interiors in typed synchronous `& {}` and
`. {}` regions without synthetic host commands. It isolates ordinary
direct-call binding and command-resolution exit mutation, invalidates
explicitly escaping scope/provider mutation, carries shared location
outcomes, and keeps block arguments and leading `param()` declarations
atomic. Ordinary assignment state transfer remains an atomic task 7.5b
follow-up; task 7.5b is not complete. Continue with synchronous current-runspace callbacks;
child process/runspace jobs and parallel
atomic. `Measure-Command -Expression` and `Trace-Command -Expression`
now form the first command-owned vertical slice: their Main regions are
synchronous/once, execute against current-scope state, downgrade across
conflicting loop visits, and are pinned by live PowerShell probes for
current-scope, opaque-data, shadowed-command, and module-qualified
behavior. Ordinary assignment state transfer remains an atomic task 7.5b
follow-up; task 7.5b is not complete. Continue with `ForEach-Object`,
`Where-Object`, and in-process `Invoke-Command`; 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
Original file line number Diff line number Diff line change
Expand Up @@ -263,6 +263,11 @@ private static bool IsUnsupportedModuleQualifiedCmdlet(string command)
return false;
}

if (PwshExecutionRegionBindingCatalog.IsSupportedModuleQualifiedCommand(command))
{
return false;
}

var separator = command.LastIndexOf('\\');
if (separator <= 0 || separator + 1 >= command.Length)
{
Expand Down
172 changes: 158 additions & 14 deletions src/ShellSyntaxTree/Internal/Pwsh/Parsing/PwshForEachValueAnalysis.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
using System.Runtime.CompilerServices;
using System.Text;
using ShellSyntaxTree.Internal.Pwsh.Lexing;
using ShellSyntaxTree.Internal.Pwsh.Verbs;
using ShellSyntaxTree.Internal.Resolving;

namespace ShellSyntaxTree.Internal.Pwsh.Parsing;
Expand Down Expand Up @@ -978,6 +979,8 @@ internal sealed class PwshForEachValueAnalyzer
private readonly IReadOnlyList<Clause> _incompleteClauses;
private readonly Dictionary<Clause, CommandOccurrenceFacts> _facts =
new(ClauseReferenceComparer.Instance);
private readonly Dictionary<Clause, IReadOnlyList<ExecutionRegionSyntax>>
_executionRegions = new(ClauseReferenceComparer.Instance);
private bool _isComplete = true;
private int _remainingLoopAnalysisTransitions = MaxLoopAnalysisTransitions;
private long _executionRegionEffectCount;
Expand Down Expand Up @@ -1124,7 +1127,7 @@ private PwshFlowResult AnalyzeSimple(SimpleCommandSyntax simple, AnalysisContext
}

var flow = location.Value;
return ApplyExecutionRegionEffect(simple, new PwshFlowResult(
return ApplyExecutionRegionEffect(simple, current, new PwshFlowResult(
flow.OnSuccess is AnalysisContext success
? success.Invalidate(locationEffectUnknownCwd)
: null,
Expand All @@ -1133,7 +1136,7 @@ flow.OnFailure is AnalysisContext failure
: null));
}

return ApplyExecutionRegionEffect(simple, location.Value);
return ApplyExecutionRegionEffect(simple, current, location.Value);
}

if (simple.Clause.Verb.IsDynamic)
Expand All @@ -1142,6 +1145,7 @@ flow.OnFailure is AnalysisContext failure
_childScopeEscapeRiskCount++;
return ApplyExecutionRegionEffect(
simple,
current,
PwshFlowResult.Both(current.Invalidate(unknownCwd: true)));
}

Expand All @@ -1160,29 +1164,164 @@ flow.OnFailure is AnalysisContext failure

return ApplyExecutionRegionEffect(
simple,
current,
PwshFlowResult.Both(current.Invalidate(unknownCwd)));
}

return ApplyExecutionRegionEffect(simple, PwshFlowResult.Both(current));
return ApplyExecutionRegionEffect(simple, current, PwshFlowResult.Both(current));
}

private PwshFlowResult ApplyExecutionRegionEffect(
SimpleCommandSyntax simple,
AnalysisContext receiverInput,
PwshFlowResult flow)
{
if (simple.ExecutionRegions.Count == 0)
{
return flow;
}

_executionRegionEffectCount++;
return new PwshFlowResult(
flow.OnSuccess is AnalysisContext success
? success.Invalidate(unknownCwd: true)
: null,
flow.OnFailure is AnalysisContext failure
? failure.Invalidate(unknownCwd: true)
: null);
var binding = PwshExecutionRegionBindingCatalog.Bind(
simple.Clause,
IsCommandIdentityProven(simple.Clause, receiverInput));
if (!IsCurrentScopeOnceReceiver(binding) ||
!TryApplyExecutionRegionBindings(simple, binding, out var regions))
{
RecordExecutionRegions(
simple.Clause,
simple.ExecutionRegions,
simple.ExecutionRegions);
_executionRegionEffectCount++;
_childScopeEscapeRiskCount++;
return new PwshFlowResult(
flow.OnSuccess is AnalysisContext success
? success.Invalidate(unknownCwd: true)
: null,
flow.OnFailure is AnalysisContext failure
? failure.Invalidate(unknownCwd: true)
: null);
}

RecordExecutionRegions(simple.Clause, regions, simple.ExecutionRegions);
if (flow.JoinedState is not AnalysisContext regionInput)
{
return flow;
}

var executionRegionEffectCount = _executionRegionEffectCount;
var nonRegionStateMutationCount = _nonRegionStateMutationCount;
var bodyFlow = AnalyzeBlock(regions[0].Body, regionInput);
_executionRegionEffectCount = executionRegionEffectCount + 1;
_nonRegionStateMutationCount = nonRegionStateMutationCount;
return bodyFlow.JoinedState is AnalysisContext bodyExit
? PwshFlowResult.Both(bodyExit)
: flow;
}

private static bool IsCommandIdentityProven(
Clause clause,
AnalysisContext input)
{
if (!input.CommandResolutionInvalidated)
{
return true;
}

return clause.Verb.Tokens.Count == 1 &&
PwshExecutionRegionBindingCatalog.IsSupportedModuleQualifiedCommand(
clause.Verb.Tokens[0]);
}

private void RecordExecutionRegions(
Clause clause,
IReadOnlyList<ExecutionRegionSyntax> regions,
IReadOnlyList<ExecutionRegionSyntax> unknownFallback)
{
if (!_executionRegions.TryGetValue(clause, out var prior))
{
_executionRegions.Add(clause, regions);
return;
}

if (!HaveSameCompleteRegionFacts(prior, regions))
{
_executionRegions[clause] = unknownFallback;
}
}

private static bool HaveSameCompleteRegionFacts(
IReadOnlyList<ExecutionRegionSyntax> left,
IReadOnlyList<ExecutionRegionSyntax> right)
{
if (left.Count != right.Count)
{
return false;
}

for (var index = 0; index < left.Count; index++)
{
if (left[index].HostClauseElementIndex != right[index].HostClauseElementIndex ||
left[index].Phase != right[index].Phase ||
left[index].Timing != right[index].Timing ||
left[index].Cardinality != right[index].Cardinality)
{
return false;
}
}

return true;
}

private static bool IsCurrentScopeOnceReceiver(
PwshExecutionRegionBindingResult binding) =>
binding.Status == PwshExecutionRegionBindingStatus.ProvedExecution &&
binding.ParameterSet is PwshExecutionRegionParameterSet.MeasureExpression or
PwshExecutionRegionParameterSet.TraceExpression &&
binding.Bindings.Count == 1 &&
binding.Bindings[0].Timing == ExecutionRegionTiming.Synchronous &&
binding.Bindings[0].Cardinality == ExecutionRegionCardinality.Once &&
binding.Bindings[0].IsComplete;

private static bool TryApplyExecutionRegionBindings(
SimpleCommandSyntax simple,
PwshExecutionRegionBindingResult binding,
out IReadOnlyList<ExecutionRegionSyntax> regions)
{
var resolved = new ExecutionRegionSyntax[binding.Bindings.Count];
for (var bindingIndex = 0;
bindingIndex < binding.Bindings.Count;
bindingIndex++)
{
var current = binding.Bindings[bindingIndex];
ExecutionRegionSyntax? source = null;
for (var regionIndex = 0;
regionIndex < simple.ExecutionRegions.Count;
regionIndex++)
{
if (simple.ExecutionRegions[regionIndex].HostClauseElementIndex ==
current.HostClauseElementIndex)
{
source = simple.ExecutionRegions[regionIndex];
break;
}
}

if (source is null)
{
regions = Array.Empty<ExecutionRegionSyntax>();
return false;
}

resolved[bindingIndex] = source with
{
Phase = current.Phase,
Timing = current.Timing,
Cardinality = current.Cardinality,
};
}

regions = resolved;
return true;
}

private void RecordFacts(
Expand All @@ -1202,7 +1341,7 @@ private void RecordFacts(
ValueProvenance = source.ValueProvenance,
HasCompleteValueProvenance = source.HasCompleteValueProvenance,
IsComplete = source.IsComplete &&
!input.CommandResolutionInvalidated &&
IsCommandIdentityProven(simple.Clause, input) &&
(!isForEachIncomplete || mayPromote),
};
if (!_facts.TryGetValue(simple.Clause, out var prior))
Expand Down Expand Up @@ -1778,10 +1917,15 @@ private SimpleCommandSyntax RewriteSimple(
facts);
}

var executionRegions = new ExecutionRegionSyntax[simple.ExecutionRegions.Count];
var sourceRegions = _executionRegions.TryGetValue(
simple.Clause,
out var analyzedRegions)
? analyzedRegions
: simple.ExecutionRegions;
var executionRegions = new ExecutionRegionSyntax[sourceRegions.Count];
for (var index = 0; index < executionRegions.Length; index++)
{
var region = simple.ExecutionRegions[index];
var region = sourceRegions[index];
executionRegions[index] = region with
{
Body = RewriteBlock(region.Body, facts),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ public void Static_command_spellings_share_the_canonical_receiver(string source)
}

[Fact]
public void Module_qualified_receiver_is_catalogued_but_not_admitted_before_body_emission()
public void Module_qualified_receiver_is_admitted_once_body_emission_is_guaranteed()
{
var resolved = PwshExecutionRegionBindingCatalog.TryResolveStaticCommandName(
"Microsoft.PowerShell.Core\\ForEach-Object",
Expand All @@ -100,9 +100,13 @@ public void Module_qualified_receiver_is_catalogued_but_not_admitted_before_body

Assert.True(resolved);
Assert.Equal("ForEach-Object", canonical);
Assert.True(parsed.IsUnparseable);
Assert.Empty(parsed.Commands);
Assert.Empty(parsed.Clauses);
Assert.False(parsed.IsUnparseable);
Assert.Equal(2, parsed.Commands.Count);
Assert.Equal(2, parsed.Clauses.Count);
var host = Assert.IsType<SimpleCommandSyntax>(Assert.Single(parsed.Syntax.Statements));
var region = Assert.Single(host.ExecutionRegions);
Assert.Equal(ExecutionRegionPhase.Unknown, region.Phase);
Assert.All(parsed.Commands, command => Assert.False(command.IsComplete));
}

[Theory]
Expand Down
Loading