474 Commits
Author SHA1 Message Date
Siegfried Pammer 8e50cadcc7 Make debug step recording opt in
Non-UI decompilation runs do not consume debug steps, but debug builds still allocated steppers and step nodes unless each context opted out manually. Keep step contexts empty unless the caller enables RecordSteps, while preserving the shared stepper path for the UI debug-step view.

Assisted-by: OpenCode:openai/gpt-5.5:OpenCode
2026-10-10 21:18:52 +02:00
Siegfried Pammer d4d430ebe4 Avoid debug assert message allocations
Debug assertions should not allocate formatted messages on successful checks. Route interpolated assertion messages through a DEBUG-only handler so the message is built only when the assertion fails.

Assisted-by: OpenCode:openai/gpt-5.5:OpenCode
2026-10-10 18:34:40 +02:00
Siegfried Pammer e528a358ed Preserve VB WithEvents backing fields
AccessedThroughProperty-backed controls need to remain addressable as fields so WinForms initialization can use compiler-generated backing storage without hiding the setter that maintains WithEvents subscriptions.

Assisted-by: OpenCode:openai/gpt-5.5:OpenCode
2026-10-03 14:52:07 +02:00
Siegfried Pammer f386a148b0 Merge pull request #4160 from icsharpcode/refactor/extension-method-syntax-in-callbuilder
Decide extension method syntax in CallBuilder
2026-09-29 07:45:01 +02:00
Siegfried Pammer 8b183ab200 Recognize metadata property backing fields
Some assemblies preserve the C# backing-field naming pattern and accessor shape but omit CompilerGeneratedAttribute from the field. Use the existing property/backing-field metadata lookup so those properties can still collapse to field-backed or automatic property output instead of leaking <Name>k__BackingField.

Assisted-by: OpenCode:openai/gpt-5.5:OpenCode
2026-09-24 07:06:35 +02:00
Siegfried Pammer 9b16bf3edb Decide extension method syntax in CallBuilder
Every other decision about how a member reference is spelled is made
while the expression is built, behind Disambiguator, and CallBuilder
already writes target.ExtensionMethod for delegate references and method
groups. Extension calls were the last spelling decided afterwards, by an
AST transform that had to rebuild a resolver from a UsingScope
annotation on the tree root and re-derive every argument's resolve
result from AST annotations.

The check now runs against the resolver ExpressionBuilder already owns,
whose using scope is the namespace superset the DecompileRun collected
from the IL. That superset is wider than the imports actually printed,
so it can only refuse infix syntax where the transform allowed it, never
bind a call to a different overload.

Two consequences had to be handled. The declaring type is no longer
named anywhere once the call is infix, so IntroduceUsingDeclarations
would not have imported its namespace and the output would not compile;
it now imports from the invocation itself. And an extension GetEnumerator
in infix form is spelled like an instance call, so the foreach matcher
can no longer read a plain match as an instance method.

Writing the receiver as the target means skipping an argument, which the
argument list only supported where there were neither names nor optional
arguments: it skipped before trimming the optional tail, so a skip past
an omitted argument took one argument too many, and it gave up entirely
on names. Both now skip after trimming, which also settles a latent
off-by-one on the collection-initializer path.

Assisted-by: Claude:claude-opus-5:Claude Code
2026-09-21 16:58:00 +02:00
Siegfried Pammer c1abc29765 Merge pull request #4091 from icsharpcode/split-obj-vt
Split StackType.O into StackType.Obj+StackType.VT
2026-09-13 21:45:49 +02:00
Daniel Grunwald c760a1d622 Fix "Remove redundant comp(... != 0)" to avoid changing the value
When an IfInstruction was created with resultType=bool, because only
values 0 or 1 are possible, then we need to preserve this property in
transforms -- otherwise the added correctness test would fail.
2026-09-13 11:57:46 +02:00
Siegfried Pammer 4a0918cbe5 Look past comment placeholders when matching statement sequences
The decompiler emits comments - //IL_ warnings, "Could not convert
BlockContainer", "try-fault", a Nop's comment - as an EmptyStatement in the
middle of a statement sequence. Every transform that walks such a sequence
then stops recognizing its pattern the moment one of those lands in it:
constructor initializers stay in the body, `using var` and `for` are not
introduced, and a finalizer keeps its `override Finalize` shape, which does
not compile at all.

