Skip to content

Conversation

@yotsuda
Copy link
Contributor

@yotsuda yotsuda commented Dec 15, 2025

PR Summary

Adds support for assigning ITuple types (System.Tuple, System.ValueTuple) and types with Deconstruct methods (DictionaryEntry, KeyValuePair<,>, etc.) to multiple variables using array assignment syntax.

Fixes #7471

PR Context

Problem

When assigning a Tuple, ValueTuple, DictionaryEntry, or KeyValuePair to multiple variables, PowerShell previously assigned the entire object to the first variable instead of deconstructing it:

$tuple = [Tuple]::Create(1, 2)
$a, $b = $tuple
# Result: $a contains the entire tuple, $b is $null

$de = [System.Collections.DictionaryEntry]::new("key", "value")
$k, $v = $de
# Result: $k contains the entire DictionaryEntry, $v is $null

This prevented convenient use of APIs that return tuples (like [Math]::DivRem()) and made hashtable enumeration cumbersome.

Solution

Added special handling in PSGetMemberBinder for two cases:

  1. ITuple types - Detected via System.Runtime.CompilerServices.ITuple interface, elements extracted using the indexer
  2. Deconstruct methods - Types with a public Deconstruct method with all out parameters (like DictionaryEntry and KeyValuePair<,>)

Both handle three scenarios: exact match, fewer elements than variables (fills with null), more elements than variables (remaining go to last variable as array).

PR Checklist

Changes Made

1. Compiler.cs (+18 lines)

  • Added cached reflection references for ITuple.Length property and ITuple.get_Item method
  • Added references for EnumerableOps.GetTupleSlice, GetDeconstructMethod, InvokeDeconstruct, and GetDeconstructSlice methods

2. Binders.cs (+120 lines)

  • Added ITuple detection and handling in PSGetMemberBinder.FallbackGetMember
  • Added Deconstruct method detection and handling
  • Implemented three-case logic for both: exact element count, fewer elements, more elements

3. MiscOps.cs (+97 lines)

  • Added GetTupleSlice - extracts remaining elements from ITuple
  • Added GetDeconstructMethod - finds Deconstruct method on a type
  • Added InvokeDeconstruct - invokes Deconstruct and returns values
  • Added GetDeconstructSlice - extracts remaining values from already-deconstructed array (avoids calling Deconstruct multiple times)

4. Indexer.Tests.ps1 (+136 lines)

  • Added 13 comprehensive tests covering ITuple and Deconstruct scenarios

Total: 4 files changed, 371 insertions(+)

Behavior Examples

Tuple assignment

$tuple = [Tuple]::Create(1, 2)
$a, $b = $tuple
$a $b
Before (1, 2) tuple object $null
After 1 2

DictionaryEntry assignment

$de = [System.Collections.DictionaryEntry]::new("key", "value")
$k, $v = $de
$k $v
Before DictionaryEntry object $null
After "key" "value"

KeyValuePair assignment

$kvp = [System.Collections.Generic.KeyValuePair[string,int]]::new("count", 42)
$k, $v = $kvp
$k $v
Before KeyValuePair object $null
After "count" 42

Hashtable enumeration

$ht = @{ name = "test" }
foreach ($entry in $ht.GetEnumerator()) {
    $key, $value = $entry
}
$key $value
Before DictionaryEntry object $null
After "name" "test"

Math.DivRem

$quotient, $remainder = [Math]::DivRem(17, 5)
$quotient $remainder
Before (3, 2) ValueTuple object $null
After 3 2

Testing

Test Cases (13 new tests)

ITuple tests:

  1. Tuple with exact element count (2 elements)
  2. Single element Tuple (1 element) - boundary
  3. Tuple with 7 elements - boundary (max without nesting)
  4. Tuple with 8 elements - boundary (uses internal Rest nesting)
  5. ValueTuple with 3 elements
  6. More elements than variables (overflow to last)
  7. Fewer elements than variables (null for extras)
  8. Math.DivRem practical example

Deconstruct tests:
9. DictionaryEntry assignment
10. KeyValuePair assignment
11. DictionaryEntry with more variables than elements
12. Hashtable enumeration practical example
13. Deconstruct method called only once (verifies no redundant calls)

Test Results

Environment Passed Failed Status
Local build 20/20 0 ✅ All pass

Implementation Details

Supported Types

Via ITuple interface:

  • System.Tuple<> (1-8 elements)
  • System.ValueTuple<> (1-8 elements)
  • Any type implementing System.Runtime.CompilerServices.ITuple

Via Deconstruct method:

  • System.Collections.DictionaryEntry
  • System.Collections.Generic.KeyValuePair<,>
  • Any type with a public Deconstruct(out T1, out T2, ...) method

