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
14 changes: 11 additions & 3 deletions IMPLEMENTATION_PLAN.md
Original file line number Diff line number Diff line change
Expand Up @@ -319,10 +319,18 @@ priorities.
`PwshInitialStateMode` API and safe-default contract are now locked;
`-NoProfile -NonInteractive` alone is explicitly insufficient without a
controlled startup, inherited environment, and module baseline. Design
cases select the mode individually and pin default `Unknown`. Tasks
7.3-7.4 must consume the contract rather than inferring isolation. Expand the
cases select the mode individually and pin default `Unknown`. The first
value-analysis pass now consumes that contract, retains parser-owned
argument provenance, proves quoted scalar and literal-array domains,
retains ordered duplicate visits separately from public set summaries,
guards a pinned documented preference inventory plus fresh-host built-ins
with a live PowerShell oracle, composes case-insensitive distinct nested
bindings, and leaves pipeline
objects, null, overflow, wrappers, redirects, same-name overwrites, and
post-loop state conservative. Task 7.4 must add ordered persistent scope
and location transfers without restoring parser-frame bindings. Expand the
design corpus for cardinality, mutation, separators, wrappers, redirects,
and transition caps before tasks 7.3-7.7. The simple-command slice is
and transition caps before tasks 7.4-7.7. The simple-command slice is
delivered for ordinary, adjacent, quoted, here-string, redirect, standalone,
call-operator, dynamic-identity, and host-wrapper positions, with
current-scope state propagation and bounded expression rejection pinned
Expand Down
15 changes: 10 additions & 5 deletions SPEC.POWERSHELL.md
Original file line number Diff line number Diff line change
Expand Up @@ -431,11 +431,16 @@ profiles, startup scripts, or uncontrolled ambient variables, aliases,
functions, and modules.

Even under that assertion, only ordinary unscoped binding names that do not
case-insensitively collide with PowerShell's automatic, constant, or read-only
variables are eligible. Scoped/provider bindings such as `$global:x`,
`$script:x`, `$private:x`, and `$env:X` fail the loop region closed. A typed,
validated, constant, or read-only ambient binding therefore cannot coerce,
reject, or otherwise alter a value the analyzer presents as an exact string.
case-insensitively collide with PowerShell's automatic, constant, read-only,
typed, validated, preference, or configuration variables are eligible.
Scoped/provider bindings such as `$global:x`, `$script:x`, `$private:x`, and
`$env:X` fail the loop region closed. A built-in or ambient binding therefore
cannot coerce, reject, or otherwise alter a value the analyzer presents as an
exact string, or change host behavior as a side effect of loop assignment.
The supported preference-variable inventory is pinned to PowerShell's
`about_Preference_Variables` reference rather than inferred only from variables
materialized by a fresh host; lazy and configuration-dependent names remain
ineligible even when `Get-Variable` does not initially enumerate them.

Parenthesized groups, `$()`, and static `Invoke-Expression` execute in the
current runspace and share supported binding, command-resolution, and location
Expand Down
17 changes: 11 additions & 6 deletions SPEC.md
Original file line number Diff line number Diff line change
Expand Up @@ -297,12 +297,17 @@ does not establish this contract. A fixed bootstrap may establish these
constraints only when it cannot define or mutate loop-bound variables or
policy-relevant command identities.

The mode does not erase PowerShell's built-in automatic variables. Exact and
finite binding proofs remain limited to
ordinary unscoped variable names that do not collide, case-insensitively, with
automatic, constant, or read-only bindings known to the supported PowerShell
runtime. Scoped/provider forms such as `$global:x`, `$script:x`, and `$env:X`
are outside the bounded loop-binding grammar.
The mode does not erase PowerShell's built-in variable state. Exact and finite
binding proofs remain limited to ordinary unscoped variable names that do not
collide, case-insensitively, with automatic, constant, read-only, typed,
validated, preference, or configuration bindings known to the supported
PowerShell runtime. A `foreach` assignment to a built-in preference variable
can coerce an authored string into an enum or reject it, and can change host
behavior independently of the loop value; it is therefore not an ordinary
string binding. Documented preference names remain excluded even when they are
lazy or configuration-dependent and therefore absent from a fresh
`Get-Variable` inventory. Scoped/provider forms such as `$global:x`, `$script:x`, and
`$env:X` are outside the bounded loop-binding grammar.

The assertion applies only to the host that the caller actually constrains.
Current-runspace regions such as `( ... )`, `$()`, and a static
Expand Down
9 changes: 7 additions & 2 deletions openspec/changes/v0-3-structured-shell-analysis/design.md
Original file line number Diff line number Diff line change
Expand Up @@ -440,8 +440,13 @@ read-only, or scoped; aliases, functions, and modules can independently change
command identity. Syntax alone cannot erase any of those facts.

The positive binding grammar therefore accepts only ordinary unscoped names
that do not case-insensitively collide with automatic, constant, or read-only
variables known to the supported runtime. Current-runspace groups, `$()`, and
that do not case-insensitively collide with automatic, constant, read-only,
typed, validated, preference, or configuration variables known to the
supported runtime. Built-in preference variables are excluded because
assignment can coerce or reject authored strings or alter host behavior. The
reviewed documented inventory is pinned independently from the live fresh-host
oracle so lazy and configuration-dependent preferences cannot escape the gate.
Current-runspace groups, `$()`, and
static `Invoke-Expression` share supported binding, command-resolution, and cwd
state. A decoded child `pwsh` host starts at `Unknown` unless its own invocation
independently proves the complete constrained-host contract. Recognized
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -185,7 +185,11 @@ MAY establish those constraints only when it cannot define or mutate loop-bound
variables or policy-relevant command identities. `-NoProfile -NonInteractive`
alone SHALL NOT satisfy the contract. Exact and finite
binding analysis SHALL remain limited to ordinary unscoped names that do not
case-insensitively collide with automatic, constant, or read-only variables.
case-insensitively collide with automatic, constant, read-only, typed,
validated, preference, or configuration variables known to the supported
PowerShell runtime. The preference inventory SHALL include documented lazy and
configuration-dependent names even when a fresh `Get-Variable` inventory omits
them.
Scoped/provider binding forms SHALL fail closed.