The destructor matcher moves the placeholders it skipped into the body that
replaces the old one, so the warning that caused the problem is not dropped
along with the statement carrying it.

Assisted-by: Claude:claude-opus-5:Claude Code
2026-09-10 20:01:19 +02:00
Siegfried Pammer 46d21ed9a5 Accept a deconstructed backing-field store only without a setter
A constructor store to an auto-property's backing field is expressible
after the field declaration is gone in one of two ways: ReplaceBackingFieldUsage
rewrites it to an assignment of a setter-less property, or
TransformFieldAndConstructorInitializers lifts it into a property initializer.
A deconstruction target assigns several members at once, so it can never take
the second route, and a property that kept a setter would invoke that setter
instead of storing the field. Without the restriction the declaration is
removed while the store keeps referencing it.

Assisted-by: Claude:claude-opus-5:Claude Code
2026-09-10 18:16:16 +02:00
Siegfried Pammer ebb8697e13 Ignore nullability when comparing backing field and property type of auto properties 2026-09-09 18:38:57 +02:00
Daniel Grunwald 05d274ff72 Merge pull request #4077 from icsharpcode/type-inference
Improvements for type inference
2026-08-30 12:59:00 +02:00
Daniel Grunwald 99613cc797 Fix comment + LINQ: validate type arguments in ThenBy-chain. 2026-08-30 12:22:36 +02:00
Daniel Grunwald 26f2a22367 Do not use LINQ query expressions when explicit type arguments were needed. 2026-08-30 12:07:46 +02:00
Siegfried Pammer e423cfb9ca Decide backing-field references by one verdict, not three
Three places decided independently whether a backing field would still be
declared, and they disagreed. ReplaceBackingFieldUsage rewrote a constructor
store into a property assignment whenever the property looked collapsible,
without asking whether PatternStatementTransform would actually remove the
declaration; ConvertField printed the "field" keyword on the FieldKeyword
setting alone, ignoring the GetterOnlyAutomaticProperties veto that
MemberIsHidden applies to the same field.

Two consequences, both silent. A setter-less property under
GetterOnlyAutomaticProperties = false kept its declaration and got "field" in
the getter anyway, so the keyword bound to a second synthesized field and the
declared one went unwritten. A settable property under AutomaticProperties =
false had its initializing store turned into a property assignment that
TransformFieldAndConstructorInitializers could no longer lift, leaving the
constructor in the output with an unconverted base-constructor call.

BackingFieldWillBeRemoved is now the single verdict every branch consults, and
it mirrors the transform's own entry gate. A property that keeps explicit
accessors is no longer addressed by name: the store stays a field reference and
becomes the property initializer, which is what field-backed storage means. The
one exception is a setter-less property's constructor store, which C# allows to
be written as an assignment and which has no other expressible form.

IsBackingFieldOfAutomaticProperty now answers through TryGetBackingField instead
of its own name check, so the two directions of "is this field that property's
storage" cannot diverge on staticness or field type. ReplaceBackingFieldUsage
dispatches on the resolve result rather than the identifier's spelling; after
ConvertField the same field appears both as "field" and under its metadata name
carrying the same annotation, so matching the name was matching the wrong thing.

The keyword's own precondition moves into a CanUseFieldKeyword local function.
Seven clauses with comment blocks wedged between them had to be read as one
expression; as one early return per rule the list reads in order.

