A constant narrower than its stack type builds as a plain ldc.i4, which
infers as int, so a conditional whose other branch really is a char saw
two different types and the whole tree was left as the Expression calls
that built it - EF Core's StringCharConverter.ToChar is one. Bool and
enum constants were already wrapped for this reason; the wrap now
applies wherever the built value does not infer as the declared type.
Assisted-by: Claude:claude-opus-5:Claude Code
The builder returned by ConvertLambda hands back null when a nested
conversion declines, and the result was cast and dereferenced before
anything checked it, so a tree the transform cannot handle took down the
whole method with a NullReferenceException instead of being left alone.
EF Core's StringCharConverter.ToChar is such a tree: the conditional
spills the Expression.Call arguments into stack slots, which
MatchGetMethodFromHandle does not see through.
Assisted-by: Claude:claude-opus-5:Claude Code
A hand-built Expression.Equal whose operands are a type parameter has no
lambda to decompile to: `v == other` is CS0019 for a type parameter, and
boxing both operands compiles but compares box identity where the tree
compares values once the parameter is a value type. The conversion now
declines, leaving the Expression calls that built the tree.
Found in EF Core's BoolToTwoValuesConverter<TProvider>.ToBool, which
decompiled to code that does not compile.
Assisted-by: Claude:claude-opus-5:Claude Code
A conversion of a small integer type to Int32 returns its operand unchanged,
because such values already occupy an I4 stack slot. The two operands of
`(short a, int b) => a + b` are therefore Int16 and Int32, and requiring them
to be equal rejected the conversion; the whole expression tree was then left
untransformed, or worse, aborted the enclosing method. What
BinaryNumericInstruction requires of its operands is a common stack type.
TryConvertExpressionTree also has to cope with a builder that fails, rather
than dereferencing the lambda it did not get.
Assisted-by: Claude:claude-opus-5[1m]:Claude Code
An unconstrained type parameter has no known IsReferenceType, and since the
stack types were split it takes StackType.VT, so a call on it reached
CallInstruction with a VT 'this' argument where Obj was expected. It might be
a value type at runtime, so it needs the same box a known value type gets.
Assisted-by: Claude:claude-opus-5[1m]:Claude Code
An expression tree leaves the boxing of a value-type receiver implicit: it
carries no Convert node for it, because the boxing follows from the method
being declared on a reference type. Enum.HasFlag invoked on an enum value is
the common case, and it reached CallInstruction with an I4 'this' argument
where Obj was expected, which aborts a debug build outright.
Deciding by the target's own type also retires the StackType.VT arm, which
ExpectedTypeForThisPointer never returns. Since box records the type of what
it boxes, an expression-tree cast to that same type in front of it is dropped.
Assisted-by: Claude:claude-opus-5[1m]:Claude Code
A conversion from a value type to a reference type produced an opaque cast, so
no box instruction appeared anywhere in the converted tree, while the same C#
compiled as a plain lambda yields box T. The operand type is what box takes,
and it is available from the converted operand.
Assisted-by: Claude:claude-opus-5[1m]:Claude Code
Compiling the same C# twice, once as Expression<Func<...>> and once as a plain
Func<...>, and diffing the two ILFunction bodies exposes where the conversion
reconstructs something the IL reader would never build. Three such cases:
The sign is part of the opcode only for the checked add/sub/mul and for
div/rem/shr; ILReader leaves it at Sign.None elsewhere, while the conversion
took it from the operand type unconditionally.
Expression.MemberInit is an object initializer, not a collection initializer.
Expression.Convert's three-argument overload carries the user-defined
conversion operator - which is how the decimal conversions are encoded - and
that argument was read by nothing, so every such conversion collapsed into an
opaque cast that dropped the method. Emitting the call matches what the
transform already does for decimal arithmetic.
Assisted-by: Claude:claude-opus-5[1m]:Claude Code
Every converter now states the Expression.* call it matches and the ILAst it
produces, and the argument-count switches label the factory overload each case
stands for. The shapes were read off ILAst dumps of compiled expression trees
rather than from the factory signatures; two branches are documented as
unreachable, since no arithmetic or logical factory declares the four-argument
(left, right, liftToNull, method) overload their case matches.
Also drops the result-type local left in ConvertField, which BuildField
re-derives from the field and the type hint.
Assisted-by: Claude:claude-opus-5[1m]:Claude Code
ConvertConstant is its only caller, and with the result type gone the
out parameter that reconstructed it - a switch over LdNull/LdStr/Ldc* -
has no consumer. What remains is a match condition: the two-argument
Expression.Constant overload must pass its type as typeof(T), while the
one-argument overload legacy csc emits for display-class instances has
nothing to check.
Assisted-by: Claude:claude-opus-5[1m]:Claude Code
Every converter handed back a (Func<ILInstruction>, IType) pair, and the
IType was consumed by matchers that ran before the builder. Now that the
ILAst instructions carry their own types, InferType() on the built operand
answers the same questions, so the type-dependent decisions move into the
builders and the pair collapses to the builder alone. Builders that could
already fail (ConvertArrayIndex) set the precedent for returning null from
inside one; ConvertInstruction propagates that.
Assisted-by: Claude:claude-opus-5[1m]:Claude Code
ConvertBind is the only expression-tree converter whose IType nobody reads:
ConvertMemberInit, its sole caller, takes Item1 and discards the rest, and
the member type is recoverable from the Call/StObj it builds anyway.
Assisted-by: Claude:claude-opus-5[1m]:Claude Code
System.Linq.Expressions resolves the user-defined operator behind a binary
factory by metadata name (Expression.Add looks up op_Addition), and the
checked factories reuse the unchecked names: AddChecked also looks up
op_Addition, never op_CheckedAddition. Recording that name at the call site
keeps the mapping next to the factory it belongs to.
Assisted-by: Claude:claude-opus-5[1m]:Claude Code
Change InferType() to an abstract method and implement for every ILInstruction.
With this change, we now always have enough information to create a variable of an appropriate type to store the result of evaluating the instruction.
This previously was not the case for instructions producing "other value type", for which the stacktype-based fallback incorrectly produced `object`.
The C# debug-steps view highlights and centers the exact AST node a
transform changed; the ILAst view already had the step tree and
replay-at-step but produced no highlight. Bring it to parity.
IL rendering has no token-writer seam like the C# output visitor, so
per-instruction text spans are recorded by bracketing
ILInstruction.WriteTo via a new INodeTrackingOutput. The dominant
inst.ReplaceWith(newInst) transform pattern detaches the instruction
passed to Step, so ILTransformContext gains EndStep to record the
produced instruction; Stepper additionally records the position's
ancestor chain as fallback candidates before the step-limit throw, so
the "show state before" view -- which halts at the selected step --
still resolves to a surviving ancestor (ultimately the ILFunction).
The highlight-range resolver is shared with the C# language.
Assisted-by: Claude:claude-opus-4-8:Claude Code
- Never introduce casts for ldobj-address-chains
- Add special-case to TransformExpressionTrees: transform addressof(ldloc) to ldloca
- Classify foreach and using variables as readonly lvalues