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
12 changes: 11 additions & 1 deletion IMPLEMENTATION_PLAN.md
Original file line number Diff line number Diff line change
Expand Up @@ -415,7 +415,17 @@ priorities.
resolution preferences, and retains conservative host command-resolution
invalidation for exported functions across canonical, alias, and supported
module-qualified identities.
Child process/runspace jobs and parallel
`Start-Job` now schedules initialization before main in an isolated child
process state, inherits or applies the invocation working directory, and
prevents child exit mutation from contaminating the host continuation.
Inline working-directory values retain exact value provenance, while an
explicit relative working directory remains unknown because PowerShell
resolves it from a platform-specific child startup location rather than
the caller location. Known but unsupported job variants retain their
proved child-process isolation while their body analysis stays fail
closed; an explicit alternate `-PSVersion` remains visible but incomplete
because it falls outside the pinned PowerShell 7 runtime model.
Child 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
176 changes: 159 additions & 17 deletions src/ShellSyntaxTree/Internal/Pwsh/Parsing/PwshForEachValueAnalysis.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1202,17 +1202,26 @@ private PwshFlowResult ApplyExecutionRegionEffect(
return flow;
}

var commandIdentityProven = IsCommandIdentityProven(
simple.Clause,
receiverInput);
var binding = PwshExecutionRegionBindingCatalog.Bind(
simple.Clause,
IsCommandIdentityProven(simple.Clause, receiverInput));
if (!IsSupportedSynchronousReceiver(binding) ||
commandIdentityProven);
if (!IsSupportedExecutionRegionReceiver(binding) ||
!TryApplyExecutionRegionBindings(simple, binding, out var regions))
{
RecordExecutionRegions(
simple.Clause,
simple.ExecutionRegions,
simple.ExecutionRegions);
_executionRegionEffectCount++;
if (commandIdentityProven &&
binding.Receiver == PwshExecutionRegionReceiver.StartJob)
{
return flow;
}

_childScopeEscapeRiskCount++;
return new PwshFlowResult(
flow.OnSuccess is AnalysisContext success
Expand Down Expand Up @@ -1257,6 +1266,11 @@ flow.OnFailure is AnalysisContext failure
return AnalyzeNewModule(regions[0], bodyInput, flow);
}

if (binding.ParameterSet == PwshExecutionRegionParameterSet.StartJobScriptBlock)
{
return AnalyzeStartJob(simple, binding, regions, receiverInput, flow);
}

var executionRegionEffectCount = _executionRegionEffectCount;
var nonRegionStateMutationCount = _nonRegionStateMutationCount;
var bodyFlow = AnalyzeExecutionRegionBody(regions[0].Body, regionInput);
Expand Down Expand Up @@ -1305,6 +1319,92 @@ private PwshFlowResult AnalyzeNewModule(
return PwshFlowResult.Both(hostExit.WithCwd(bodyExit.WorkingDirectory));
}

private PwshFlowResult AnalyzeStartJob(
SimpleCommandSyntax simple,
PwshExecutionRegionBindingResult binding,
IReadOnlyList<ExecutionRegionSyntax> regions,
AnalysisContext receiverInput,
PwshFlowResult hostFlow)
{
var executionRegionEffectCount = _executionRegionEffectCount;
var nonRegionStateMutationCount = _nonRegionStateMutationCount;
var locationStateMutationCount = _locationStateMutationCount;
var childScopeEscapeRiskCount = _childScopeEscapeRiskCount;
try
{
var child = CreateStartJobInput(simple, binding, receiverInput);
if (TryAnalyzeRegionPhase(
regions,
ExecutionRegionPhase.Initialization,
child,
out var initialized))
{
TryAnalyzeRegionPhase(
regions,
ExecutionRegionPhase.Main,
initialized,
out _);
}
}
finally
{
_executionRegionEffectCount = executionRegionEffectCount + regions.Count;
_nonRegionStateMutationCount = nonRegionStateMutationCount;
_locationStateMutationCount = locationStateMutationCount;
_childScopeEscapeRiskCount = childScopeEscapeRiskCount;
}

return hostFlow;
}

private AnalysisContext CreateStartJobInput(
SimpleCommandSyntax simple,
PwshExecutionRegionBindingResult binding,
AnalysisContext receiverInput)
{
var child = receiverInput
.Invalidate(unknownCwd: false, invalidateCommandResolution: false)
.WithoutBindings();
if (binding.WorkingDirectoryElementIndex is not int elementIndex)
{
return child;
}

if (!TryGetElementValue(
simple,
elementIndex,
binding.WorkingDirectoryValueOffset,
out var targetValue))
{
return child.WithCwd(workingDirectory: null);
}

if (receiverInput.TryEvaluateValue(targetValue, out var domain) &&
domain.Kind == ShellValueDomainKind.Exact &&
domain.Values.Count == 1)
{
targetValue = ShellValue.Literal(domain.Values[0]);
}

var options = new PwshParserOptions
{
HomeDirectory = _options.HomeDirectory,
WorkingDirectory = receiverInput.WorkingDirectory,
InitialStateMode = _options.InitialStateMode,
};
var resolved = PwshResolver.Resolve(
targetValue,
treatAsPath: true,
options,
workingDirectoryUnknown: true,
ShellResolutionConsumer.PowerShellCmdletPath);
return child.WithCwd(
resolved.IsPath &&
resolved.Resolved is not null
? resolved.Resolved
: null);
}

private PwshFlowResult AnalyzeInProcessInvokeCommand(
PwshExecutionRegionBindingResult binding,
ExecutionRegionSyntax region,
Expand Down Expand Up @@ -1557,19 +1657,37 @@ private static bool HaveSameCompleteRegionFacts(
return true;
}

private static bool IsSupportedSynchronousReceiver(
PwshExecutionRegionBindingResult binding) =>
binding.Status == PwshExecutionRegionBindingStatus.ProvedExecution &&
(binding.ParameterSet is PwshExecutionRegionParameterSet.MeasureExpression or
PwshExecutionRegionParameterSet.TraceExpression or
PwshExecutionRegionParameterSet.InvokeInProcess or
PwshExecutionRegionParameterSet.NewModuleScriptBlock or
PwshExecutionRegionParameterSet.ForEachScriptBlock or
PwshExecutionRegionParameterSet.WhereScriptBlock) &&
AllBindingsAreCompleteAndSynchronous(binding.Bindings);
private static bool IsSupportedExecutionRegionReceiver(
PwshExecutionRegionBindingResult binding)
{
if (binding.Status != PwshExecutionRegionBindingStatus.ProvedExecution)
{
return false;
}

private static bool AllBindingsAreCompleteAndSynchronous(
IReadOnlyList<PwshExecutionRegionBinding> bindings)
return binding.ParameterSet switch
{
PwshExecutionRegionParameterSet.StartJobScriptBlock =>
!binding.HasExplicitPSVersion &&
AllBindingsAreCompleteWithTiming(
binding.Bindings,
ExecutionRegionTiming.Concurrent),
PwshExecutionRegionParameterSet.MeasureExpression or
PwshExecutionRegionParameterSet.TraceExpression or
PwshExecutionRegionParameterSet.InvokeInProcess or
PwshExecutionRegionParameterSet.NewModuleScriptBlock or
PwshExecutionRegionParameterSet.ForEachScriptBlock or
PwshExecutionRegionParameterSet.WhereScriptBlock =>
AllBindingsAreCompleteWithTiming(
binding.Bindings,
ExecutionRegionTiming.Synchronous),
_ => false,
};
}

private static bool AllBindingsAreCompleteWithTiming(
IReadOnlyList<PwshExecutionRegionBinding> bindings,
ExecutionRegionTiming timing)
{
if (bindings.Count == 0)
{
Expand All @@ -1579,7 +1697,7 @@ private static bool AllBindingsAreCompleteAndSynchronous(
for (var index = 0; index < bindings.Count; index++)
{
if (!bindings[index].IsComplete ||
bindings[index].Timing != ExecutionRegionTiming.Synchronous)
bindings[index].Timing != timing)
{
return false;
}
Expand Down Expand Up @@ -2252,17 +2370,41 @@ private bool TryGetElementDomain(
int elementIndex,
AnalysisContext input,
out ShellValueDomain domain)
{
if (TryGetElementValue(simple, elementIndex, 0, out var value))
{
return input.TryEvaluateValue(value, out domain);
}

domain = ShellValueDomain.Unknown;
return false;
}

private bool TryGetElementValue(
SimpleCommandSyntax simple,
int elementIndex,
int valueOffset,
out ShellValue value)
{
var source = _factsFactory(simple);
foreach (var provenance in source.ValueProvenance)
{
if (provenance.ClauseElementIndex == elementIndex)
{
return input.TryEvaluateValue(provenance.Value, out domain);
if (valueOffset < 0 || valueOffset > provenance.Value.Decoded.Length)
{
value = ShellValue.Literal(string.Empty);
return false;
}

value = valueOffset == 0
? provenance.Value
: provenance.Value.Slice(valueOffset);
return true;
}
}

domain = ShellValueDomain.Unknown;
value = ShellValue.Literal(string.Empty);
return false;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,12 @@ internal sealed record PwshExecutionRegionBindingResult

internal bool HasNoNewScope { get; init; }

internal int? WorkingDirectoryElementIndex { get; init; }

internal int WorkingDirectoryValueOffset { get; init; }

internal bool HasExplicitPSVersion { get; init; }

internal IReadOnlyList<PwshExecutionRegionBinding> Bindings { get; init; } =
Array.Empty<PwshExecutionRegionBinding>();
}
Expand Down Expand Up @@ -447,6 +453,14 @@ private static PwshExecutionRegionBindingResult BindReceiver(
HasExplicitInputObject = arguments.HasNamed("InputObject"),
HasNoNewScope = receiver == PwshExecutionRegionReceiver.InvokeCommand &&
arguments.IsSwitchEnabled("NoNewScope"),
WorkingDirectoryElementIndex = receiver == PwshExecutionRegionReceiver.StartJob
? arguments.FirstNamedArgumentElementIndex("WorkingDirectory")
: null,
WorkingDirectoryValueOffset = receiver == PwshExecutionRegionReceiver.StartJob
? arguments.FirstNamedArgumentValueOffset("WorkingDirectory")
: 0,
HasExplicitPSVersion = receiver == PwshExecutionRegionReceiver.StartJob &&
arguments.HasNamed("PSVersion"),
Bindings = bindings.OrderBy(binding => binding.HostClauseElementIndex).ToArray(),
};
}
Expand Down Expand Up @@ -850,7 +864,8 @@ private static BoundArguments BindArguments(
{
result.NamedArguments.Add(new BoundArgument(
index, -1, true, resolution.CanonicalName,
HasTrailingComma(element), parameter.InlineValue!));
HasTrailingComma(element), parameter.InlineValue!,
element.Value.Length - parameter.InlineValue!.Length));
if (HasTrailingComma(element)
&& !AcceptsScriptBlockArray(resolution.CanonicalName!))
{
Expand All @@ -864,7 +879,8 @@ private static BoundArguments BindArguments(
{
result.NamedArguments.Add(new BoundArgument(
index, -1, false, resolution.CanonicalName, false,
parameter.InlineValue!));
parameter.InlineValue!,
element.Value.Length - parameter.InlineValue!.Length));
continue;
}

Expand Down Expand Up @@ -938,11 +954,11 @@ private static ParsedParameter ParseParameter(string value)
return new ParsedParameter(value.Substring(1), false, false, true, false, null);
}

var inlineValue = value.Substring(colon + 1).Trim();
var inlineValue = value.Substring(colon + 1);
return new ParsedParameter(
value.Substring(1, colon - 1),
inlineValue.Length > 0,
LooksLikeScriptBlock(inlineValue),
LooksLikeScriptBlock(inlineValue.Trim()),
true,
true,
inlineValue);
Expand Down Expand Up @@ -1549,7 +1565,8 @@ private readonly record struct BoundArgument(
bool IsScriptBlock,
string? ParameterName,
bool HasTrailingComma,
string Value);
string Value,
int ValueOffset = 0);

private sealed class BoundArguments
{
Expand Down Expand Up @@ -1605,6 +1622,38 @@ internal bool IsSwitchEnabled(string name)

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

internal int? FirstNamedArgumentElementIndex(string parameterName)
{
foreach (var argument in NamedArguments)
{
if (string.Equals(
argument.ParameterName,
parameterName,
StringComparison.OrdinalIgnoreCase))
{
return argument.ElementIndex;
}
}

return null;
}

internal int FirstNamedArgumentValueOffset(string parameterName)
{
foreach (var argument in NamedArguments)
{
if (string.Equals(
argument.ParameterName,
parameterName,
StringComparison.OrdinalIgnoreCase))
{
return argument.ValueOffset;
}
}

return 0;
}

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

internal IEnumerable<BoundArgument> NamedScriptBlocks(string parameterName) =>
Expand Down
Loading