Assisted-by: Claude:claude-opus-5[1m]:Claude Code
2026-08-30 11:48:44 +02:00
Siegfried Pammer bdf48e5a47 Recognize field-backed properties when AutomaticProperties is off
ExpressionBuilder.ConvertField prints the C# 14 "field" keyword based on the
FieldKeyword setting alone, but PatternStatementTransform only entered the
property transform when AutomaticProperties was on. With FieldKeyword on and
AutomaticProperties off the backing-field declaration was therefore never
removed, and the output declared the backing field next to accessors already
written in terms of "field".

That output still compiles, which is what makes it dangerous: the keyword binds
to a second, freshly synthesized backing field while the declared one stays
unwritten, so the recompiled assembly has different storage than the input.
Only a CS0169 "field is never used" warning hints at it.

AutomaticProperties governs only whether trivial accessors collapse to
"get;"/"set;"; the declaration removal inside the transform is a separate step
that FieldKeyword alone is enough to justify. The entry gate now mirrors
CSharpDecompiler.MemberIsHidden, which already made the field's visibility
depend on either setting, with GetterOnlyAutomaticProperties vetoing the
getter-only case for both.

Assisted-by: Claude:claude-opus-5[1m]:Claude Code
2026-08-30 11:48:44 +02:00
Christoph Wille fe9f6db3ee Emit C# 7.1 default literals where the context supplies the type
Shortening default(T) is the same problem as removing the redundant cast
around a lambda whose delegate type the context already fixes, so it uses
the same mechanism: ConvertTo makes the explicit type implicit when the
conversion is an identity conversion and the caller allows an implicit
one. The literal keeps the type it was shortened from, so any later
conversion to a different type - or any context that requires an explicit
type, such as an overload resolution recheck falling back to CastArguments
- can spell default(T) out again. That keeps the value intact where the
bare literal would change it, e.g. "object o = default(SomeStruct)", which
boxes a non-null struct while "default" would be null.

Because the shortened literal resolves to DefaultLiteralResolveResult,
CallBuilder's existing overload resolution recheck sees a real default
literal and rejects ambiguous calls on its own; no separate bookkeeping
about which arguments may stay untyped is needed. Only the contexts that
supply no target type at all restore the explicit form: an awaited
expression, and arguments of operator methods, which later become operator
or cast syntax rather than calls.

Assisted-by: Claude:claude-opus-5[1m]:Claude Code
2026-08-22 21:35:22 +02:00
Daniel Grunwald 30c6b70d9d Fix #3352: avoid using query expressions if there's indications that the lambda parameter types cannot be omitted.
I think this isn't reliable enough yet to actually omit parameter types for lambdas (it only protects against switching to the wrong overload; not against type inference failures); so for now it's only used in the query expression transform.
2026-08-17 21:51:37 +02:00
Daniel Grunwald 07b2649fdf Merge pull request #3888 from sailro/scoped-local-inference-v2
Fix #3853: infer scoped locals from ref safety
2026-08-15 22:57:58 +02:00
Siegfried Pammer 471c3b1776 Move the remaining ResolveResult subclasses into the Semantics namespace
The resolve-result hierarchy was split between ICSharpCode.Decompiler.Semantics
and ICSharpCode.Decompiler.CSharp.Resolver, so consumers had to know which half
a given result came from and import both namespaces. All subclasses now live
next to their base class; MethodListWithDeclaringType follows the method group
it describes, and ILVariableResolveResult gets its own file instead of sitting
among the syntax-tree annotation helpers.

Assisted-by: Claude:claude-opus-5[1m]:Claude Code
2026-08-15 11:03:11 +02:00
Siegfried Pammer 48fcf0313e Move UsingScope and CSharpTypeResolveContext into the type system
Both types are consumed well outside the C# output layer - DecompileRun
carries the using scope, and the IL transforms build a resolve context from
it - so living in ICSharpCode.Decompiler.CSharp.TypeSystem misrepresented
where they belong and forced a C#-specific namespace import on every
consumer.

