Skip to content

Infer the C# type of ldlen - #4075

Merged
siegfriedpammer merged 1 commit into
masterfrom
fix/3704-ldlen-infertype
Aug 30, 2026
Merged

siegfriedpammer merged 1 commit into
masterfrom
fix/3704-ldlen-infertype

Conversation

@siegfriedpammer

@siegfriedpammer siegfriedpammer commented Aug 29, 2026

Copy link
Copy Markdown
Member

NullPropagationTransform only rewrites x != null ? x.Chain : fallback into x?.Chain ?? fallback when the chain's inferred type is a non-nullable value type. InferType had no case for ldlen, so array length came back as SpecialType.UnknownType, NullableType.IsNonNullableValueType returned false, and the rewrite was skipped:

// GetData()?.Length ?? 0
byte[] data = GetData();
Use((data != null) ? data.Length : 0);

The access chain itself was fine - the same chain without the ?? fallback takes the MatchDefaultValue(Nullable<int>) branch, never consults InferType, and produced GetData()?.Length correctly. Only the coalescing branch was blocked.

The inferred type mirrors ExpressionBuilder.VisitLdLen, which picks Array.Length or Array.LongLength from ResultType alone - note the split is == StackType.I4, not != StackType.I8, because ILReader emits LdLen(StackType.I, ...) and only ExpressionTransforms.VisitConv folds it down to ldlen.i4.

Two other InferType consumers see a real type where they previously saw Unknown, both toward more accuracy: AllStoresUseConsistentType in ExpressionBuilder, and CheckImplicitTruncation in TransformAssignment, which otherwise falls through to "assume that the value might be changed by truncation".

Found while investigating #3704, where the surviving ternary also keeps the tested array in a stack slot and strands the typeof of a dynamic call's static target. That issue is fixed separately in #4072; this change is independent of it.

Why the DynamicTests cases shrink to one

#4072 added four members whose premise was that temporary: the array survived as its own statement, so the typeof could not reach the call site by ordinary inlining. Inferring the type of ldlen removes the temporary, so the target now reaches the call site by inlining and #4072's StaticTargetType path is no longer taken there. Measured by reverting #4072's source changes and running DynamicTests:

fixture result with #4072 reverted
the four members as this PR would leave them 24 / 24 pass
the four members as they are on master 18 / 24 fail
DynamicAwait (untouched by this PR) 10 / 10 fail

They no longer detect the regression they were written for, and with the temporary gone all four take the same route through VisitDynamicInvokeMemberInstruction. One is kept as StaticTargetOnDynamicCall; the old name asserted a shape the source no longer produces. DynamicAwait keeps the #3704 regression covered, so nothing is lost by dropping the other three along with the callees that existed only for them.

Full ICSharpCode.Decompiler.Tests run: 3542 passed, 0 failed, 45 skipped.

🤖 Generated with Claude Code

@siegfriedpammer
siegfriedpammer force-pushed the fix/3704-ldlen-infertype branch from aa593fc to a21e1dd Compare August 29, 2026 23:29
NullPropagationTransform only rewrites "x != null ? x.Chain : fallback"
into "x?.Chain ?? fallback" when the chain's inferred type is a
non-nullable value type, and InferType had no case for ldlen. Array length
therefore came back as UnknownType, so "arr?.Length ?? 0" was left as a
ternary.

The inferred type mirrors ExpressionBuilder.VisitLdLen, which decides
between Array.Length and Array.LongLength from the result type alone.

Found while investigating #3704, where the surviving ternary also keeps the
tested array in a stack slot and strands the typeof of a dynamic call's
static target. That issue is fixed separately in #4072, whose DynamicTests
cases pinned the ternary as expected output; those blocks round-trip
exactly now, so they are gone.

Also carries a review follow-up that missed #4072: the static-target test
in VisitDynamicInvokeMemberInstruction is a plain null check, the way
DynamicInvokeMemberInstruction itself tests the field, rather than a
pattern match binding a name it does not need.

Assisted-by: Claude:claude-opus-5[1m]:Claude Code
@siegfriedpammer
siegfriedpammer force-pushed the fix/3704-ldlen-infertype branch from a21e1dd to 40b9076 Compare August 30, 2026 09:55
@siegfriedpammer
siegfriedpammer merged commit 19e702c into master Aug 30, 2026
15 checks passed
@siegfriedpammer
siegfriedpammer deleted the fix/3704-ldlen-infertype branch August 30, 2026 10:23
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.

2 participants