Commit Graph
31 Commits
Author SHA1 Message Date
dualfroz 85285c1cea Fix #4019: Recognize with expressions whose initializer awaits
Roslyn spills the receiver of an initializer assignment that precedes an
await into a hoisted temporary, so the clone of a with expression (and the
object of an object initializer) reaches the initializer transform through
a copy it rejects. Only copies between two hoisted compiler temporaries are
inlined: folding copies of hoisted user locals as well would turn a user's
local into an initializer.
2026-10-10 17:52:06 +02:00
upstream ef47a24511 Upstream icsharpcode/ILSpy state of the touched files before b3c37fd8d4 2026-10-10 13:18:39 +02:00
dualfroz 72e7db13fa #4136: Simplify bool-typed if (c) 1 else 0 to c != 0
An IfInstruction tests its condition for non-zero, so a bool-typed
`if (c) ldc.i4 1 else ldc.i4 0` always equals `c != 0`, which
ExpressionTransforms already folds to `c` for bool `c`. Without this
identity the node reached the C# output as `c ? true : false`, or as
`c || 1 == 0` once negated. The lost `??` form is not addressed here.
2026-10-10 13:50:53 +02:00
upstream 6e2842992c Upstream icsharpcode/ILSpy state of the touched files before 29888045ca 2026-10-10 13:07:54 +02:00
dualfroz ffe7e7b3f1 Fix #3702: Don't rewrite GetValueOrDefault() to == true in ?. chains
Turning bool?.GetValueOrDefault() into `== true` ends an enclosing
null-conditional chain, so `c?.Flag.GetValueOrDefault()` became
`(c?.Flag == true)` of type bool instead of bool?. A following `?? false`
then fails to compile (CS0019), and a following `.ToString()` yields
"False" instead of null when c is null. Keep the call when its target is
part of a ?. chain.
2026-10-16 10:07:54 +02:00
upstream 8e3e41fedb Upstream icsharpcode/ILSpy state of the touched files before 53822318d9 2026-10-10 13:03:54 +02:00
dualfroz 3f1ef717b6 Fix #4017: Look through LdObjIfRef on the operand of await
A constrained GetAwaiter call on a type parameter that is not known to be
a value type wraps its receiver in LdObjIfRef so the reference is read
before any further arguments. ExpressionBuilder has no translation for
LdObjIfRef, so the operand was dropped. GetAwaiter takes no further
arguments, so the operand can be translated from the address, as
CallBuilder already does for call receivers.
2026-10-16 10:07:54 +02:00
upstream 587f5c9583 Upstream icsharpcode/ILSpy state of the touched files before c5f455e56b 2026-10-10 13:01:51 +02:00
dualfroz 999d3b48fa Fix #4137: Keep nint in conditionals with mixed native int branches
When the branches of a conditional have different types, its type falls
back to the IL stack slot type, which is (U)IntPtr. IntroduceNativeIntTypeOnLocals
only retypes variables, so with native integers enabled the branches got
cast to IntPtr. Map (U)IntPtr to n(u)int the same way boxing does; pointer
types are unaffected.
2026-10-10 13:50:52 +02:00
upstream 197acf8cbe Upstream icsharpcode/ILSpy state of the touched files before 77a02d16fe 2026-10-10 12:50:07 +02:00
dualfroz 9b36f6e6da Fix #4135: Keep bool-to-int conditional in its local
Once comp(x != 0) is removed for a bool x, `int n = b ? 1 : 0` becomes a
plain copy of the bool, and copy propagation replaced every load of n with
`(b ? 1 : 0)`. Refuse copies between bool and non-bool variables rather
than requiring identical types, which would also block legitimate copies
such as dynamic -> object or Derived[] -> Base[].
2026-10-10 13:50:52 +02:00
upstream 1942c2ab0f Upstream icsharpcode/ILSpy state of the touched files before bf9e4c8951 2026-10-10 12:48:58 +02:00
dualfroz 8c8b8caad8 Fix #3180: Spell out type arguments of a null-conditional extension call
When the extension method call cannot be written without type arguments
(another extension method with the same name and a different arity is
found first), the call falls back to the static form. For 'a?.M()' that
fallback produced 'Enumerable.Count(s?)', which is not valid C#. Writing
the type arguments excludes the extension methods of a different arity,
so for a null-conditional target the type arguments are tried before
giving up.
2026-10-03 11:46:13 +02:00
upstream a6526a1dab Upstream icsharpcode/ILSpy state of the touched files before 1e0fba1611 2026-10-03 11:46:13 +02:00
dualfroz 3b5201cabc Fix #4153: Keep foreach when a named argument reads the loop variable
Once a named argument is hoisted, the call becomes a CallWithNamedArgs
block, and inlining moves the Current getter into the block's first
argument store. The foreach detection could not move it back out:
CanInlineIntoSlot refused the block, and for an instance call the store
of the 'this' argument in front counted as a write the getter cannot be
moved past. That store only writes the block's own variable, so the
getter is checked against the stored value instead, the same way
CanExtendNamedArgument allows inlining into that position.
2026-10-03 03:33:10 +02:00
upstream 3e462ad472 Upstream icsharpcode/ILSpy state of the touched files before 3c35e4cef6 2026-10-03 03:33:10 +02:00
dualfroz c61379b8b9 Fix #4016: Don't consume nullable flags for anonymous type arguments
Roslyn writes [Nullable] for an anonymous type as if the type was not
generic, so its type arguments have no flags. Taking one per argument
shifted every annotation after the anonymous type, which showed on the
cached lambda fields in <>c, and tripped the dummy-flag assert when the
shifted stream landed on a generic struct. [Dynamic] and
[TupleElementNames] do have entries for those arguments (checked against
what Roslyn emits), so only the nullable index skips them.
2026-10-03 02:29:11 +02:00
upstream 2dc58c945d Upstream icsharpcode/ILSpy state of the touched files before 23d0066f49 2026-10-03 02:29:11 +02:00
dualfroz e0a7ccf651 Expect the optimized dispatch labels from the legacy VB compiler too 2026-09-13 21:57:03 +02:00
dualfroz d2b048ebb7 Keep the Issue3659 expected output out of the format check 2026-09-13 21:01:36 +02:00
upstream b324f1f7aa Upstream icsharpcode/ILSpy state of the touched files before 352acfd456 2026-09-13 21:01:36 +02:00
dualfroz 14f32689d4 Replace the Issue3659 IL test with a VBPretty test 2026-09-13 18:32:51 +02:00
upstream 5088708223 Upstream icsharpcode/ILSpy state of the touched files before 39c6f5eada 2026-09-13 18:32:51 +02:00
dualfroz d5dfe13b26 Recognize a VB exception filter's type test
vbc builds an exception filter as a single non-short-circuiting
expression: `isinst`, then the user's `when` conditions, combined with
`and`. DetectCatchWhenConditionBlocks only knew the branch chain csc
emits, so the type test stayed inside the filter and the handler kept the
`object` variable it has in IL:

    catch (object obj) when ((obj is Exception) & (num2 != 0) & (num == 0))
    {
        ProjectData.SetProjectError((Exception)obj);

A catch type has to derive from Exception, so that does not compile.

The conjunction form is matched too now, lifting the test to the catch
type as the block form already does. The other conjuncts stop running for
a non-matching exception once the test moves, so the filter has to be
pure for this to be invisible; PropagateExceptionVariable then drops the
castclass in the handler:

    catch (Exception ex) when ((num2 != 0) & (num == 0))
    {
        ProjectData.SetProjectError(ex);

Closes #3659
2026-09-11 23:55:33 +02:00
upstream 8563d1b747 Upstream icsharpcode/ILSpy state of the touched files before 3d1d1be13b 2026-09-11 23:55:33 +02:00
dualfroz 594bb08fca Recognize a VB exception filter's type test
vbc builds an exception filter as a single non-short-circuiting
expression: `isinst`, then the user's `when` conditions, combined with
`and`. DetectCatchWhenConditionBlocks only knew the branch chain csc
emits, so the type test stayed inside the filter and the handler kept the
`object` variable it has in IL:

    catch (object obj) when ((obj is Exception) & (num2 != 0) & (num == 0))
    {
        ProjectData.SetProjectError((Exception)obj);

A catch type has to derive from Exception, so that does not compile.

The conjunction form is matched too now, lifting the test to the catch
type as the block form already does. The other conjuncts stop running for
a non-matching exception once the test moves, so the filter has to be
pure for this to be invisible; PropagateExceptionVariable then drops the
castclass in the handler:

    catch (Exception ex) when ((num2 != 0) & (num == 0))
    {
        ProjectData.SetProjectError(ex);

Closes #3659
2026-09-13 15:38:45 +02:00
upstream 297a659cc0 Upstream icsharpcode/ILSpy state of the touched files before 05718412c7 2026-09-11 23:55:33 +02:00
dualfroz b5ad0ac820 Recognize a VB exception filter's type test
vbc builds an exception filter as a single non-short-circuiting
expression: `isinst`, then the user's `when` conditions, combined with
`and`. DetectCatchWhenConditionBlocks only knew the branch chain csc
emits, so the type test stayed inside the filter and the handler kept the
`object` variable it has in IL:

    catch (object obj) when ((obj is Exception) & (num2 != 0) & (num == 0))
    {
        ProjectData.SetProjectError((Exception)obj);

A catch type has to derive from Exception, so that does not compile.

The conjunction form is matched too now, lifting the test to the catch
type as the block form already does. The other conjuncts stop running for
a non-matching exception once the test moves, so the filter has to be
pure for this to be invisible; PropagateExceptionVariable then drops the
castclass in the handler:

    catch (Exception ex) when ((num2 != 0) & (num == 0))
    {
        ProjectData.SetProjectError(ex);

Closes #3659
2026-09-15 19:16:29 +02:00
upstream aa0b6871d2 Upstream icsharpcode/ILSpy state of the touched files before d5ff770959 2026-09-11 23:55:33 +02:00
dualfroz 167098573c Keep a discarded method-group conversion out of the delegate cache
CachedDelegateInitializationWithField only fired when the instruction
after the caching `if` read the cache field exactly once. A discarded
conversion -- `_ = (Action<int>)M;` -- reads it zero times: Roslyn emits
the null check and the store, and nothing else. The transform bailed out,
so the `<>O` cache class and its `<0>__M` field survived into the output
under names no C# compiler will accept.

Zero usages is now handled: the `if` is replaced by the delegate
construction it guarded, which is what the source expressed, and the
caching disappears with the field. The rest of the method is checked for
loads of the same field first, since only then is this `if` the sole
initialization.

Closes #3965
2026-09-19 11:14:31 +02:00
upstream 80cf67e749 Upstream icsharpcode/ILSpy state of the touched files before 259e513829 2026-09-11 23:42:13 +02:00