Assisted-by: Claude:claude-opus-5[1m]:Claude Code
2026-08-15 11:03:10 +02:00
Sebastien LebretonandCopilot aab8b9b288 Fix #3853: infer scoped locals from ref safety
Local scopedness is erased from IL and PDBs, so it can only be recovered
from the body. Compare each declaration initializer with later assignments,
field stores, and receiver captures using the C# 11 ref/value escape rules,
and emit scoped only when a later operation is strictly narrower.

Assisted-by: Copilot:gpt-5.6-sol:GitHub Copilot CLI
Co-authored-by: Copilot <[email protected]>
Copilot-Session: 86d2918e-5a24-48b4-9a86-41d331ec3720
2026-08-14 16:58:36 +02:00
Siegfried Pammer 1e8ec6068b Keep VB auto-properties collapsing when the field keyword is on
VB emits an auto-property as a "_<PropertyName>" backing field plus accessors
it does not mark [CompilerGenerated]. The pre-C# 14 transform knows this: it
relaxes its accessor requirement whenever it finds such a field, which is how
a VB auto-property still prints as "{ get; set; }".

Routing every property through the field-backed path lost that. Collapsing an
accessor demanded [CompilerGenerated] unconditionally, so a VB auto-property
stopped collapsing and grew explicit "field" accessors instead - correct code,
but noise where every other compiler's equivalent stays a one-liner. Only the
legacy vbc configurations show it, and those run on Windows alone, so the
Linux and macOS jobs stayed green while both Windows ones failed on
VBPropertiesTest and Async.

Assisted-by: Claude:claude-opus-5:Claude Code
2026-08-13 13:15:21 +02:00
Siegfried Pammer 3da129632e Fix #3624: decompile semi-auto properties with the C# 14 field keyword
Backing-field references inside a property's own get/set/init accessors
are emitted as the `field` keyword at IL-to-AST translation time
(ExpressionBuilder.ConvertField), so arbitrary accessor bodies become
expressible and no separate rewrite pass is needed. Compiler-generated
trivial accessors then collapse individually to `get;`/`set;`, which
also handles mixed shapes like `{ get; set { ... field ... } }`; the
backing-field declaration is removed with its remaining attributes
re-hosted as `field:` sections, and constructor stores become property
initializers (or property assignments, for setter-less properties).
Implicit zero-stores that auto-default struct constructors emit for
unassigned backing fields are dropped rather than lifted.

Recognition stays AST/metadata-based rather than mirroring the ILAst
analysis used for automatic events: events must prove compiler-generated
bodies before discarding them, while the field keyword discards nothing,
so name association plus the accessor context is sufficient.

Below C# 14 (or with the new FieldKeyword setting off), the field
declaration survives under its metadata name, so the UI keeps showing
the truth; EscapeInvalidIdentifiers - the transform the compilable-output
flows (project export, VS, tests) already add - now maps
`<P>k__BackingField` to the readable `P__BackingField` instead of the
generic character escape. A genuine field literally named "field" is
qualified as `this.field` inside accessors, and locals are not named
"field" there, since C# 14 rebinds the bare identifier. Bodiless
accessors mixed into multi-line properties get their own line in the
output.

The fixture covering the feature surface lands with the implementation rather
than as a separate xfailed commit. It is excluded from the test-assembly
compilation because its nullable annotations would trip warnings-as-errors
there.

Assisted-by: Claude:claude-fable-5:Claude Code
2026-08-13 13:15:21 +02:00
Siegfried Pammer 3cc49044ec Extend UseLambdaSyntax to emit statement lambdas
Under UseLambdaSyntax, anonymous functions became lambdas only when an
expression body was possible; statement-bodied ones kept C# 2 delegate
syntax. Now every anonymous function whose parameter shape a lambda can
express uses lambda syntax; delegate syntax remains for ref/out/in and
params parameters and for pre-C# 3 language profiles.

