Skip to content

Fix #3804: decompile foreach over inline array#3837

Open
siegfriedpammer wants to merge 1 commit into
masterfrom
fix-3804-foreach-inline-array
Open

Fix #3804: decompile foreach over inline array#3837
siegfriedpammer wants to merge 1 commit into
masterfrom
fix-3804-foreach-inline-array

Conversation

@siegfriedpammer

@siegfriedpammer siegfriedpammer commented Jun 27, 2026

Copy link
Copy Markdown
Member

Fixes #3804.

Problem

Decompiling a foreach over an inline-array value emitted a for loop whose body called the compiler-internal <PrivateImplementationDetails>.InlineArrayElementRef(ref b, i) helper. That type name contains angle brackets and cannot be written in C#, so the output did not compile.

Approach

The earlier version of this PR relaxed the IL matcher to rewrite InlineArrayElementRef to the inline-array indexer b[i] for any index. As @dgrunwald pointed out, that is unsound: InlineArrayElementRef is the compiler's unchecked element accessor, whereas the C# indexer b[i] (variable index) lowers to a bounds-checked span access, so the two only agree when the index is provably in-bounds.

This version reconstructs the foreach at the AST level instead, in PatternStatementTransform, alongside the existing TransformForeachOnArray. It matches the exact shape the compiler emits:

for (i = 0; i < N; i++) { item = <PrivateImplementationDetails>.InlineArrayElementRef(ref buffer, i); ... }

and only fires when the loop bound N equals buffer's inline-array length. That equality is the in-bounds proof: 0 <= i < length. The InlineArrayElementRef helper call is left untouched in the IL, so a loop that does not match this shape keeps the faithful (if unnameable) helper call rather than silently gaining a bounds check. This is sound in all cases, including hand-crafted IL.

Output:

public int Sum(Byte16 b)
{
    int num = 0;
    foreach (byte b2 in b)
    {
        num += b2;
    }
    return num;
}

Verified that the checked span path (b[i] with a variable index), a partial-range loop (i < 8), and a genuine for loop reusing the index are all left unchanged.

Test

Adds a SumForeach method to the InlineArrayTests Pretty fixture; it round-trips exactly (no EXPECTED_OUTPUT needed) across all roslyn4OrNewerOptions configs. Confirmed red without the transform (all 4 configs fail) and green with it; the full Pretty suite (1189 tests) stays green.

This PR was authored by an AI agent (Claude Code) operated by @siegfriedpammer and reviewed and manually verified by him.

🤖 Generated with Claude Code

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Fixes a C# decompilation bug where foreach over an inline-array value could decompile into a reference to the compiler-internal <PrivateImplementationDetails>.InlineArrayElementRef(...), producing non-compilable C# due to the angle brackets in the type name. The change updates the inline-array helper matcher so the call is rewritten into the inline-array indexer form (e.g. b[i]) even when the index is not a compile-time constant, and adds a regression test.

Changes:

  • Relax InlineArrayElementRef pattern matching to accept non-constant index expressions while keeping bounds validation for constant indices.
  • Add a Pretty test case that compiles a foreach over an inline array and expects decompilation into a compilable for loop using b[i].

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated no comments.

File Description
ICSharpCode.Decompiler/IL/Transforms/InlineArrayTransform.cs Allows matching InlineArrayElementRef with non-constant indices (e.g. loop variable from foreach lowering), enabling rewrite to ldelema.inlinearray and ultimately b[i].
ICSharpCode.Decompiler.Tests/TestCases/Pretty/InlineArrayTests.cs Adds a regression fixture using the EXPECTED_OUTPUT idiom to validate decompilation of foreach over an inline array into a compilable loop.

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

@dgrunwald

dgrunwald commented Jun 29, 2026

Copy link
Copy Markdown
Member

This changes the behavior with an out-of-bounds non-constant index.

This transform is only valid if the decompiler can prove the index is in-bounds.

@siegfriedpammer siegfriedpammer force-pushed the fix-3804-foreach-inline-array branch from 9872c3b to 837fbf8 Compare July 5, 2026 08:47
@siegfriedpammer

Copy link
Copy Markdown
Member Author

Reworked to address the soundness concern. Rather than rewriting the unchecked InlineArrayElementRef helper to the bounds-checked indexer b[i], the PR now reconstructs the foreach at the AST level and only fires when the loop bound equals the inline array's length -- which proves 0 <= i < length. The helper call is left untouched in the IL, so any loop that doesn't match the exact compiler-emitted shape keeps the faithful (unnameable) call instead of silently gaining a bounds check. Force-pushed onto current master; the full Pretty suite stays green.

(Analysis and implementation by an AI agent (Claude Code) operated by @siegfriedpammer, reviewed and verified by him.)

The C# compiler lowers a foreach over an inline array into a for loop whose
body reads each element through <PrivateImplementationDetails>.InlineArrayElementRef(ref
buffer, i). That helper cannot be named in C#, so the decompiled for loop did
not compile.

Reconstruct the foreach at the AST level instead. Rewriting the unchecked
InlineArrayElementRef helper to the bounds-checked indexer buffer[i] would be
unsound for an out-of-bounds index, so the transform fires only when the loop
bound equals the inline array's length: that proves 0 <= i < length, matching
the exact shape the compiler emits and nothing else. A loop that does not match
keeps the faithful (if unnameable) helper call rather than silently gaining a
bounds check.

Assisted-by: Claude:claude-fable-5:Claude Code
@siegfriedpammer siegfriedpammer force-pushed the fix-3804-foreach-inline-array branch from 837fbf8 to a494b1b Compare July 5, 2026 12:32
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Decompiling foreach over an inline array emits invalid '<PrivateImplementationDetails>.InlineArrayElementRef'

3 participants