Fix #3804: decompile foreach over inline array#3837
Conversation
There was a problem hiding this comment.
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
InlineArrayElementRefpattern matching to accept non-constant index expressions while keeping bounds validation for constant indices. - Add a Pretty test case that compiles a
foreachover an inline array and expects decompilation into a compilableforloop usingb[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.
|
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. |
9872c3b to
837fbf8
Compare
|
Reworked to address the soundness concern. Rather than rewriting the unchecked (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
837fbf8 to
a494b1b
Compare
Fixes #3804.
Problem
Decompiling a
foreachover an inline-array value emitted aforloop 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
InlineArrayElementRefto the inline-array indexerb[i]for any index. As @dgrunwald pointed out, that is unsound:InlineArrayElementRefis the compiler's unchecked element accessor, whereas the C# indexerb[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
foreachat the AST level instead, inPatternStatementTransform, alongside the existingTransformForeachOnArray. It matches the exact shape the compiler emits:and only fires when the loop bound
Nequalsbuffer's inline-array length. That equality is the in-bounds proof:0 <= i < length. TheInlineArrayElementRefhelper 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:
Verified that the checked span path (
b[i]with a variable index), a partial-range loop (i < 8), and a genuineforloop reusing the index are all left unchanged.Test
Adds a
SumForeachmethod to theInlineArrayTestsPretty fixture; it round-trips exactly (noEXPECTED_OUTPUTneeded) across allroslyn4OrNewerOptionsconfigs. 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