Two latent issues surfaced by the wider lambda coverage: DeclareVariables
assumed an insertion point directly under a LambdaExpression is an
expression body it must convert to a block, which block-bodied lambdas
now violate; and anonymous methods declared without a parameter list
carry compiler-generated parameter names like '<p0>' that are not valid
identifiers, so the lambda's mandatory parameter list regenerates such
names from the parameter type: (object obj, EventArgs e) => ...
A side effect visible in fixtures: an explicit parameter list can make
a delegate-creation cast redundant that bare 'delegate' syntax needed
for overload resolution, e.g. new Thread((ThreadStart)delegate { })
becomes new Thread(() => { }).

Assisted-by: Claude:claude-fable-5:Claude Code
2026-08-13 10:54:52 +02:00
Sebastien LebretonandCopilot 2d2e9c15ef Keep instance-dependent assignments in constructors
A moved field initializer cannot read another instance member. Reject primary-constructor conversion for that case and preserve the original constructor.

Co-authored-by: Copilot <[email protected]>
Copilot-Session: 0dd407b6-9410-48df-add5-761ca4a8dec0
2026-08-12 09:24:38 +02:00
Siegfried Pammer 4a2afbb723 Walk indexer accesses when looking for a null-conditional source
A query source can be reached through an indexer as well as through a member
access or a call: `holder?[0].Where(...).Select(...)` puts an IndexerExpression
between the LINQ call and the `?.`. The receiver walk stopped there, so query
syntax was still introduced over a source the conditional access had lifted to
a nullable value type, and the output failed to compile with CS1936 - the same
way as the case that was reported, one node kind further along.

IndexerExpression.Target is nullable where MemberReferenceExpression's and
InvocationExpression's are not, so only that arm needs to match on the target.

Assisted-by: Claude:claude-opus-5[1m]:Claude Code
2026-08-10 19:32:41 +02:00
Sebastien LebretonandCopilot 3a8e7dee23 Keep null-conditional LINQ chains in method syntax
Query syntax cannot preserve a null-conditional receiver that lifts a value type. Detect null conditionals through the LINQ receiver chain before introducing query syntax.

Co-authored-by: Copilot <[email protected]>
Copilot-Session: 0dd407b6-9410-48df-add5-761ca4a8dec0
2026-08-10 18:17:52 +02:00
Siegfried Pammer 247cc1f3ed Declare VB anonymous types that C# anonymous types cannot express
A C# anonymous type is immutable and compares every member. VB's are neither
unless every property is declared 'Key': otherwise the properties are settable
and only the 'Key' ones take part in Equals and GetHashCode. Writing such a
type as 'new { ... }' silently gave it value equality and made any assignment
to one of its properties fail to compile, so only an anonymous type with no
settable property is treated as one; the rest keep their own declaration.

Those declarations carry the shape VB gave them, so the round-trip preserves
both mutability and 'Key' equality. Their names are the remaining obstacle,
since the VB compiler separates the parts with '$': the type, its backing
fields and any local named after it are renamed to use '_' instead, and a
comment on the declaration says why the type is spelled out.

Generated variable names are now rejected when they would not be legal C#
identifiers, which also stops a display class from lending its unspeakable
name to a local in the NoLocalFunctions output.

Assisted-by: Claude:claude-fable-5:Claude Code
2026-08-07 12:04:25 +02:00
Siegfried Pammer 8873983ff9 Gate decrement conversion behind IntroduceIncrementAndDecrement
Operator precedence made the condition read
(setting && Add) || Subtract, so 'x -= 1' was converted to 'x--' even
with IntroduceIncrementAndDecrement disabled. Only observable with the
non-default setting, which no fixture configuration exercises, so no
test accompanies the fix.