Current-runspace groups, `$()`, and static `Invoke-Expression` payloads SHALL
Expand All @@ -211,6 +215,11 @@ initial-state assertion.
- **THEN** default-mode analysis does not claim the authored strings are the effective loop values
- **THEN** selecting isolated mode for that reused runspace would violate the caller contract

#### Scenario: Built-in preference binding is not an ordinary string slot
- **WHEN** isolated-mode PowerShell parses a loop binding named `ConfirmPreference`, `ErrorActionPreference`, or another known built-in preference or configuration variable
- **THEN** the complete loop region is unparseable
- **THEN** the analyzer does not assume assignment avoids type coercion, validation, rejection, or host-behavior changes

#### Scenario: Child host does not inherit the parent's assertion
- **WHEN** isolated-mode PowerShell parses a supported `pwsh -NoProfile -Command` child containing a `foreach`
- **THEN** the child receives `Unknown` initial state unless the child invocation independently proves the complete constrained-host environment
Expand Down
13 changes: 12 additions & 1 deletion openspec/changes/v0-3-structured-shell-analysis/tasks.md
Original file line number Diff line number Diff line change
Expand Up @@ -133,7 +133,18 @@
and module baseline, current-runspace sharing, child-host noninheritance,
mutation invalidation, and ambient typed/read-only binding hazards in the
canonical specs and case-specific design corpus.
- [ ] 7.3 Derive exact and finite string domains without treating pipeline objects as literal strings.
- [x] 7.3 Derive exact and finite string domains without treating pipeline objects as literal strings.
- The PowerShell-specific value pass consumes lexer provenance, composes
case-insensitive distinct active bindings, publishes bounded literal
scalar/array domains only under the explicit isolated-runspace contract,
and collapses object, null, unsupported, and over-cap values to Unknown.
The internal plan retains ordered duplicate visits and an exact authored
count separately from its public set summary; unknown object iterables are
zero-or-more. Reserved or stateful built-in binding collisions fail
atomically; a pinned documented preference inventory covers lazy names and a
live PowerShell 7.x oracle guards the fresh-host inventory. Decoded
child hosts, current-runspace wrappers, redirect values, same-name nested
overwrites, and post-loop state remain conservative for tasks 7.4-7.6.
- [ ] 7.4 Propagate PowerShell scope and location state according to the locked statement semantics.
- [ ] 7.5 Cover aliases, cmdlets, native commands, nested loops, pipelines, script blocks, and wrapper boundaries.
- [ ] 7.6 Add adversarial cases for object-valued iterables, mutation, dynamic invocation, splatting, and cap overflow.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,14 @@ private bool TryParseForEach(
}

var bindingToken = _tokens[_position++];
if (_options.InitialStateMode ==
PwshInitialStateMode.IsolatedNonInteractiveNoProfile &&
!PwshForEachValueAnalysis.IsEligibleBindingName(bindingName))
{
error = "PowerShell foreach binding collides with a reserved or stateful built-in variable";
return false;
}

if (_position == _tokens.Count ||
_tokens[_position].Kind != PwshTokenKind.Word ||
!string.Equals(
Expand All @@ -74,6 +82,7 @@ private bool TryParseForEach(
}

var iterableTokens = CopyTokens(iterableStart, closePosition);
var isLiteralIterable = IsLiteralForEachExpression(iterableTokens);
var firstIterable = iterableTokens[0];
var lastIterable = iterableTokens[iterableTokens.Count - 1];
var iterableSourceStart = firstIterable.SourceStart;
Expand Down Expand Up @@ -110,7 +119,7 @@ private bool TryParseForEach(
return false;
}

command = new ForEachSyntax
var forEach = new ForEachSyntax
{
Binding = new LoopBindingSyntax
{
Expand All @@ -136,6 +145,13 @@ private bool TryParseForEach(
SourceLength = bodyToken.SourceStart + bodyToken.SourceLength -
start.SourceStart,
};
_forEachPlans.Add(
forEach,
PwshForEachValueAnalysis.CapturePlan(
bindingName,
iterableTokens,
isLiteralIterable));
command = forEach;
error = null;
return true;
}
Expand Down Expand Up @@ -267,7 +283,13 @@ private bool TryParseForEachIterator(
sourceLength,
CompoundOperator.None,
insideCommandSubstitution: false);
return coordinator.TryParse(out iterator, out error);
if (!coordinator.TryParse(out iterator, out error))
{
return false;
}

MergeFacts(coordinator);
return true;
}

private bool TryParseForEachBody(
Expand Down Expand Up @@ -318,7 +340,13 @@ private bool TryParseForEachBody(
sourceLength,
CompoundOperator.None,
insideCommandSubstitution: false);
return coordinator.TryParse(out body, out error);
if (!coordinator.TryParse(out body, out error))
{
return false;
}

MergeFacts(coordinator);
return true;
}

private static bool TryReadSimpleLoopBinding(PwshToken token, out string name)
Expand Down
Loading