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
7 changes: 7 additions & 0 deletions IMPLEMENTATION_PLAN.md
Original file line number Diff line number Diff line change
Expand Up @@ -408,6 +408,13 @@ priorities.
Pipelines with any supported synchronous execution region plus another
stateful stage withhold body facts that downstream initialization or
per-object interleaving can invalidate.
`New-Module` initialization now runs once synchronously from caller
state in a child module scope, propagates shared location, restores
ordinary child bindings, applies common variable-writer initialization
before the body, invalidates body identity when a writer can alter
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
blocks; deferred breakpoint/event/completion actions; then unknown
receiver and nested/adversarial matrices. Preserve script blocks proved
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -367,8 +367,7 @@ internal static bool TryGetEffect(
out bool unknownCwd)
{
unknownCwd = false;
var verb = clause.Verb.CanonicalVerb ??
(clause.Verb.Tokens.Count == 0 ? null : clause.Verb.Tokens[0]);
var verb = GetCanonicalVerb(clause);
if (verb is null)
{
return false;
Expand Down Expand Up @@ -405,8 +404,7 @@ internal static bool MayEscapeChildScope(
Clause clause,
IReadOnlyList<EffectiveArgument> effectiveArguments)
{
var verb = clause.Verb.CanonicalVerb ??
(clause.Verb.Tokens.Count == 0 ? null : clause.Verb.Tokens[0]);
var verb = GetCanonicalVerb(clause);
if (verb is null)
{
return true;
Expand Down Expand Up @@ -488,6 +486,27 @@ internal static bool MayEscapeChildScope(
return !hasLocalProviderTarget;
}

internal static bool HasVariableWritingArgument(Clause clause)
{
var verb = GetCanonicalVerb(clause);
return verb is null || HasVariableWritingArgument(verb, clause);
}

private static string? GetCanonicalVerb(Clause clause)
{
var verb = clause.Verb.CanonicalVerb ??
(clause.Verb.Tokens.Count == 0 ? null : clause.Verb.Tokens[0]);
if (verb is not null &&
PwshExecutionRegionBindingCatalog.TryResolveStaticCommandName(
verb,
out var canonicalName))
{
return canonicalName;
}

return verb;
}

private static bool IsProvedChildLocalProviderMutation(
string verb,
bool isAliasTarget,
Expand Down Expand Up @@ -1229,6 +1248,15 @@ flow.OnFailure is AnalysisContext failure
flow);
}

if (binding.ParameterSet == PwshExecutionRegionParameterSet.NewModuleScriptBlock)
{
var bodyInput = PwshPersistentStateMutation.HasVariableWritingArgument(
simple.Clause)
? receiverInput.Invalidate(unknownCwd: false)
: receiverInput;
return AnalyzeNewModule(regions[0], bodyInput, flow);
}

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

private PwshFlowResult AnalyzeNewModule(
ExecutionRegionSyntax region,
AnalysisContext input,
PwshFlowResult hostFlow)
{
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;
_childScopeEscapeRiskCount = childScopeEscapeRiskCount +
(childScopeMayEscape ? 1 : 0);

var restoredExit = AnalysisContext.JoinNullable(
RestoreChildScopeExit(
body.OnSuccess,
input,
locationMutated,
childScopeMayEscape),
RestoreChildScopeExit(
body.OnFailure,
input,
locationMutated,
childScopeMayEscape));
if (restoredExit is not AnalysisContext bodyExit ||
hostFlow.JoinedState is not AnalysisContext hostExit)
{
return hostFlow;
}

return PwshFlowResult.Both(hostExit.WithCwd(bodyExit.WorkingDirectory));
}

private PwshFlowResult AnalyzeInProcessInvokeCommand(
PwshExecutionRegionBindingResult binding,
ExecutionRegionSyntax region,
Expand Down Expand Up @@ -1497,6 +1563,7 @@ private static bool IsSupportedSynchronousReceiver(
(binding.ParameterSet is PwshExecutionRegionParameterSet.MeasureExpression or
PwshExecutionRegionParameterSet.TraceExpression or
PwshExecutionRegionParameterSet.InvokeInProcess or
PwshExecutionRegionParameterSet.NewModuleScriptBlock or
PwshExecutionRegionParameterSet.ForEachScriptBlock or
PwshExecutionRegionParameterSet.WhereScriptBlock) &&
AllBindingsAreCompleteAndSynchronous(binding.Bindings);
Expand Down Expand Up @@ -1762,7 +1829,8 @@ binding.ParameterSet is PwshExecutionRegionParameterSet.ForEachScriptBlock or
PwshExecutionRegionParameterSet.WhereScriptBlock or
PwshExecutionRegionParameterSet.InvokeInProcess or
PwshExecutionRegionParameterSet.MeasureExpression or
PwshExecutionRegionParameterSet.TraceExpression;
PwshExecutionRegionParameterSet.TraceExpression or
PwshExecutionRegionParameterSet.NewModuleScriptBlock;
}

private static bool SimpleMayMutatePipelineState(SimpleCommandSyntax simple)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,110 @@ public void In_process_invoke_command_joins_body_outcomes_before_host_continuati
Assert.True(continuation.IsComplete);
}

[Theory]
[InlineData("New-Module { Get-Item child.txt }")]
[InlineData("nmo -ScriptBlock { Get-Item child.txt }")]
[InlineData("Microsoft.PowerShell.Core\\New-Module { Get-Item child.txt }")]
public void New_module_publishes_a_synchronous_initialization_region(string source)
{
var result = ParseIsolated(source);

var host = Assert.IsType<SimpleCommandSyntax>(Assert.Single(result.Syntax.Statements));
var region = Assert.Single(host.ExecutionRegions);
Assert.Equal(ExecutionRegionOrigin.CommandArgument, region.Origin);
Assert.Equal(ExecutionRegionPhase.Initialization, region.Phase);
Assert.Equal(ExecutionRegionTiming.Synchronous, region.Timing);
Assert.Equal(ExecutionRegionCardinality.Once, region.Cardinality);
Assert.Equal(2, result.Commands.Count);
Assert.All(result.Commands, command => Assert.True(command.IsComplete));
}

[Fact]
public void New_module_body_reads_caller_state_but_host_invalidates_continuation()
{
var result = ParseIsolated(
"foreach ($x in 'outer') { }; New-Module { " +
"Write-Output $x; foreach ($x in 'inner') { } }; " +
"Write-Output $x");

var writes = result.Commands
.Where(command => command.Clause.Verb.Tokens[0] == "Write-Output")
.ToArray();
Assert.Equal(2, writes.Length);
Assert.Equal(
new[] { "outer" },
Assert.Single(writes[0].EffectiveArguments).Value.Values);
Assert.True(writes[0].IsComplete);
Assert.Equal(
ShellValueDomainKind.Unknown,
Assert.Single(writes[1].EffectiveArguments).Value.Kind);
Assert.False(writes[1].IsComplete);
}

[Theory]
[InlineData("OutVariable")]
[InlineData("PipelineVariable")]
[InlineData("ErrorVariable")]
[InlineData("WarningVariable")]
[InlineData("InformationVariable")]
public void New_module_variable_writers_invalidate_body_input(string parameter)
{
var result = ParseIsolated(
"foreach ($x in 'outer') { }; " +
$"New-Module -ReturnResult {{ Write-Output $x }} -{parameter} x");

Assert.False(result.IsUnparseable);
var write = Assert.Single(
result.Commands,
command => command.Clause.Verb.Tokens[0] == "Write-Output");
Assert.Equal(
ShellValueDomainKind.Unknown,
Assert.Single(write.EffectiveArguments).Value.Kind);
Assert.False(write.IsComplete);
}

[Fact]
public void New_module_writer_target_invalidates_body_command_resolution()
{
var result = ParseIsolated(
"New-Module -ReturnResult { Compress-Archive a b } " +
"-OutVariable PSModuleAutoLoadingPreference");

Assert.False(result.IsUnparseable);
var body = Assert.Single(
result.Commands,
command => command.Clause.Verb.Tokens[0] == "Compress-Archive");
Assert.False(body.IsComplete);
}

[Fact]
public void Module_qualified_new_module_invalidates_exported_function_continuation()
{
var result = ParseIsolated(
"Microsoft.PowerShell.Core\\New-Module { " +
"Set-Item Function:\\git -Value 'Remove-Item child.txt' }; " +
"git child.txt");

Assert.False(result.IsUnparseable);
var continuation = result.Commands.Last();
Assert.Equal("git", continuation.Clause.Verb.Tokens[0]);
Assert.False(continuation.IsComplete);
}

[Fact]
public void New_module_joins_body_outcomes_before_host_continuation()
{
var result = ParseIsolated(
"New-Module { Set-Location /maybe } && Get-Item child.txt");

var continuation = result.Commands.Last();
Assert.Equal("Get-Item", continuation.Clause.Verb.Tokens[0]);
Assert.Equal(
ShellValueDomainKind.Unknown,
continuation.WorkingDirectory.Kind);
Assert.False(continuation.IsComplete);
}

[Fact]
public void Current_scope_once_receiver_propagates_binding_state()
{
Expand Down Expand Up @@ -615,6 +719,21 @@ public void Pipeline_stage_effects_make_synchronous_region_state_unknown(
});
}

[Fact]
public void New_module_pipeline_state_mutation_fails_atomically()
{
var result = new PwshParser(new PwshParserOptions
{
WorkingDirectory = "C:/work",
InitialStateMode = PwshInitialStateMode.IsolatedNonInteractiveNoProfile,
}).Parse(
"New-Module { Write-Output value } | Write-Output -OutVariable x");

Assert.True(result.IsUnparseable);
Assert.Empty(result.Commands);
Assert.Empty(result.Clauses);
}

[Fact]
public void Unknown_region_poisons_later_variable_facts()
{
Expand Down
24 changes: 23 additions & 1 deletion tests/ShellSyntaxTree.Tests/Parsing/ShellValueOracleTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1131,7 +1131,21 @@ public void PowerShell_synchronous_regions_observe_scope_and_pipeline_stage_effe
"Write-Output -OutVariable x | Out-Null; " +
"$x='start'; Trace-Command -Name ParameterBinding -Expression { " +
"\"trace-stage=<$x>\" } -PSHost 5>$null | " +
"Write-Output -OutVariable x | Out-Null; $x");
"Write-Output -OutVariable x | Out-Null; $x; " +
"Set-Location $start; $x='outer'; New-Module { " +
"Write-Host \"module-body-x=<$x>\"; $x='inner'; " +
"Set-Location $target; Set-Alias moduleAlias Get-Date } | Out-Null; " +
"\"module-after-x=<$x>\"; " +
"\"module-cwd-target=<$((Get-Location).Path -eq $target)>\"; " +
"\"module-alias=<$([bool](Get-Alias moduleAlias -ErrorAction Ignore))>\"; " +
"Set-Location $start; New-Module { Set-Location $missing " +
"-ErrorAction SilentlyContinue } | Out-Null; " +
"\"module-failure-status=<$?>\"; " +
"\"module-failure-cwd-start=<$((Get-Location).Path -eq $start)>\"; " +
"$x='outer'; $null = New-Module -ReturnResult { " +
"Write-Host \"module-outvar=<$x>\" } -OutVariable x; " +
"$x='start'; New-Module -ReturnResult { Write-Host \"module-stage=<$x>\" } | " +
"Write-Output -OutVariable x | Out-Null");

Assert.Equal(
new[]
Expand All @@ -1150,6 +1164,14 @@ public void PowerShell_synchronous_regions_observe_scope_and_pipeline_stage_effe
"invoke-interleave=<invoke-interleave=<>>",
"measure-stage=<>",
"trace-stage=<>",
"module-body-x=<outer>",
"module-after-x=<outer>",
"module-cwd-target=<True>",
"module-alias=<False>",
"module-failure-status=<True>",
"module-failure-cwd-start=<True>",
"module-outvar=<>",
"module-stage=<>",
},
Lines(output));
}
Expand Down