Design Decisions

  1. ITuple first, then Deconstruct - ITuple is checked first as it's more specific; Deconstruct is a fallback for types like DictionaryEntry
  2. Length-based restriction for ITuple - Dynamic binding includes tuple length in restrictions for correct caching
  3. Instance Deconstruct only - Extension methods are not supported (consistent with PowerShell's general behavior)
  4. Consistent overflow/underflow behavior - Follows same patterns as array/IList assignment
  5. Single Deconstruct invocation - GetDeconstructSlice takes the already-deconstructed array to avoid calling Deconstruct() multiple times

@iSazonov iSazonov added the CL-Engine Indicates that a PR should be marked as an engine change in the Change Log label Dec 15, 2025
@iSazonov iSazonov requested a review from Copilot December 15, 2025 11:46
@iSazonov iSazonov requested review from Copilot and removed request for Copilot December 15, 2025 12:47
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This pull request adds tuple deconstruction support to PowerShell, enabling ITuple types (Tuple, ValueTuple) and types with Deconstruct methods (DictionaryEntry, KeyValuePair<,>) to be assigned to multiple variables using array assignment syntax. This addresses issue #7471 by implementing special handling in the dynamic binding layer to automatically decompose these types into their constituent elements during assignment operations.

Key changes:

  • Enhanced PSGetMemberBinder to detect and handle ITuple interface and Deconstruct methods
  • Added helper methods in EnumerableOps for slicing tuples and invoking deconstruction
  • Comprehensive test coverage with 13 new tests for various tuple and deconstruction scenarios

Reviewed changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated 1 comment.

File Description
test/powershell/Language/Scripting/Indexer.Tests.ps1 Added 13 comprehensive tests covering ITuple and Deconstruct assignment scenarios including edge cases (underflow, overflow, single Deconstruct call verification)
src/System.Management.Automation/engine/runtime/Operations/MiscOps.cs Implemented four helper methods in EnumerableOps: GetTupleSlice, GetDeconstructMethod, InvokeDeconstruct, and GetDeconstructSlice for tuple and deconstruct operations
src/System.Management.Automation/engine/runtime/Binding/Binders.cs Extended PSGetMemberBinder.FallbackGetMember with ITuple and Deconstruct detection and handling logic with proper dynamic restrictions
src/System.Management.Automation/engine/parser/Compiler.cs Added cached reflection info for ITuple properties/methods and EnumerableOps helper methods to improve runtime performance

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines 1036 to 1037
var iTuple = target.Value as System.Runtime.CompilerServices.ITuple;
if (iTuple is not null)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
var iTuple = target.Value as System.Runtime.CompilerServices.ITuple;
if (iTuple is not null)
var iTuple = target.Value as System.Runtime.CompilerServices.ITuple;
if (target.Value is ITuple iTuple)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you for the quick feedback! Applied with correction (merged both lines into one). Commit: 1d4f6a6

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

System.Runtime.CompilerServices is still used in 5 points. Please remove.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry for missing those. Removed the prefix from 4 occurrences in my changes:

  • Compiler.cs (lines 301, 304)
  • Binders.cs (line 1082)
  • MiscOps.cs (line 3651)

The 5th reference in CorePsTypeCatalog.cs is not my code. It's an auto-generated type catalog string literal. Should I update that one as well?

Commit: 89b0793

}

It 'Hashtable enumeration produces deconstructable DictionaryEntry' {
$ht = @{ name = "test" }
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does the feature work for PSCustomObject?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, PSCustomObject is not supported. It doesn't implement ITuple, IList, or have a Deconstruct method.

I intentionally didn't add support because PSCustomObject properties are name-based, not position-based. Order-based deconstruction would be fragile:

$person = [PSCustomObject]@{ name = "John"; age = 30 }
$name, $age = $person  # Works

# Later, add a property...
$person = [PSCustomObject]@{ id = 1; name = "John"; age = 30 }
$name, $age = $person  # Silently breaks: $name = 1, $age = "John"

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Name-based matching (like JavaScript's const { name, age } = person) would be safer, but I believe that would require a different approach and new syntax—beyond the scope of this PR.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, it seems we have such issue for named arguments and could enhance it to the area.

Copy link
Collaborator

@iSazonov iSazonov left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@yotsuda Although this is a great feature, I can't imagine when the owners of the code will find the time to do a review. Let's be patient.

@microsoft-github-policy-service microsoft-github-policy-service bot added the Review - Needed The PR is being reviewed label Dec 24, 2025
@microsoft-github-policy-service
Copy link
Contributor

This pull request has been automatically marked as Review Needed because it has been there has not been any activity for 7 days.
Maintainer, please provide feedback and/or mark it as Waiting on Author

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

CL-Engine Indicates that a PR should be marked as an engine change in the Change Log Review - Needed The PR is being reviewed

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Assigning to an array literal of variables (LHS) should support Deconstruct methods

2 participants