Assisted-by: Claude:claude-fable-5:Claude Code
2026-08-03 23:17:13 +02:00
Siegfried Pammer 87c13f99dc Convert float/double += 1 into increment and decrement expressions
Reviving the 2020 test-cases-fp-types fixtures (compound assignment on
float, double and decimal) exposed an asymmetry: post-increment and
post-decrement on float/double round-tripped as x++/x--, but the pre
forms came back as x += 1f because the increment detection in
PrettifyAssignments only accepted integer constants. C# defines ++/--
on floating-point types as adding or subtracting exactly 1, so the
conversion is exact for a constant 1 operand. Decimal already works
through the op_Increment/op_Decrement path.

Assisted-by: Claude:claude-fable-5:Claude Code
2026-08-03 23:17:13 +02:00
Siegfried Pammer 06480c2625 Show the owning property's documentation for accessor methods
The compiler-generated documentation file contains a P: entry for a
parameterized property, but its accessors are emitted as ordinary
methods, whose M: id has no documentation entry. Fall back to the
owning property's documentation both when inserting XML documentation
into decompiled output (on the first accessor only, to avoid
duplicating it) and in the text view's tooltip (for either accessor).

Assisted-by: Claude:claude-fable-5:Claude Code
2026-07-27 16:28:34 +02:00
Siegfried Pammer 3a6b074930 Merge pull request #3896 from sailro/fix-3894-span-concat
Fix #3895: keep byref-like ToString in string concatenation
2026-07-23 17:03:47 +02:00
Sebastien LebretonandCopilot 675fca83a8 Fix #3895: keep byref-like ToString in string concatenation
String concatenation cannot box a byref-like value, so removing its explicit
ToString call produces invalid C#. Preserve that call while retaining the
surrounding string-concatenation reconstruction.

Assisted-by: Copilot:gpt-5.6-sol:GitHub Copilot CLI
Co-authored-by: Copilot <[email protected]>
Copilot-Session: 86d2918e-5a24-48b4-9a86-41d331ec3720
2026-07-20 01:02:38 +02:00
Sebastien LebretonandCopilot ad991548b0 Fix #3892: preserve unsafe for pointer constructors
Object creation can require an unsafe context solely because the selected
constructor has a pointer parameter. Apply the existing unsafe-signature
check to object creation nodes so the emitted declaration remains compilable.

Assisted-by: Copilot:gpt-5.6-sol:GitHub Copilot CLI
Co-authored-by: Copilot <[email protected]>
Copilot-Session: 86d2918e-5a24-48b4-9a86-41d331ec3720
2026-07-20 00:30:04 +02:00
Siegfried Pammer 7554c1b004 Remove the syntactic automatic-event patterns
With recognition on the ILAst and reference substitution during
translation, the four accessor-body patterns in
PatternStatementTransform were only reachable as a fallback, and any
divergence between them and the AutoEventDecompiler verdict produced
inconsistent output. A compiler shape the ILAst matchers do not know now
degrades to explicit accessors with the backing field kept in the
output, which stays compilable. Bodyless events (abstract, extern,
interface members) previously relied on the patterns' no-body clause to
become field-like; since C# cannot express bodyless custom accessors,
DoDecompile now chooses the field-like form for them directly. Also
deletes the orphaned IsEventBackingFieldName helper; the name
association lives in PropertyAndEventBackingFieldLookup.

Assisted-by: Claude:claude-fable-5:Claude Code
Claude-Session: https://claude.ai/code/session_01Btdypgm8utyxqt1Etn2BDi
2026-07-18 21:56:16 +02:00
Siegfried Pammer 461df88572 Replace automatic-event backing-field references during translation
PatternStatementTransform renamed backing-field identifiers to the event
after the fact, keyed on the metadata name association alone: references
belonging to an event that is not actually automatic were still renamed,
binding them to a custom event that is unusable as a value (the same
defect class as #3858), and the rename bypassed the resolver checks, so
qualifiers were computed for the hidden field instead of the printed
event. ExpressionBuilder.ConvertField now performs the substitution,
keyed on the AutoEventDecompiler verdict whose memo moves into
DecompileRun so that member hiding, the event declaration, and reference
translation all decide from one analysis. Checking the verdict's field
identity also keeps same-typed sibling events apart (#3575), and the
qualifier logic running against the event drops spurious this./type
qualifiers from raise sites.

mcs 2.x accesses a sibling automatic event's backing field directly
inside custom accessors instead of calling the accessor, so the fixture
expects the resulting Delegate.Combine form there.

Assisted-by: Claude:claude-fable-5:Claude Code
Claude-Session: https://claude.ai/code/session_01Btdypgm8utyxqt1Etn2BDi
2026-07-18 21:56:16 +02:00
Siegfried Pammer 1b9131905d Fix #3858: Detect automatic events on the ILAst instead of the C# AST
The syntactic accessor-body patterns in PatternStatementTransform sit
downstream of every settings-dependent transform, so each new compiler
shape or settings combination silently broke recognition: with
AggressiveInlining enabled, static events inline the Delegate.Combine
call into CompareExchange positionally, which none of the four patterns
matched, while call sites were still rewritten to the event name from
metadata alone - producing uncompilable output (CS0079).

Recognition now happens in DoDecompile(IEvent) by structurally matching
the ILAst of the accessors, decompiled with a fixed set of settings the
same way RecordDecompiler analyzes method bodies. This makes detection
independent of the user-visible settings by construction. Events that
are not recognized fall back to the classic path unchanged, including
the existing AST patterns.

mcs 2.x compiles the accessors as a compound assignment, evaluating
'this' once via IL 'dup'; the simple-combine matcher accepts that
stack-slot alias.

Assisted-by: Claude:claude-fable-5:Claude Code
2026-07-18 21:56:16 +02:00
Siegfried Pammer 1e6a19e2c4 Fix #3860: Avoid 'out var' if the variable recurs in the argument list
Passing the same local as multiple out arguments of one call made
DeclareVariables turn the first use into an implicitly-typed declaration,
producing 'f(out var x, out x)'. Referencing an implicitly-typed out
variable in another argument of the declaring call is rejected by the
compiler (CS8196), because its type is only inferred once overload
resolution of that call has completed. The explicitly-typed form
'f(out int x, out x)' is valid, so fall back to the explicit type in
that case.

Assisted-by: Claude:claude-fable-5:Claude Code
2026-07-11 13:24:18 +02:00
Siegfried Pammer 07d5a01d2b Add license headers to source files that lacked them
Backfills the standard MIT X11 header on hand-written files that never
got one, attributing each to its first-commit author and year from git
history. Code vendored from dotnet/runtime and Humanizr/Humanizer gets
its origin's license lines and a provenance note instead. Generated
files (Resources.Designer.cs, the version-info template), tool-managed
suppression files, and BAML test-case fixtures intentionally stay
header-less.

Assisted-by: Claude:claude-fable-5:Claude Code
2026-07-05 21:09:46 +02:00
Siegfried Pammer f380b31a27 Fix #3804: reconstruct foreach over an inline array
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
2026-07-05 20:43:51 +02:00
Sebastien Lebreton 44ac0dd05d Address review: keep while-loop instead of headless for for ref locals
Per @siegfriedpammer: rather than rescue the hoisted for-initializer in
DeclareVariables, don't form the for-loop at all. TransformFor now bails when the
loop variable is a by-ref-like local used after the loop, leaving the while-loop
(matching source); the ref decl keeps its initializer, no CS8174. Reverts the
DeclareVariables change; test now expects the while form.

Assisted-by: Copilot:claude-opus-4.8:GitHub Copilot CLI
2026-07-05 14:24:08 +02:00
Sebastien Lebreton 13c80c4588 Fix #3827: keep the initializer on a ref local hoisted out of a for-loop
A ref local that is used after a for-loop has its declaration hoisted in front of
the loop, but its only initialization is the for-initializer ref-assignment. The
declaration was then emitted without an initializer (`ref T x;`), which does not
compile (CS8174).

When a by-ref-like local's matching assignment is the first for-initializer, move
the ref-assignment's value up into the declaration (`ref T x = ref expr;`) and
drop the for-initializer.

Assisted-by: Copilot:claude-opus-4.8:GitHub Copilot CLI
2026-07-05 14:24:08 +02:00
Siegfried Pammer 112f32f88f Lift chained ctor calls when sibling constructors' field inits diverge
A type's leading field assignments are extracted to field declarations
only when every constructor that does not chain with this() agrees on
them. When they disagree, the analyzer gave up on the whole type, so the
remaining constructors' this()/base() calls were never lifted into
initializers -- a struct with two divergent constructors plus a chaining
one rendered the chain as `this = new TSelf(...)` instead of `: this(...)`.

Chain lifting does not depend on the shared-initializer extraction, so a
mismatch now just skips the extraction (the assignments stay in the
bodies) and the transform continues. Primary constructors still bail,
since their parameters drive the initializers that must be extracted.

Assisted-by: Claude:claude-opus-4-8:Claude Code
2026-07-05 10:14:42 +02:00
Siegfried Pammer d35c082616 Move debug-step stepping to a neutral namespace
The step recorder is shared by C# AST and ILAst replay, so keep the public infrastructure out of IL.Transforms and pass language-specific node navigation through neutral node metadata.

Assisted-by: OpenCode:openai/gpt-5.5:OpenCode
2026-07-02 01:08:51 +02:00
Siegfried Pammer ffd1eac3d0 Share debug-step candidate recording between IL and C#
The IL (Stepper) and C# (TransformContext) paths each recorded the changed
node, its seam neighbours, and its ancestor chain with duplicated logic that
could drift. Move the ordering/dedup/seam strategy onto
Stepper.Node.RecordModifiedNode; each language keeps only its own node
navigation.

Assisted-by: Claude:claude-opus-4-8:Claude Code
2026-07-02 01:08:51 +02:00
Siegfried Pammer 55fbd6d53f Consolidate unsafe-modifier step guards
Keep the IsUnsafe visitor path context-free while avoiding repeated null guards around debug-step recording in the unsafe modifier transform.

Assisted-by: OpenCode:openai/gpt-5.5:OpenCode
2026-07-02 01:08:51 +02:00
Siegfried Pammer 6ee3462ece Ignore null C# debug-step produced nodes
Keep existing modified-node candidates when a transform cannot provide a produced node, matching the IL transform helper and preserving highlight fallback quality.

Assisted-by: OpenCode:openai/gpt-5.5:OpenCode
2026-07-02 01:08:51 +02:00
Siegfried Pammer e43dab7e11 Record candidates for the C# step that hits the step limit
When the step limit falls on a C# transform step, Stepper.Step records the node
as LimitReachedStep but throws before TransformContext can attach the node's
highlight candidates, so the 'show state before' view had only the bare modified
node to resolve against -- and nothing if that node renders no text of its own.
The IL path already records its candidates before the throw; mirror that on the
C# side by attaching the candidates to the limit-reached node in the catch, then
re-throwing so the pipeline still halts.

Assisted-by: Claude:claude-opus-4-8:Claude Code
2026-07-02 01:08:51 +02:00
Siegfried Pammer 2bd41edfaf Bridge only the debug-step marker in NodeLookup
NodeLookup.AddNode indexed every annotation of every rendered node by reference
identity, but the debug-step highlighter only ever looks up the DebugStepMarker;
the rest were dead keys, and a shared annotation (ResolveResult and friends,
copied across nodes) would resolve to whichever node rendered last. Make
DebugStepMarker public and bridge only it -- behaviour-preserving for resolution
while dropping the per-annotation dictionary churn on every rendered node.

Assisted-by: Claude:claude-opus-4-8:Claude Code
2026-07-02 01:08:51 +02:00