Release notes
G# is pre-1.0. The repository's version base is currently 0.5, and product versions are derived by Nerdbank.GitVersioning from that base and the Git commit. Until the project reaches a stable compatibility promise, release notes should be read as implementation status notes rather than a long-term compatibility contract.
Reading these notes
You are reading development documentation. Select 0.4 in the version menu for the published release snapshot. Check the selected documentation version before applying a migration or copying a command.
The published 0.4.591 release is the version used by the website's installation instructions and executable showcase examples. Start with installation, data types, channel concurrency, and canonical formatting. Detailed changes and compatibility notes follow.
Unreleased (0.5 line)
-
The compiler now builds with the published G# SDK 0.5.179 (issue #2189). The bootstrap pin moves from 0.5.7 to the release containing the measured compiler lookup, generator setup and bounded-worker improvements. The analyzer-testing package follows the same pin. This separate pin update builds the repository with that released compiler; its CI provides stage-2 verification under the N-1 rule.
-
Declaration discovery skips impossible anonymous-object walks (issue #5055, ADR-0203). Parsing records whether each source tree can contain an anonymous object. Generator projection and compilation avoid the full-tree rich-anonymous declaration search for other trees, while keeping the existing synthesis order where needed. Core profiles eliminated 700 walks per declaration pass and reduced projection/declaration allocations by 667/410 MiB; the three-pair compiler and Oahu rebuild comparison did not demonstrate a build-speed gain. Declaration scopes remain request-owned, and generated partials still bind afresh; generator execution, diagnostics and SDK success stamps are unchanged.
-
Generated-document translation avoids metadata-only name scans (issue #5054, ADR-0203).
gsgenreuses the active Roslyn semantic model when reserving imports. Source-name collision checks visit source-bearing assemblies instead of traversing every metadata type. Globally visible source references, aliases, nullable types and generated partial declarations retain their existing spellings. The request stays sequential and uses no new cache. -
Bounded method-local lowering and instruction emission (issue #5042, ADR-0203). Opt-in compiler workers can lower noncapturing primitive lambdas and emit primitive/local method instructions into independent buffers. Cross-body discovery, metadata, generic remaps and PDB publication remain ordered; unknown or metadata-dependent bodies keep the sequential path. The existing shared worker and memory budget applies. Measurements now separate discovery, method instructions, implementation/reference output, PDB construction and PE serialization.
-
Opt-in bounded compiler workers (issue #5041, ADR-0203).
GSharpMaxWorkers(default 1) requests one MSBuild core grant shared across compiler phases and reusable requests; unsupported hosts stay sequential. File parsing and proven primitive/local bodies run independently, then publish results and diagnostics in their established order. Inferred signatures and shared synthesis remain coordinator-owned.GSharpWorkerMemoryBudgetMiBbounds workspace concurrency estimates, not the total process heap. Direct compiler users can select--workers:Nand--worker-memory-mib:N. Other bodies remain sequential; no general workload speedup is promised. -
Candidate lookup indexes and opt-in compiler measurements (issue #5040). Alias collision and unknown-arity nested lookup use name/arity and canonical-owner candidate buckets instead of scanning every alias. Imported extension metadata and record-shape classification reuse context-sensitive indexes, including negative results. Persisted reference indexes reject changed reference contents even when assembly identities match. Set
GSHARP_MEASURE_PHASESto a JSON path for nested phase timings, process counters, and input/cache metrics without changing diagnostics. -
Suspending
for … inloops dispose their enumerators (issue #5037). Loop cleanup now survivesyieldandawait: exhaustion, early consumer disposal,break,return, and exceptions release acquired disposable enumerators. Nested iterator cleanup runs inside-out even when an inner enumerator'sDisposethrows. -
Bounded compiler reuse is opt-in (issue #5038).
GSharpUseSharedCompilationreuses one serial compiler worker per MSBuild node and build. Requests isolate input paths, options, environment, and reference contexts. Workers retire by request count, idle time, or memory. Transport failures are explicit; compiler failures are never retried. Imported generic symbols no longer retain private metadata contexts through a host assembly's cache bucket. -
Generator discovery and setup avoid repeated work (issue #5039).
gsgendiscovers actual generators before loading G# sources or projecting declarations. Generator and foreign C# translation share metadata references, and back-translation reuses immutable Roslyn input and generated syntax trees. Opt-in bounded build workers reuse host code and content-validated metadata while loading fresh generators for every request. Empty manifests never bypass generation or diagnostics. Generator input descriptors and task-validated success stamps preserve no-op builds while invalidating changed options, metadata, and item membership; diagnostic errors remain visible on repeated builds, and removing the final source and analyzer deletes stale generated parts. Opt-in phase reports separate discovery, setup, projection, execution, translation, and output. -
Body-only edits preserve imported types in the language server (issue #5025). Incremental binding now moves each explicit import and alias to the edited file's current syntax tree along with its member declarations. Repeated edits no longer lose per-file namespace context, degrade inferred local hovers to
?, or introduce false missing-type/function diagnostics. This also fixes the same failure when VS Code opens unchanged text with a lowercase, encoded Windows drive URI. -
Local hovers work when a file opens before workspace discovery finishes (issue #5024). The language server now replaces the standalone document tree with its owning project's matching tree after discovery, preserving unsaved buffer text. Local declarations and references resolve without reopening the file or making another edit, including after a server restart with the file open. Watcher refreshes preserve regenerated project source when an older generated document remains open.
-
Stepping over a handled exception reaches the catch body instead of an earlier source line (issue #5014). Structural
trywrappers no longer replace the first statement's debug location, and catch/filter setup is marked as hidden compiler-generated code. This fixes the jump to the first top-level statement in Visual Studio and VS Code, while preserving source locations in ordinary functions and finally bodies. -
Test Explorer keeps loose-file tests and project tests visible (issue #5013). An open file without a project caused test discovery to throw a
NullReferenceExceptionand return no tests. Discovery now preserves nullable project metadata for loose files. It also matches open document URIs to file paths, so a buffer opened before project discovery replaces its disk snapshot instead of appearing as a duplicate loose test. -
A G# library's nullable annotations now reach its consumers from every kind of member (issue #4988). A member compiled without a method body lost the nullability of its return type in metadata, so a separately compiled consumer saw
Task[Token]for an interface method declaredfunc LoadAsync() Task[Token?];, typedawait store.LoadAsync()as non-null and reported a spurious GS0536 on its!!. The same loss affected abstractsharedinterface methods, a delegate'sInvoke, auto-property getters and setters, theadd/removeaccessors of field-like events, interface properties, events and indexers, and a data class'sDeconstruct. gsc now writes the same nullability metadata for a signature whichever way it was declared, and it matches what csc writes for the equivalent C#, including for netstandard2.0 libraries. A data class's==and!=now takeT?operands, like a C# record's. A consumer that depended on the lost annotations can now get nullability diagnostics it did not get before. -
Breaking: statement
if letnow has the same header as theif letexpression (issue #4961, ADR-0201).if let text = value && text.Length > 0 { … }reported GS0157 ("Cannot find type text"): the statement form folded the&&into the initializer, so the guard referencedtextoutside its scope. The statement and expression forms now share one header. A top-level&&after the last binding starts an optionalboolguard. Bindings are evaluated left to right and stop at the firstnil, so later initializers and the guard don't run; the statement form used to evaluate every initializer before testing any of them. A later initializer sees earlier bindings at their non-null type (if let a = s, let n = Len(a)reported GS0154). A statement with more than one binding and anelsewhose arms bothreturnno longer reports GS0100 ("Not all code paths return a value"). The bound names are visible in later initializers, the guard and the then-block only. In both forms the then-block now sees the guard as anifbranch sees its condition:&& o != nilnarrowso, and&& o is string tputstin scope (the expression form reported GS0159 and GS0532 for these). Every initializer ends at a top-level&&or||;??and?:initializers still work, and a?:initializer now also parses in the expression form. Compatibility: a statement initializer such asif let v = a && b ? x : yused to bind the whole conditional. It now reports GS0624; writeif let v = (a && b ? x : y)to keep the old meaning. Nullable assignment or conditional conversion can also hide the boolean: withcached bool?,if let v = cached = a && bnow reports GS0624 rather than silently changing the stored value and body execution. Parenthesize the entire initializer to preserve its meaning, or the prefix assignment/conditional to make the new guard explicit. Hover, go-to-definition and references now resolve anif letbinding in either form, including its uses in later initializers and the guard; before, they resolved nothing, or a same-named outer local. New diagnostics: GS0624 (an ambiguous initializer/guard boundary, including nullable conversions), GS0625 (guard letandwhile letdon't accept a guard yet, #4968; they reported GS0157 before) and GS0626 (a top-level||after the bindings). -
An
ifexpression accepts the sameif init; condinitializer as theifstatement (issue #4962, ADR-0064).let s = if var v = g(); v > 3 { "big ${v}" } else { "small ${v}" }reported a spurious GS0276 and a series of GS0005 errors, and so did a trailingif var v = g(); v > 3 { … } else { … }at the end of an arrow-lambda block body, even though the sameifcompiled as a statement. Both forms now read the header the same way. The initializer runs once, before the condition. What it declares is visible in the condition, in both branches and in everyelse iflink, and not after theif. gsfmt also keeps anif init; condheader on one line in both forms; it used to put a line break after the;. -
Hot reload calls
MetadataUpdateHandlercallbacks after each applied update (issue #4960). Underdotnet watch, G# applied method-body updates to the running process but never called the application's[assembly: MetadataUpdateHandler(typeof(T))]handlers. A UI framework that redraws fromUpdateApplicationtherefore kept showing old output until another event triggered a redraw. This affects every release with G# hot reload, including 0.4. Handlers now run as they do for C# underdotnet watch. EveryClearCacheruns before anyUpdateApplication, and handlers in referenced assemblies run before those of the assemblies that reference them. Each callback receives the changed types in update order, each type once: the type of each edited method and the types that contain it. A handler that throws, or one whose methods do not have the staticvoid (Type[]?)shape, is reported asGSHR1005, and the other handlers still run. -
The language server reports the member the compiler bound for a call on a narrowed or interface-typed receiver (issue #4989). After
guard let s = store, wherestoreis declaredIStore?and assigned a different implementation on each branch, hoverings.LoadAsync()could show one implementation's method (for examplefunc (KeychainTokenStore) LoadAsync()) when the interface came from a referenced assembly and that implementation'sLoadAsyncwas called elsewhere in the program. With the interface declared in source, hover and go-to-definition on the call showed nothing, and hoveringsshowed nothing either. Hover, go-to-definition, find references, signature help, semantic tokens and member completion now use the member and receiver type the compiler bound, sos.LoadAsync()isIStore.LoadAsync. The compiler itself was already correct, in 0.4.1270 too:sreads asIStore, and calling a member that only one implementation declares reports GS0159. Hovering the binding still shows its storage type,IStore?. -
Blocks, arrow bodies and
switchfollow one rule for their last item (issues #4963, #4965, #4966, ADR-0202). A value block can now end inthrow,return,breakorcontinue:let a = if c { 1 } else { return 0 }andlet v int32 = { log("x") throw e }compile, and such a branch takes the other branch's type. A switch expression can be the last item of a block, an if-branch or a lambda body, and aswitchwith:arms also works as a statement.case 1, 2:works in switch expressions, not only in switch statements.func f() T -> { … }takes the same block body a lambda does, soreturnstatements and a body ending inforwork. A void arrow body accepts anifwithoutelseand a switch with void arms (set -> if value > 0 { n = value }), and so does a lambda whose value is discarded.(c) -> { if c { return 1 } else { return 2 } }and a void lambda ending inif c { list.Remove(x) } else { list.Clear() }compile. A void switch, like a switch statement, needs nodefault.let a = throw ereports GS0124 instead of crashing the compiler. Compatibility: a lambda body whose last item is a nested{ … }or anif let … else …now returns that block's value, as a lambda ending in a plainif … else …already did; this can change an inferred lambda type (anAction/Funcoverload choice) when that value is not void. End the lambda withreturnto keep it void. -
??=is an expression, like every other assignment operator (issue #4967, ADR-0072 amendment).let r = (x ??= y),f(x ??= y),return x ??= y,x = y ??= zand the right-associative chaina ??= b ??= cused to fail to parse (GS0005);=, the compound operators and++/--already worked in those positions. The value is the left operand's final value: the value it already held, or the assigned value of the right side. When the right side is known non-nil its type is the target's underlying type, as in C#, soint32? ??= 42is anint32andstring? ??= "x"is astring; a right side that can be nil, including an unannotated (T!) value, keepsT?. The target's receiver and indices are still evaluated once and the right side only when the target is nil. Existing??=statements keep their lowering and narrowing, with the receiver-storage correction below. -
??=writes through struct receivers instead of losing the update in a copy (issue #5005). Both positions now update an array element (a[i].F ??= v), a nested field (h.Outer.Inner.F ??= v) or a property on that storage in place, including CLR fields across REPL submissions. The shared capture saves only reference owners and indices; a right side that awaits and reassigns those variables still writes to the original location, without a byref state-machine field. Receiver null and bounds checks still precede indexer-argument side effects. Read-only struct receivers and struct temporaries now report the same assignment diagnostics as=instead of writing a discarded copy. Smart-cast boxed struct receivers report GS0499 instead of emitting an invalid write. -
The conditional operator
?:narrows its branches like theifexpression (issue #4964).s != nil ? s.Length : 0,s == nil ? 0 : s.Length,(s != nil && t != nil) ? s.Length + t.Length : 0and(o is string) ? o.Length : 0reported GS0158 (Cannot find member Length), while the same code written asif s != nil { s.Length } else { 0 }compiled. A?:branch now sees the same nil-guard and type-test narrowing as the matchingif-expression branch, through!,&&,||, nullable value types (int32?) and platform types (T!). 0.4.1270 rejected these too, so this is not a regression. Because a narrowed branch has the narrowed type, a ternary that reads the guarded value can now have a narrower result type, as theifexpression already had: withn int32?,n != nil ? n : 0isint32rather thanint32?, which can change an inferredlet/vartype or the overload chosen for an argument. To keep the nullable type, state it (let r int32? = n != nil ? n : 0). A!!on a value the condition already proved non-nil, such ass != nil ? s!!.Length : 0, now reports the GS0536 redundant-assertion warning, as it does in anifbranch; remove the!!.Nullable<T>members keep working on the guarded value, in every narrowing form:n != nil ? n.Value : 0,n.HasValueandn.GetValueOrDefault()bind against theNullable<T>storage when the narrowed type has no member of that name, and now also compile after anif n != nilstatement or early return, where they used to report GS0158. A member the narrowed type has itself (a struct's ownValue) still binds to it.ValueandHasValuealso bind on a nullable same-compilation struct or enum (issue #4986). In an expression-tree lambda,(v int32?) -> v != nil ? v + 1 : 0used to compile and then throw when the tree was built; it now reports GS0473, as theifform does (issue #4974; write(v ?? -1) + 1). -
Calling a narrowed nullable function pointer no longer crashes (issue #4943). After a nil check (
if fn != nil { fn() }, an earlyreturn,if let fn = … { fn() },guard let), calling a(unmanaged[CC] () -> R)?local or parameter emittedcallion itsNullable<nint>storage instead of the address, so the call jumped to a garbage address and the process died with SIGSEGV. This happened since function pointers were introduced, so it is not a 0.4 regression. The call path now builds the narrowed callee through the same unwrap as every other narrowed read. A narrowed nullable function-pointer field also becomes callable, asFp(),this.Fp()orh.Fp();this.Fp()andh.Fp()reported GS0159 before. GS0536's advice to drop the redundant!!fromfn!!()is now safe to follow. The emitter now reports an internal error (GS9998) instead of emitting a narrowed value-type nullable read that skipped its unwrap. -
cast[T]converts an address to a raw function pointer, andas/isagainst a function-pointer type work again (issue #4938).cast[unmanaged[Cdecl] () -> int32](address)reported GS0125 / GS0130: the look-ahead that decides whethername[...]is a generic call did not recognise theunmanaged[CC] (T) -> Rclause, socast[...]parsed as an indexer. It now parses with or without[CC], with pointer parameters (unmanaged[Cdecl] (nint, *uint8) -> nint), and as a pointer target (cast[*uint8](address)); the calledcast[T](x)no longer reports GS0521, which still applies to every other generic argument list.[]unmanaged[Cdecl] () -> int32also parses as an array of function pointers (it readunmanagedas the element's type name). Separately,address as (unmanaged[CC] () -> R)?compiled but always returned nil,x is unmanaged[…]was always false andcase f is unmanaged[…]never matched, since v0.4.1270 and in every 0.5.x: issue #4685 made the type-test opcodes name the function pointer's FNPTR type, which no boxed value is an instance of. They nameSystem.IntPtragain, the behaviour of 0.4.1150, soasfrom anintyields the pointer (ADR-0095, amendment for #4938). Arrays of function pointers keep their exact FNPTR element type. -
The language server now ships MessagePack 2.5.307, which fixes GHSA-qhrr-8q5h-9q3h (issue #4941; "Nested arrays can blow past buffer length limits", moderate severity, patched in 2.5.303). The language server (and the REPL, which references it) reaches MessagePack through StreamJsonRpc, whose dependency floor is below the patched version, so the repository pins it in
build/gsharp.build.propsand in the isolatedsrc/vs-gsharp/test/VsGsharp.UnitTestsproject. Before the bump, NuGetAudit failed every restore of a project that pulled in 2.5.301 withNU1902(warnings are errors). The pin stays on the 2.5 line that StreamJsonRpc 2.22.x is built against. -
gsc embeds its own
NullableAttribute/NullableContextAttributefor netstandard2.0 and .NET Framework targets, and no longer references the build host'sSystem.Private.CoreLib(issues #4921 and #4922). Compiling for a target whose framework publishes neither attribute resolved them through the host-runtime fallback and emitted TypeRefs scoped toSystem.Private.CoreLib, an assembly that does not exist on .NET Framework, so the SDK task assembly built by gsc could not load under Visual Studio's MSBuild. Such outputs now carryinternalcopies of both attributes (plusMicrosoft.CodeAnalysis.EmbeddedAttribute) in the assembly, as csc emits, and nullability metadata is stamped against them, so it round-trips for consumers of netstandard2.0 libraries. A copy the project declares itself, or one a referenced framework publishes, is used instead.src/Sdk/Gsharp.NET.Sdkis now compiled by thegscbuilt from the same commit rather than the pinned bootstrap SDK, so a release ships a cleantools/task/Gsharp.NET.Sdk.dllwithout waiting for a second release;build/check-netstandard-host-refs.pyfails thebuildjob if the packed task assembly references anySystem.Private.*assembly. -
A netstandard2.0 / .NET Framework compilation binds only to members its target framework declares (issue #4924). Instance calls on a predefined type (
string,object,int32, ...) enumerated the build host'sSystem.Private.CoreLibtype, sos.Split(',')in the netstandard2.0 SDK task bound toSplit(char, StringSplitOptions), which .NET Framework does not have, and Visual Studio's MSBuild failed every G# build withMissingMethodException(released in 0.5.6). Member lookup on a receiver now goes through the target's declaration (ReferenceResolver.TryGetMemberLookupType, also used for staticstring.Xaccess), so the call bindsSplit(params char[]); a receiver whose type the downlevel target does not declare at all (a host type a dependency's signature pulled in, such asSystem.DateOnly) reports GS0546 instead of binding the host's members. For a downlevel target the resolver also no longer adds the host's BCL assemblies to what binding can name, so a host-only extension such asMemoryExtensions.ContainsorCollectionExtensions.TryAddis no longer bound (and referenced fromSystem.Private.CoreLib); such calls now report GS0159, as with csc. Code that relied on a host-only overload must use one netstandard2.0 has, and a netstandard2.0String.Splitresult is[]!string!(an oblivious array) like any other unannotated netstandard2.0 member.build/check-netstandard-member-refs.pynow fails thebuildjob if any TypeRef or MemberRef of the packed task assembly does not resolve on its netstandard2.0 reference surface, and a newdesktop-msbuild-sdk-smokejob buildsInternalAnalyzers.gsprojwith Visual Studio's MSBuild against the SDK package from the same build; publishing requires it. -
Design decision:
data class/data structwill synthesize the C# record ABI (ADR-0199, issue #3501). Planned breaking change:ToStringadopts the C# record format (Point { X = 3, Y = 4 }) in place ofPoint(X=3, Y=4), every data type implementsIEquatable<T>, declared data classes and structs getPrintMembers, and cs2gs stops emitting the@__Cs2GsRecordProvenance_4828marker. This entry records the accepted design; the behavior ships in follow-up PRs. -
cs2gs migrate --stop-after <stage>(issue #4853, parent #3501). Runs the stages up to and includingtranslate,compile,ilverifyortest-parity(the default).--stop-after compilestill performs the redundant-!!polish and writes the migrated tree, so a cut-over rehearsal no longer pays for test parity. A run that stops beforetest-parityrecords its passing apps as unverified (not green) and prints a partial-run line.--translate-onlyselects the same stages as--stop-after translatebut keeps its own reporting: it does not mark apps unverified, because the nightly pairs it withcs2gs validateshards. -
The
@__Cs2GsRecordProvenance_4828marker is removed (ADR-0199 seam a, issues #3501, #4828). cs2gs no longer tags translated records and gsc no longer recognizes the name, so it is an ordinary unknown annotation. A translated body-only record now keeps G#'s ownDeconstruct(fields, then auto properties), like a hand-writtendata class. The Core public-API snapshot allows exactly those ten recordDeconstructadditions throughgsharp-core-public-api-allowed-additions.txt; any other addition still fails. -
Breaking:
data class/data structprint and compare like a C#record(ADR-0199, issue #3501).ToStringnow renders the C# record format,Point { X = 3, Y = 4 }(Empty { }when there are no members), instead ofPoint(X=3, Y=4). Values use the current culture, as in C#, so afloat64prints1,5underde-DE; the old invariant-culture rule is gone. Only public fields and readable public properties print, in declaration order, and members inherited from a data base now appear. Every declared data type synthesizes the recordPrintMembers(StringBuilder)slot (private on a sealed type that starts it, otherwiseprotected virtual), and a hand-writtenPrintMembersof the compatible shape replaces it (GS0623 rejects any other shape). Every data type, including the backing type of adata objectorobjectliteral, now implementsIEquatable<Self>. Anonymous literals print as{ Name = David, Count = 2 }with no type name and get noPrintMembers, copy constructor or clone. cs2gs no longer generates aPrintMembersmethod for a translated record and its test-name parity no longer rewrites record spellings. -
Data-type documentation now describes the C# record ABI (ADR-0199 seam c, issue #3501). The language spec, the data-types tour page and the ADR-0029 diagnostics text describe the record
ToStringformat, current-culture formatting, implicitIEquatable<T>,PrintMembersand GS0623; ADR-0029, ADR-0146 item 6 and ADR-0157 carry supersession notes, and ADR-0199 is Accepted. Known gaps are tracked in #4874 (cs2gs duplicates a base parameter repeated by a derived positional record), #4876 (GS0387 over an abstractPrintMembers), #4877 (imported non-data intermediary hides the inherited slot) and #4879 (cs2gs-translated C# anonymous types print their generated type name). -
cs2gs migrate mirrors every file a project references and ignores stray
out/outputs (issues #4850 and #4852, parent #3501). A.csfile referenced as data or foreign input (aNone/Content/EmbeddedResourceitem, or aCompileitem of an already-G# project such assamples/ForeignCompile/ThisAssembly.csand the Adr0169 fixtures) is now copied verbatim instead of being translated or dropped; one decision point (RepositoryVerbatimSources) feeds the mirror copy, the orphan step and the completeness check, and an include it cannot resolve statically is reported as a warning. The top-levelout/build-output root is no longer inventoried, so tracked scratch tools there are not discovered as apps (out/scratch/ildumpis untracked; the self-migration corpus is 56 apps). -
cs2gs no longer asserts a nullable argument forwarded to a generic-method type parameter (issue #4843, parent #3501). A promoted
object?parameter forwarded toDecodeSignature(provider, genericContext)was bridged with a runtime!!and threw a NullReferenceException in the migrated Core.Tests public-API snapshot test; gsc infers the method type argument from the argument, so the forward is now emitted unasserted. -
cs2gs no longer emits
__yielded{N}temporaries foryield return(issue #4859, parent #3501). The #4705 fix hoisted most yielded values (array creations, string literals, calls) into synthetic locals. Only non-tuple values whose printed form starts with((whichyieldat statement position does not parse as a plain expression, #4705), sized array creations (printed[3]T, whichyield [would parse as indexing), and branching expressions and lambdas (which need the iterator element type as their target), are materialized now, and that local is nameditem(item_2, ... when taken) instead of__yielded{N}. The__yieldedfamily has a retired row in the self-migration counter table. -
cs2gs preserves nullable tuple values stored through generic indexers (issue #4833, parent #3501). Assigning a tuple containing an oblivious nullable return into a dictionary-style value slot now promotes the matching tuple element on the receiver declaration, avoiding a later GS0155 when an absent value is represented by
nil. -
cs2gs preserves nullable elements in bare array initializers (issue #4831, parent #3501). Initializers such as
object[] arguments = { null };now use the same nullable-element projection as explicit and implicit array creations, so migrated code emits[]object?{nil}instead of failing with GS0155. -
cs2gs preserves implicit value conversions on switch-expression arms (issue #4832, parent #3501). Arms now use their Roslyn-converted non-nullable value type, including user-defined conversions such as
TypeDefinitionHandletoEntityHandle, so migrated switches retain one result type instead of failing with GS0179. Existing numeric widening and reference-arm projection are unchanged. -
Self-migrated Core now has exact native public ABI parity (issue #4828, parent #3501). Record synthesis matches Roslyn constructor, deconstruction, parameter-name and override-finality metadata; ordinary overrides omit redundant
finalflags on sealed owners; and preserved owned extensions keep only their native static-holder MethodDef while translated receiver calls route through it. The strict #4654 renderer remains unchanged. -
Explicit abstract properties retain storage-free CLR contracts (issues #4765 and #4715, ADR-0197).
abstract propandabstract override proppreserve getter/setter/init requirements, accessor visibility and virtual slots; concrete descendants must implement every required accessor. Abstract indexer accessor parameters retain optional/default metadata in implementation and reference assemblies, so CLR callers can omit optional indices. cs2gs propagates C# abstractness explicitly across ordinary classes and positional, partial and generic records, while initialized get/init overrides retain their backing storage and init accessor. Existing concrete open auto-properties and getter-only contracts are unchanged. -
cs2gs preserves nullable inferred locals inside lambdas (issue #4826). Local nullability scans now stay in the declaring lambda block, so a
Record.Exceptionresult observed byAssert.Nullremains nullable instead of gaining a checked!!assertion. -
cs2gs distinguishes stated-nullable metadata from platform types in oblivious projects (issue #4816). Explicit locals that store imported or source-promoted maybe-null values now retain
T?, while ordinary oblivious reads keep gsc'sT!coercion. Anonymous contracts also converge nested array, tuple, generic and delegate nullability without widening proven non-null storage. -
Copies retain construction provenance (issues #4776 and #4796). Data-struct
withand.copy(...)copy existing storage without rerunning primary constructors or declaration initializers. Expression-tree source data-class copies use the exact virtual clone before ordered updates, retaining private/base state and invocation-time, once-only evaluation rather than constructing a fresh object.- The emitted-PE baseline now records the intentional storage-copy IL in
DataStructErgonomicsandWebsiteSwift; real gsc/native C# oracles pin fresh-construction counts, strict IL, output and original/copy state. The other 158 sample entries and hash policy are unchanged.
- The emitted-PE baseline now records the intentional storage-copy IL in
-
cs2gs converges linked-source nullability across repository projects (issue #4817). Shared declarations are remapped by identical source path and content before assembly identity, so null evidence from any linking project produces one order-independent signature. The linked-output consistency check remains strict.
-
Imported G# sealed hierarchies retain their declaring-assembly restriction (issue #4807, PR #4808). Class semantics metadata now records inheritance mode, so foreign plain/data subclasses cannot treat a sealed hierarchy's intentionally non-sealed CLR image as open, including closed and symbolic generics and reference assemblies. Native non-sealed records and same-compilation sealed hierarchies remain legal. Legacy marked G# producers must be rebuilt to establish external inheritance permission.
-
cs2gs self-hosting guards remapped type arguments before storing them (issue #4722). Failed sibling remapping returns before publishing a nullable result into a fixed non-null Roslyn type-argument array; constructed and nested imported contract annotations remain intact.
-
Data-class
withand.copy(...)use the existing clone contract (issue #4796). Generic classes with a primary/base constructor no longer invoke a nonexistent parameterless constructor. Copies retain their constructed runtime type, source-base and private/readonly state, without rerunning field or base initializers; updates execute once in written order. -
cs2gs keeps nil-observing reference casts and distinct constructed tuple contracts (issue #4722). Built-in reference casts preserve nullable operator results until a coalescing fallback, while user-defined conversions, unboxing and non-null operator inputs retain their checks. Fixed-contract identity and sibling remapping preserve nested type-argument annotations, so an enabled contract cannot freeze a different oblivious implementation. Reporting fixtures use collectible, byte-loaded native assemblies that remain deletable on assertion failures.
-
cs2gs reports converted generic-store checks independently of asserted inputs (issue #4722). An authored
value!still reports its own fail-fast input check; a nullable user-defined conversion result checked before a fixed generic store now has a separateCS2GS-GENERIC-STORE-BRIDGEreport. Unchanged asserted values do not gain duplicate reports, and generated assertions and conversion evaluation are unchanged. -
Imported oblivious tuple contracts no longer freeze source implementations as non-null (issue #4719). Imported declarations remain immutable, but concrete oblivious reference leaves are nil-compatible platform endpoints, not non-null locks. Inherited eligibility and shared-component lock seeding use the same contract decision, preserving legitimate nullable operator results while retaining explicit native non-null contracts, operator-input checks and open type-parameter rules.
-
cs2gs tuple-leaf promotion respects each declaration's writable contract (issues #4719 and #4751). Explicitly non-null source leaves and fixed imported interface, base, delegate and store positions retain their non-null result bridges. Genuinely oblivious source-owned contracts still promote null-bearing leaves together, including
Task/ValueTaskresults, partial declarations, containing-type generic parameters and local nullable-context islands. Parenthesized tuple returns, yields and conditional or switch arms share this destination policy, and whole tuples supplied as native generic arguments retain their caller-owned leaf contracts; an unrelated tuple argument does not unlock a fixed native reference slot. Original member annotations and inherited contracts remain authoritative after generic owner projection, including both partial-property declarations and fixed signature components spanning sibling projects. Fixed locks retain declaration identity rather than freezing unrelated members that share a containing-type argument; writable seeds are filtered before projected taint is combined. Indexer parameters follow the same inherited and partial declaration contracts by ordinal. Sibling remapping preserves assembly identity, overload signatures and constructed owner and method arguments; local-only queries reuse the computed fixed-contract closure. Getter queries use their owning property or indexer contract. Operator inputs and converted results remain separate boundaries; an intermediate result obeys its immediate conversion consumer's parameter contract before the final result is checked. Null-accepting inputs are not asserted merely because the result has a non-null destination, and nil-observing branch results reach their coalescing fallback without an early assertion, including through checked and unchecked wrappers. Nullable branch destinations retain their selected implicit operator instead of lowering it to a safe reference cast. Guarded scalar iterator operands likewise preserve a nullable operator result rather than asserting it as non-null. -
cs2gs transports retained build-generated compilation inputs across validation roots and configurations (issue #4801, ADR-0196). Validation manifests carry the actual producing source text, hash and project ownership for non-inventoried build inputs, including net10 and netstandard2.0 SDK declarations. Authored sources still require the clean authoritative corpus, and source-declared test counts and budgets are unchanged. Old manifests lacking generated evidence fail closed rather than inventing historical contents.
-
Declaration initializer provenance now survives lowering, so struct literal initializers run once without suppressing authored overrides. Managed-reference safety also checks initializer functions separately for each constructed owner.
-
Expression-tree brace literals retain initializer-only constructor semantics (issue #4755). Owning value-struct initializers select their exact hidden marker signature rather than an authored parameterless constructor; later explicit member writes remain ordered after declaration initialization. Authored constructor calls retain their original body and metadata.
-
Source data-class copies use their existing virtual clone slot (issue #4796).
withpreserves get-only and private readonly storage, declaration provenance and constructed-owner identity without selecting or rerunning an authored constructor. Explicit updates retain lexical, once-only evaluation after the receiver is copied. -
Ordinary generic struct literals retain declared field initializers and primary-parameter scope (issues #4755 and #4747). Construction runs definition-bound initializers once in their owning type, using the actual primary inputs before later explicit member writes or collection population, instead of silently leaving initialized fields at CLR zero or crashing on a primary-parameter reference. Missing primary inputs use the existing required-storage validation and report GS0604 if their substituted type would default a non-null managed handle, including nested aggregates and fixed arrays. Constructed value/reference arguments, private fixed-array and readonly storage retain their owning metadata. Braces initialize independently of an authored constructor body, preserving normal authored calls and legal raw array/default zero-initialization. Authored-constructor declaration closures retain their owning storage and nested captures; duplicate primary/explicit constructor signatures report the existing GS0284 diagnostic rather than emitting ambiguous constructor metadata. Imported primary structs retain their existing CLR literal path and reconstructed collection defaults instead of requesting a same-compilation constructor.
- Explicit authored constructors also validate declaration defaults in their constructed generic owner, reporting GS0604 before emitting a null non-null managed-reference slot; constructor-body field assignments and legal scalar, reference and nullable defaults keep their existing policy.
- Expression-tree lambdas retain supplied primary inputs as invocation-time tree locals, preserving lexical evaluation order and once-only capture before constructor-owned declaration initialization, including reordered inputs and later member writes.
-
Data-class copy constructors initialize imported record bases correctly (issue #4789). The shared copy emitter now calls the accessible copy constructor on the actual direct CLR base, retaining symbolic generic arguments and base state before copying derived members. Object-root and source-base copying are unchanged; an imported non-object base without an accessible copy contract fails emission instead of producing invalid IL.
-
Native non-sealed record bases use CLR inheritance eligibility (issue #4807). Ordinary and closed-generic imported records now follow the same imported-base constructor and copy paths as symbolic generic records, instead of being rejected by G# source-only
openpolicy. Real CLR sealed bases and closed G# source bases remain rejected; imported equality dispatch is a separate concern. -
Inherited generic CLR method returns preserve source owner arguments (issue #4785). Generic calls through a source-derived receiver now project the return from the actual declaring owner's symbolic arguments, including multilevel, reordered and nested bases, while keeping the method's own argument order and physical tuple types.
-
Qualified nested constructor calls retain their generic enclosing owner (issue #4803). Calls such as
Outer[int32].User()now use the existing receiver-aware nested-type resolution instead of looking for a shared method namedUser, including inside expression-tree lambdas. Authored overload selection, enclosing/own parameter projection and constructor-reference generic slots share the existing construction machinery; no syntax or field-initializer semantics change. -
Qualified source-enum conversions retain their enclosing owner (self-migration #3501).
Outer.Kind(value)now uses the enum resolved by the shared nested-type reader instead of looking for a shared method namedKind. Attribute constants, generic and inherited owners, and once-only operand evaluation retain the existing conversion contract; unrelated same-named enums do not replace the nested type. -
Generic inference accepts a comparer implementing multiple closed interfaces when other arguments determine the key type (issue #4780). Symbolic and CLR inference no longer treat a non-unique interface projection as a contradictory bound or choose its first interface. Typed identity selectors now bind the self-migrated symbol-ordering tests without changing the comparer or generated assertions; unfixed type arguments, incompatible interfaces and violated constraints remain rejected.
-
Inherited generic CLR calls preserve source-typed tuple arguments (issue #4738). Argument conversion now projects through the inherited member's actual generic owner, including reordered base arguments, rather than constructing an erased
ValueTuple<object, …>for a source-typed slot. Nested and long tuples retain their physical element types and evaluation order; genuine object slots still use their intended conversions. -
cs2gs artifact replay preserves original source identity across checkout relocation (issue #4752). Validation manifests now export corpus-relative C# source and project metadata; legacy manifests recover only an unambiguous full-path repository mapping. Missing or invalid authoritative sources fail explicitly instead of silently reducing the source-declared Fact count to zero. Linked-source and referenced-project ownership, original per-name oracles and the existing parity budget policy are unchanged; this does not establish a runtime hang cause or test-suite completion.
-
cs2gs preserves null-observing local stores before
??fallbacks (issue #4770). The shared nullable-usage scan now recognizes coalescing, including parenthesized observations, so an intentionally null params-slot classification reaches its"not-reported"fallback instead of throwing at an inserted assertion. Defensive coalescing does not widen fixed parameter or member contracts; explicit non-null bridges and generic-store diagnostics retain their existing policy. -
cs2gs preserves reference-array covariance inside null coalescing (issue #4768). Derived/base array operands now use the shared explicit conversion path, with a nullable left cast that preserves nil until the fallback decision. The compiler accepts those nullable checked array casts, including nullable reference elements; array identity, result type and short-circuit evaluation are preserved, while G# slices remain implicitly invariant and incompatible value arrays or ranks remain rejected.
-
cs2gs preserves nested local-function state across recursive calls (issue #4802). Recursive callable groups now allocate storage in each function's declaring block, so an inner invocation cannot replace its caller's helper closures. Mutual sibling recursion and escaped callbacks retain their existing behavior; migrated recursive-delegate walks preserve argument order and once-only visits.
-
Recursive named delegates no longer crash the compiler's type queries or generic construction (issue #4757). Self-recursive, mutually recursive and expanding generic
Invokesignatures, including tuple/imported-type wrappers, retain nominal CLR identity; constructed signatures are projected on demand and type-parameter walks inspect actual arguments independently of the recursive definition graph without forcing unfinished signature projection. Ordered collection preserves signature order across sibling and nested constructions in their containing argument context, visiting each argument slot once per delegate walk instead of exponentially repeating finite nested arguments. -
cs2gs keeps attributes naming a safely lifted source delegate on the original extension owner (issue #4676). A private nested delegate already maps to a bindable top-level name, so an external-receiver extension no longer unnecessarily splits into an owner helper and a public
<Program>companion. The shared exposure check still follows the delegate's signature, constraints and containing generic arguments for genuine private types, and does not relax imported delegate accessibility or CLR identity. -
cs2gs preserves explicit static extension calls across source-project references (issue #4790). Referenced owned extensions reuse their declaring project's holder-preservation decision, including its suspension and receiver eligibility, instead of being mistaken for current-project lifted declarations. Explicit holder calls retain their native static target even when the argument's derived type has a same-named instance method; canonical receiver calls and task envelopes remain unchanged.
-
cs2gs retains native static extension-holder contracts for owned receivers (issue #4790). Canonical receiver members forward to the original static method hosted through
ExtensionOwner, rather than moving its body and deleting its declaring method. Explicit static calls and native method groups retain their original owner, signatures, defaults and constraints; private holder helpers remain private. Receiver-parameter attributes, including nullability postconditions and custom metadata, survive on the original holder. Forwarders preserverefandref readonlyreturns and their live aliases. Ordinary CLR async and iterator companions return the original envelope directly, preserving task identity, continuation behavior and lazy iteration without a second state machine. G# suspending declarations and companions instead share their logical-result suspension contract, with one compiler-owned suspension marker; a located unsupported diagnostic rejects an owned suspension shape that would add a Context parameter absent from its native holder signature. This scoped repair does not establish full Core ABI parity or self-hosting readiness. -
cs2gs preserves nullable selector results observed by LINQ predicates (issue #4769).
Any,All,WhereandFirstOrDefaulttype-pattern and nil tests now receive the original nullable value instead of an inserted!!throwing before the predicate runs. This repairs the shared recursive-lift query in the self-migrated translator; fixed non-null delegate contracts retain their checks. -
An inherited non-virtual property accessor can satisfy a G# interface across assemblies (issue #4717). The derived class now emits a private forwarding slot that calls the exact imported accessor selected by binding, including setters, init accessors, indexers and constructed generic bases. Previously the two compilations succeeded but the derived type failed IL verification and runtime loading. Ordinary base accessors stay non-virtual; existing virtual and inherited interface mappings are preserved, and reference assemblies carry the same forwarding-slot metadata.
-
Ordinary class methods now emit non-virtual CLR metadata as specified by ADR-0017 (issue #4677).
openmethods, overrides, and interface implementations retain their virtual slots, including members inherited by a descendant introducing an interface, synthesized sync/async iterators, and symbolic generic event accessors. Unrelated inherited members remain non-virtual. -
cs2gs preserves C# value and generic parameter metadata names (issue #4680, ADR-0170). Reserved names use
$namein declarations, references, and named arguments instead of the lossyname_rename, regardless of member visibility. Named-argument binding compares semantic names, so$scope:binds the CLR parameterscope; escaped spelling preserves argument slots, optional defaults, and lexical evaluation order. Documentation tags use the original unescaped names. Locals and other names without metadata retain collision-safe sanitization.
Reader's overview
The G# compiler is now written in G#. Starting with 0.5, gsc, the
language server, the formatter and the other G# tools are written in G#.
Nothing changes in how you install or use G#: the package ids, the
Gsharp.NET.Sdk project SDK, the dotnet new templates, and the VS Code and
Visual Studio extensions are the same.
Before moving an application to 0.5, pin the intended SDK, select its documentation, run the project's tests, and review the affected diagnostics.
For changes after the published snapshot, pay particular attention to stricter generic-constraint and nullable-argument diagnostics. Code previously accepted with an invalid CLR contract may now be rejected at compile time; the detailed entries below identify the diagnostic and remedy.
Before moving an application to a different compiler version, pin the intended SDK, select its documentation, run the project's tests, and review the affected diagnostics. Do not treat the development branch's implementation notes as a stable compatibility promise.
Breaking changes
-
Assembly version 0.5.0.0. The minor-version bump changes the
AssemblyVersionofGSharp.Coreand the other compiler assemblies to0.5.0.0. A G# analyzer built against 0.4'sGSharp.Corestill loads, but reportsGS9303(an analyzer was built against a differentGSharp.Coreversion than the host). Remedy: rebuild the analyzer against 0.5. -
Fail-fast
!!. The compiler is G# translated from C#, so places where C# trusted a value to be non-null (!) are now!!: a runtime check that throwsNullReferenceExceptionat the site instead of carrying null onward (issue #4612). Valid input should behave the same as 0.4.1270. If a 0.5 compiler, language server or tool reports an internal error (GS9998) with a null-reference trace on input that 0.4.1270 accepted, please file it with the input. -
data class/data structprint and compare like a C#record(ADR-0199).ToStringrenders the C# record format and equality follows the record ABI, which the self-migrated compiler needs for exact public ABI parity. See the "Breaking:data class/data structprint and compare like a C#record" entry above for the exact format. -
Inherited data equality compares the most-derived fields through base-typed calls and interfaces (issues #4777 and #4675). Compiler-owned typed-base overrides preserve structural dispatch without changing the nonvirtual base-field comparison or permitting user-authored
Equals. Direct imported record bases share that decision: self equality compares their fields nonvirtually, while native base/interface calls dispatch through the matching override with the actual symbolic generic owner. The existing imported data/record semantics detector gates this path; an ordinary CLR base's virtual domain-specificEquals(Self)remains inherited, without a synthesized component call or forwarding override. Optional native parameter-name metadata uses the standard positional fallback when absent, without changing signatures or nullability. cs2gs retains native records'IEquatable<Self>interfaces; class operators share the correct dispatch. Typed parameter nullability and generic/nested owner mappings agree in implementation and reference assemblies. Source calls and method groups retain both typed equality and the nullable object override. Ordinary and class-constrained calls to equality inherited from a constructed source generic base resolve the actual declaring owner, like method groups, without an internal compiler error. The emitted-PE sample hashes accept only the reviewed equality-slot metadata and class-operator changes; the byte-identical gate remains intact. Unrelated record ABI differences remain outside this repair. -
A compound member assignment through a receiver declared
T?now reportsGS0158(issue #4354).xs.Capacity += 1on aList[int32]?ande.InnerException.Source += "x"bound with no diagnostic and failed at run time with an unattributedNullReferenceException, while the plain writexs.Capacity = 1and the read were already reported. The compound form now follows them.- Remedy: narrow the receiver (
if let l = xs { l.Capacity += 1 }or a nil guard) or assert it (xs!!.Capacity += 1). A receiver that was never nilable is unaffected. - Event subscription through a
T?receiver (x.E += h) shares this binder and is reported the same way.
- Remedy: narrow the receiver (
-
Named attribute constructor arguments now bind to constructor slots instead of corrupting the custom-attribute blob (issue #4503).
@GeneratedRegex("a", RegexOptions.None, matchTimeoutMilliseconds: 1000)now selects and reorders the constructor argument correctly, including omitted optional defaults. Attribute property/field assignments useName = value; the existingName: valuemember spelling remains accepted when no constructor parameter has that name. Invalid, duplicate, and positional-plus-named duplicates now report diagnostics instead of emitting unreadable metadata. cs2gs preserves C#'s:constructor and=member distinction. -
Prefix
^xis now a first-classSystem.Indexexpression, and one's-complement is spelled~x(ADR-0187, issue #4350). This matches C#:let last = ^1is a reusableSystem.Index,consume(^2)passes one, and a standalone range may start from the end (let r = ^4..^1), so the formerGS0410restriction is retired. Binaryx ^ yis still bitwise XOR, and bracket indexing (xs[^1],xs[1..^1]) is unchanged. Remedy: rewrite prefix one's-complement from^xto~x, including user-declared unary operators (func (a T) operator ~() T). Parentheses do not keep the old meaning, because(^x)is also an Index. cs2gs now translates C#System.Index/System.Rangelocals, fields, parameters and expressions directly instead of reportingCS2GS-GAP, and it translates C#~to~. -
Calling an imported .NET method on a receiver declared
T?now reportsGS0159(issue #4287). Examples:s.ToUpper()on astring?field, local or parameter;l.Add(1)on aList[int32]?;Path.GetDirectoryName(p).ToUpper(). These used to compile with no diagnostic and throw a bareNullReferenceExceptionwhen the receiver was nil. A property or field read on the same receiver (s.Length) has reported since #4356, and a call to a G#-declared method always has. Calls were the remaining gap.- Remedy: assert the receiver (
s!!.ToUpper(), same result type), call it null-safely (s?.ToUpper(), whose result isstring?), or narrow it (if s != nil { … }orif let t = s { … }). - Not affected: a value-type
T?such asint32?.GetValueOrDefault(), whose members are safe on nil; an extension function declared over the nilable type; and an obliviousT!receiver, which still compiles and gets ADR-0186's attributed runtime check. - The check runs after the method is chosen, so it never changes which method a call selects.
- A nil test now narrows a bare field or property name used as a receiver (issue #4310). Inside a type,
if memo != nil && memo.TryGetValue(k, out var v)on alet memo Dictionary[K, V]?field now compiles, as thethis.memospelling already did. Before, the bare name kept itsT?type as a receiver even after the test: a call reached the gap above, and a property read (memo.Capacity) failed to bind. - cs2gs concatenates a nullable operand the way C# does.
"p:" + ofor a C#object?now translates to"p:" + o?.ToString(). A nil operand concatenates as the empty string, as in C#. The previouso.ToString()threw on nil and now reportsGS0159.
- Remedy: assert the receiver (
-
An
ifjoin no longer lifts a non-nil local across a possible jump into either branch (issue #4290). Agotothat lands after a branch's assignment could previously bypass the proof while the compiler still treated the local as non-nil after theif, producing a runtimeNullReferenceException. Functions containing a user label orgotonow conservatively skip this join narrowing. Remedy: add!!at the dereference, or restructure the control flow so no jump enters the branch after its assignment. -
A
gotono longer inherits a non-nil narrowing from an assignment its edge can bypass or invalidate (issue #4519). The compiler now joins assignment-narrowing state for forward and backward edges at each target label, so an unsafe dereference reportsGS0158instead of compiling and throwingNullReferenceException. Assignments before every incoming jump and assignments re-established after the label remain narrowed; unrelated labels do not disable narrowing for the whole function. -
A ref-returning static CLR method is now observed through its pointee (issue #4350, ADR-0056 §1).
MemoryMarshal.GetArrayDataReference(xs)orUnsafe.As[TFrom, TTo](&x)used to bind as the pointer type*T, unlike ref-returning instance members and indexers, which already auto-dereference. Passing such a result by value even emitted an address, which failed at run time withInvalidProgramException. Remedy: take&Call(...)where the address itself is needed. -
Nullability-oblivious imported reference positions are now read as platform types
T!rather than asT?(ADR-0186 step 3).--nullability=platform-typesis the new default;--nullability=enabledrestores ADR-0136's reading. A reference position arriving from an assembly that says nothing about its nullability — no[Nullable], no[NullableContext], or an explicit oblivious byte — used to be imported asT?, and every use then had to discharge a proof obligation (!!,if let,?., or a smart cast) about a fact nobody had ever stated. It is now the platform typeT!, usable as bothTandT?with no operator, and the safety question is deferred to the point where the value is actually used as non-null, where the compiler inserts an attributed runtime nil check. Annotated metadata — the whole modern BCL — is unaffected:[NullableContext(1)]members stay non-nullT, a[Nullable(2)]member staysT?, andSystem.Object.ToString()still returnsstring?; a direct use of those positions binds exactly as before, but a member chain through aT?link now asks for the same proof a source-declaredT?does (see the member-chain bullet below).- Code that previously errored now compiles.
Environment.Version.ToString(),s.ToUpper().Trim(),values.Min()andxs.Reverse()on an oblivious receiver all bind with no diagnostic.GS0159("receiver may be nil") is unchanged in definition but narrower in reach: it fires only for a source-declaredT?, an annotated-nullable imported member, a tuple element or anasresult, and can no longer fire for an oblivious CLR receiver.GS0129,GS0296,GS0298,GS0300,GS0503,GS0523andGS0536each gainT!on the "admits nil" side of their test, andstring! + "x"concatenates exactly asstring? + "x"does — as dostring! + 'c'ands += 'c'on astring!local (issue #4324). - Existing null-handling constructs keep compiling, unchanged.
x!!on aT!is legal, is not reported as redundant, and emits the same check the compiler would have inserted anyway;if let,?.,??andx != nilall accept aT!, and a nil guard narrows it toTso no check is emitted after the guard. Assertions that are no longer required are harmless; removing them is a separate, optional, mechanical pass. This is a statement about those constructs only, not about the release as a whole — see the container-conversion bullet below for the one shape that stops compiling, and the overload-resolution bullet for the one that compiles to something different. - A nil that used to be a compile error is now an attributed runtime check. Where a platform value flows into a destination whose declared type is a non-null reference type — an explicitly typed
let/var, a field or property store, an array or slice element store, anout/reftarget, an argument (including an extension method's receiver), areturn, athrowoperand, alocksubject, afor … insource, a deconstruction source, a delegate invocation target, or an instance-member receiver — the compiler inserts a check that throwsNullReferenceExceptionwith a message naming the expression, the oblivious origin and the file:line. The exception type is deliberately unchanged, so existingcatchclauses behave as they do today; what improves is the message.--platform-nil-checks=offsuppresses insertion for measurement only and is strictly weaker than either the old model or the new one — it is not a supported mode. - One documented exception to the attributed message: inside an expression-tree lambda. A lambda converted to
Expression[...]is handed toSystem.Linq.Expressionsas data and is never lowered to IL by the compiler, so there is nowhere for the check'sdup; brtrue; …; throwto live. Two consequences. A platform value used as a receiver there (.Where(b -> b.Conversion.AccountId == id)over an oblivious API) compiles and still fails on nil — measured:System.Linq.Expressionsemits the dereferencing form for instance member access regardless of virtuality, including for a non-virtual getter on a sealed class that never readsthis— but it fails with the runtime's own unattributedNullReferenceExceptionrather than the named message above, and a user-writtenx!!in receiver position is erased on the same path. Any other platform coercion inside such a lambda — an argument, a store, a return, or the lambda's own result — is a compile error (GS0473) rather than an unchecked value. Tracked as #4352. Enumerable.Min()/Count()on a nil oblivious source now throwNullReferenceExceptioninstead ofArgumentNullException. An extension receiver is an argument, so the check fires at the call site where the program made the error rather than several frames inside the library. Acatch ArgumentNullExceptionaround such a call no longer matches.xs.Reverse()on an obliviousList[int32]now binds a different method. Member lookup on aT!receiver runs againstT, so an own-surface instance member wins over an extension exactly as it does for a non-nilable receiver:List<T>.Reverse(in place, void) rather thanEnumerable.Reverse(lazy, copying). This is the fix for a silent miscompile in which the receiver's static nullability selected between two methods with different runtime semantics — but it is a behaviour change on the path that was previously miscompiling, andstring?.Trim()similarly stops retyping toReadOnlySpan<char>viaMemoryExtensions.- One overload-resolution shape can select a different method. Given a G#-declared pair
func G(s string)/func G(s string?), the callG(obliviousCall())used to pickG(string?)and now picksG(string): aT!argument is applicable to both and the more specificTparameter wins. This needs a source-declared overload pair differing only in nullability, which CLR metadata cannot express, so no imported API is affected. - Passing an oblivious generic container to a non-null-element parameter is now rejected, and this one is a genuine tightening.
C[T!],C[T]andC[T?]are three distinct constructed types, and the only implicit conversion between them isC[T!] → C[T?]. Solet xs List[string] = obliviousList()no longer compiles, where under ADR-0136 it did — not because ADR-0136 permitted the conversion in principle, but because this compiler admitsC[T?] → C[T]in both directions today (a separate, pre-existing soundness gap tracked as #4321). ADR-0186 §3 asserts this is "not a new cost"; on this compiler it is. Remedy: declare the destinationList[string?], or copy element-wise through theT! → Tboundary so each element is checked. Reading an element through the container is unaffected and needs no conversion at all —obliviousList()[0]isstring!and is checked at its own coercion point, which is where essentially all of the ergonomic benefit is. Note the tightening is present whether or not #4321 is fixed; fixing #4321 would additionally tighten source-declaredT?containers.- The same rule now applies when the destination is an imported .NET method or constructor parameter (issue #4480). Imported overloads are ranked on erased CLR shapes, but the winning argument is rebound against its nullability-aware parameter type:
List[string!]!no longer reachesIEnumerable[string]silently and reportsGS0154, matching a G# callee. A destination such asIEnumerable[string?]remains valid; in particular,String.Join(string, IEnumerable<string?>)still accepts the oblivious list. - Imported generic parameters now use their substituted nullability when checking arguments (issue #4440). This applies to instance, static, and extension calls:
List[string].Add(nil)andList[string]{"a", nil}now reportGS0155; nullable and oblivious slots continue to accept nil.
- The same rule now applies when the destination is an imported .NET method or constructor parameter (issue #4480). Imported overloads are ranked on erased CLR shapes, but the winning argument is rebound against its nullability-aware parameter type:
- An unannotated generic's type argument keeps the nullability you gave it (issue #4361). An open type-parameter slot is not an oblivious position — its nullability arrives with the type argument — so
Array.Empty[string]()from a netstandard2.0 or#nullable disableassembly is a platform array of plainstring([]!string), not ofstring!, and converts to[]?stringor[]stringjust as the annotated BCL's does; the same holds forEnumerable.Empty[T](),Array.FindAll(xs, …)withTinferred fromxs, and FsCheck'sArb.From(gen). Only the concrete container position staysT!. One consequence is a tightening: aniltuple element passed through such a method'sparams T[]against a non-null sibling (AnyEqual((nil, false), ("x", false))) is now rejected, exactly as the same call to a G#-declaredfunc AnyEqual[T](values ...T)always has been; give the sibling a nilable type ((string?, bool)) to widenT. Diagnostics now print a nested platform argument (List[string!]!) and an imported platform array in ADR-0132's positional spelling ([]!string). - Oblivious positions now round-trip through metadata. A platform position emits the oblivious byte
0, which re-imports asT!— never1, which would launder an unknown into a guarantee. Under--nullability=enabledthe same byte reads as ADR-0136'sT?, so assemblies stay readable by either mode. This is the third value of the round-trip guarantee, which ADR-0136 could not express at all. - A member chain through a stated-nullable imported member now reports, reads and writes alike (issue #4356). Member lookup used to let a chain continue through ANY imported field or property read typed
T?:e.InnerException.Message(Exception.InnerExceptionis declaredException?by the BCL), the writee.InnerException.Source = "src", orb.Value.Counton aBox[List[int32]?]whose plain genericTmember takes its?from the type argument. It did so with no narrowing and no nil check, so a nil link threw an unattributedNullReferenceException. That carve-out existed for oblivious members, which are nowT!and checked by the attributed runtime check above; what it still admitted was nullability someone had actually stated. It is gone. Such a chain now reportsGS0158on the member after theT?link, exactly as a source-declaredT?receiver always has. Remedy: assert the link (e.InnerException!!.Message, same result type), read it null-conditionally (e.InnerException?.Message, whose result isstring?), or narrow it (if let inner = e.InnerException { … }). The member's own type does not change: an annotated member is stillT?, and a direct read of it, or a read that flows into aT?destination, is unaffected. Under--nullability=enabledan oblivious member is ADR-0136'sT?again, so an oblivious chain such asEnvironment.Version.Majornow reports too and needs!!in that mode; the default mode reads it asT!and is unaffected. Code produced bycs2gsalready carries the needed!!or?.(#4365).
- Code that previously errored now compiles.
-
return thisBY VALUE from aref structinstance method is now legal, with or without@UnscopedRef(ADR-0184 D1, correcting ADR-0058). ADR-0058 rejected it withGS0219and claimed that matched C#'s default behaviour; it does not. C# givesthisa ref-safe-context that is function-local by default and a safe-to-escape (value) scope that is the CALLER's — the value the receiver holds was produced by, and outlives, the call. The two had been conflated onto oneIsScopedbit.HasFunctionLocalEscapeScopenow excludes the receiver parameter from itsIsScopedtest, and the ref-scope half moved to a dedicatedIsUnscopedRefReceiverflag read byHasFunctionLocalRefScope. A genuinelyscopedsource is unaffected: returning ascopedparameter, or a local seeded from one, is stillGS0219, inside a struct member exactly as at top level. Two existingRefSafeEscapeTestscases changed their expected outcome; one of them (RefStructMethod_WithUnscopedRef_ReturnsThis_IsLegal) had also been silently emittingGS0198for its whole life, because it wrote@UnscopedRefwith noimportand only assertedDoesNotContain(GS0219). -
A struct member that returns a reference to its own instance state without
@UnscopedRefnow reports the newGS0589(ADR-0184, the CS8170 analogue), instead ofGS0253's inaccurate "must be an lvalue". A reference rooted at a LOCAL still reportsGS0254, and a genuinely read-only (let) field still reportsGS0253whether the member is annotated or not —@UnscopedReflifts the escape-scope restriction only. -
@UnscopedRefon a declaration where it means nothing is rejected withGS0590(ADR-0184), rather than silently ignored: a class member, ashared(static) member or property, a receiver-clause function (ADR-0182 makes every receiver clause an extension, whose receiver is an ordinary by-value parameter), or a free function. Interface members and matching override/interface implementations are now supported (issue #4292): an interface implementation may add@UnscopedRefonly when the matched slot carries it, including ordinary by-value methods and property accessors, while overrides apply C#'s narrower ref-safety relevance gate. An implementation may keep its receiver scoped by omitting the annotation from an annotated slot. The check follows the implementation selected by override/interface dispatch: an explicit implementation must match the exact slot and constructed interface owner, while implicit satisfaction ignores every explicit-interface member. This closes the previous placement-diagnostic gap without lettingIBase[A]satisfyIBase[B]or an unrelated explicit implementation mask a default body. Default interface bodies keep a missing implementation optional, but an implementation that replaces one is checked by the same rule, including inherited default properties. Auto-property setters/init accessors contribute their implicitvalueparameter before accessor symbols exist, and importedScopedRefAttributemetadata is recognized by runtime assembly identity so a cross-assembly lookalike cannot change this safety decision. -
Parameter ref escapes now use C#'s implicit scopes (issue #4293, ADR-0184 amendment). Returning an ordinary
outparameter by reference now reportsGS0254, and anoutargument no longer constrains a ref-returning call.refandinparameters use the return-only ref scope, while arefparameter of ref-struct type has a separate function-local value scope and reportsGS0219if returned by value.@UnscopedRefwidens the applicable defaults, including the ref-struct value scope; it is rejected withGS0590on by-value and explicitlyscopedparameters. Constructed generic declarations preserve this contract through substitution. Imported modules opt into the implicit defaults withRefSafetyRulesAttribute(11); markerless modules retain legacy caller-scoped by-reference parameters, and explicitScopedRef/UnscopedRefmetadata remains authoritative. G# does not yet emitRefSafetyRulesAttribute, so the release does not claim equivalent C# consumer behavior or Roslyn's full ref-struct value-mixing analysis. -
Returning a reference obtained through a READ-ONLY RECEIVER is now rejected with the new
GS0591, closing a dangling-reference hole in the@UnscopedRefwork above (ADR-0184 amendment, the CS8156 analogue; found in adversarial review of the definition-side change and proven at run time before the fix).func fromIn(in a Acc) ref readonly int32 { return ref a.Slot() }— whereSlotis a legal@UnscopedRefmember returning a reference intoAcc's own instance state — compiled with ZERO diagnostics and returned a dangling reference. Stored value 7; read back after one intervening call, across two builds of the same source:-1048597222,-1083017767,-1040415262. Three different values, so this was genuinely reused stack memory, not a stale-but-stable copy. Realcscrejects the C# analogue withCS8156. Root cause: the emitter and the binder disagreed about what the emitter emits. The emitter defensively COPIES a value-type receiver into a function-local temp whenever the receiver is a read-only reference — aninparameter, aref readonlyalias, or aref readonlycall/property result — and the called member is not itself areadonlymember, which (G# has noreadonly func) is every native G# member. Three sites encode that rule:MethodBodyEmitter.EmitInstanceReceiver,MethodBodyEmitter.EmitConstrainedTypeParameterReceiver(which has noisReadOnlyCallopt-out at all, since aconstrained.-prefixed receiver is addressed through a temp regardless), andReflectionMetadataEmitter.NeedsRvalueReceiverSpill.StatementBinder.HasFunctionLocalRefScopemodelled NONE of them — it gave such a receiver caller scope, correct for the storage the author wrote and wrong for the temp the emitter substituted underneath. The definition-side change did not create the copy; it created the first way to observe it, by makingreturn ref this.<field>expressible. The rule now lives in exactly one place,RefCapabilities.RequiresReadOnlyReceiverDefensiveCopy, consulted by all three emitter sites (a behaviour-preserving extraction — each site's current condition is reproduced exactly, the constrained-receiver site via the default argument) and by the binder's newIsDefensivelyCopiedReceiverForwarding.GS0591is reported rather than the genericGS0254for the same reasonGS0589got its own identity: the defensive copy has no spelling in the source, soGS0254's "function-local storage" would point the author at storage they never wrote, whereasGS0591names the copy and the remedy (take the receiver byrefrather thanin, or return the value). The[UnscopedRef]CLR-indexer variant is closed by the same hook: issue #4265 rejected forwarding such an indexer through a BY-VALUE parameter, but thein-parameter spelling of the same forward was still accepted and dangled identically — an independently pre-existing bug, not a regression from this ADR. TheSpan[T]shape is deliberately NOT affected:func first(in s Span[int32]) ref readonly int32 { return ref s[0] }stays legal, because aSpan[T]/ReadOnlySpan[T]indexer returns a reference into the ENCAPSULATED BUFFER the value merely wraps — copying the receiver copies its pointer and length and leaves the pointee untouched — so #4265's premise still holds for every indexer that is not explicitly[UnscopedRef]. One consequence is correct but non-obvious, and is pinned by a test on purpose:func k(in a Acc, ref y int32) ref readonly int32 { return ref a.Pick(ref y) }is rejected even though the reference ultimately comes fromy, the caller's own storage. This is NOT theCS8156case — realcscACCEPTS the C# analogue (verified directly, in the same file where it rejects theSlot()shape above), because C# gives a non-[UnscopedRef]struct method'sthisascoped refref-safe-context and excludes the receiver from the result's scope entirely. G# rejects it because G#'s escape walk includes a call's receiver unconditionally,@UnscopedRefor not — pre-existing behaviour, not introduced here: the by-VALUE receiver spelling of the same shape already reportedGS0254before this change. The defensive copy makes aninreceiver function-local too, so theinspelling now joins it consistently. G# is therefore strictly MORE conservative than C# on this point, never less; narrowing it to match C#'s precision is its own separate change, recorded in the ADR under "What it does NOT address". The safe path is unchanged and verified at run time: aref(rather thanin) receiver is passed by address with no copy, and mutating through the reference the member hands back is still observed on the caller's own storage. -
A closure that captures a
ref/out/inPARAMETER of its enclosing function is now rejected withGS9010instead of silently compiling a lossy by-value snapshot (issue #4259, found auditing #4221 for DoD coverage).LambdaBinder's capture-legality checks already rejected aref structcapture (GS0219), a managed-pointer capture (GS9004), and afixed-pointer capture (GS9008), but had no check for a captured variable that is itself a by-ref PARAMETER —func Foo(ref x int32) { let Bad = func(a int32) int32 { x = x + 1; return a } }compiled clean and silently dropped the mutation (the closure's field held a snapshot ofx's value, not a live reference to the caller's argument), since neither prior check inspectsParameterSymbol.RefKindand a plain parameter's ownTypecarries no by-ref marker. The check is scoped toParameterSymbolinstances withRefKind.Ref/Out/Inspecifically, so it does not affect aref/var refLOCAL alias capture — a distinct, plain-LocalVariableSymbolcapture kind with its own (separately handled, and per #4222 sometimes legal) rules — nor a struct instance method's implicitthis(constructed withRefKind.None, verified with a struct-method closure repro). -
A closure that captures a
ref/var refLOCAL alias of its enclosing function is now rejected withGS9011instead of silently compiling a lossy by-value snapshot (issue #4271, the sibling gap #4259/GS9010deliberately left open — a ref-alias local is a plainLocalVariableSymbol, not aParameterSymbol).func Foo() { var arr = []int32{1}; var ref alias = arr[0]; var f = func() { alias = alias + 1 }; f(); Console.WriteLine(arr[0]) }compiled clean and printed1instead of2— the alias's slot holds a managed pointer, andCaptureBoxingRewriter.IsBoxablerefuses to hoist it into the shared box that gives an ordinary captured local its write-through semantics (boxing would corrupt the aliasing), so the capture silently fell back to a snapshot. Rather than build a live-reference capture path, this is rejected outright, mirroring C#'s CS8175 for the same shape (a closure may outlive the storage the alias points at — an array element, a field, another stack frame's local — and the CLR has no mechanism to keep a managed pointer valid past that). Distinct from issue #4222's per-suspension-point liveness check (GS0258): an alias declared and fully consumed within one async/iterator suspension segment, never captured by a nested closure, is unaffected — that is a different code path (RefStructAsyncLivenessAnalyzer, notLambdaBinder's capture-legality checks). The whole.gscorpus was swept: 0 files usevar ref/let refat all, so 0 files affected. -
A G#-declared generic INTERFACE must now forward its constraints too, and a CLOSED violating type argument at any G#-declared generic type clause is now refused (issues #4089 and #4090).
class Holder[T] : IGsBox[T], whereinterface IGsBox[TI SchemeOptions]is declared in the same compilation, compiled with no diagnostic and its IL VERIFIED — an interface-implementation row is not something ILVerify checks — while the CLR refused the instantiation at load time withTypeLoadException: GenericArguments[0], 'T', ... violates the constraint of type parameter 'TI'. It now reportsGS0580, exactly as the class spelling has since #4067, andclass Holder[T SchemeOptions] : IGsBox[T]keeps binding. Separately,class Bad : GsHandler[Unrelated]andvar f GsHandler[Unrelated]overopen class GsHandler[TOptions SchemeOptions]compiled and emitted IL that does not verify ([UnsatisfiedMethodParentInst] ... Method parent instantiation has unsatisfied class type parameter constraints); the constructor spellingGsHandler[Unrelated]()has always reportedGS0152, and the TYPE-clause spelling now reports the same thing, at a base clause, a field type, a variable type, a return type, a nested argument, a qualified name and a method body alike. No new diagnostic id was taken — this is C#'s CS0311, which G# already spellsGS0152, and CS0314, which it already spellsGS0580. -
Both of the above were one defect, and the fix moved WHEN the question is asked rather than adding call sites. A declaration's type-parameter constraints are resolved when its own body (a class) or its members (an interface) are bound — the aggregate-shell lifecycle of #2519, which exists so a bound may name any same-compilation type and so CRTP keeps working. A type clause in another declaration can bind first, and then the declared parameter carries no constraints at all and every checker accepts. Interfaces bind their members after every class body, which is why a G#-declared generic interface was never checked; classes are ordered base-first, which is why #4067's base-clause repro happened to work. A field type has no such ordering, so
class Unf[T] { public var f GsLate[T] }was silently accepted wheneverGsLatewas declared BELOW it and reportedGS0580when it was declared above — measured both ways, and a third witness neither issue named. Every G#-declared generic type-clause construction is now queued during the declaration phase and answered once every same-compilation constraint is resolved; constructions bound later (method bodies) are answered in place as before. The whole.gscorpus was swept with the fixed build: 197 files, 170 emitting an assembly, 0 occurrences ofGS0152and 0 ofGS0580. -
A generic declaration must now forward the constraints a G#-DECLARED generic base requires too (issue #4067). #4037 closed this for an IMPORTED base; the same shape over a base declared in the same compilation is a different code path and was still accepted.
class Unf[T] : GsHandler[T], whereopen class GsHandler[TOptions SchemeOptions]is declared in the same file, compiled with no diagnostic and emitted IL that ILVerify rejects with[UnsatisfiedMethodParentInst] … Method parent instantiation has unsatisfied class type parameter constraints. It now reports the sameGS0580, with the same remedy — forward the constraint,class Fwd[T SchemeOptions] : GsHandler[T], which verified before and still does. The rule is identical; only the substrate differs. A G#-declared generic base is closed by symbol substitution in the type-clause binder and never byType.MakeGenericType, so #4037's checker never saw it, and the implication test is written against the symbolic constraint surface instead. Two positions are still not asked about and each is a fixture row rather than a sentence: a composite open argument (GsList[[]T]) and a bound that mentions another of the definition's own parameters. A third, the G#-declared generic INTERFACE, was left open here because its own parameters have not resolved their constraints yet when a class body binds (the CRTP shell lifecycle of #2519); it was filed as #4089 together with the closed-argument gap at the same site as #4090, and both have since been closed — see the entries above. The whole.gscorpus was swept: 0 files affected. -
A bare imported dependent constraint (
where TDerived : TBase) is now checked over erased same-compilation classes (issue #4063). #4041 taught the imported constraint check to answer a dependent bound on the symbolic vector, but with INVARIANT IDENTITY — right at a generic argument position, wrong at the top of a bound, where the relation is assignability. Rather than answer with the wrong relation it declined there, andvar c Chain[ChA, ChB]over two unrelated same-compilation classes compiled and threwTypeLoadExceptionnamingTDerivedthe first time the program touched the type. That position now asks the symbols for assignability, through the very predicate the G#-declared spelling of the same bound has used since #4043, so the two spellings cannot drift apart. It reportsGS0152naming the bound the author wrote —TDeriveddoes not satisfy theTBaseconstraint — rather than theSystem.Objectthe erasure substitutes into it.Chain[ChBase, ChDerived],Chain[IFoo, ChImpl],Chain[object, string]andChain[object, int32]all keep binding; an indeterminate answer still falls through to the pre-existing comparison rather than to a rejection. The whole.gscorpus was swept: 0 files affected. -
A generic declaration must now FORWARD the constraints its imported generic base requires (issue #4037).
class Unforwarded[T] : Handler[T], where the importedHandler<TOptions>declareswhere TOptions : SchemeOptions, compiled with no diagnostic and emitted anextendsrow that names an instantiation which is not valid for everyT; ILVerify rejected it with[UnsatisfiedMethodParentInst] … Method parent instantiation has unsatisfied class type parameter constraints, and the type only became unloadable if something ever closed it. It now reports the newGS0580, which names the type parameter, the bound it does not carry, and the remedy — forward the constraint,class Forwarded[T SchemeOptions] : Handler[T], which verified before and still does. This is a different question fromGS0152(#4032), which asks whether a CLOSED type argument satisfies a bound; here there is no argument, so the rule is whether the declaration's own constraint set implies the base's. C# spells this CS0314 — measured againstcsc, not CS0311 as the issue reported, because CS0311 is the concrete-argument case G# already spellsGS0152. The same measurement set the scope:cscreports it at a base clause, a field type, a local declaration, an interface list entry and a return type alike, so the rule sits at the shared generic-type construction sites and every spelling inherits it. A type argument that merely MENTIONS a type parameter (Handler[List[T]]) and a bound that mentions another of the definition's own parameters (where U : IList[T]) are both still skipped — the rule only adds rejections it can prove. The whole.gscorpus was swept: 0 files affected. -
G# formatting is now canonical and option-free (ADR-0179).
gsfmtand the language server always use 4-space indentation, K&R braces, a fixed 120-column width, and LF output; LSP tab-size and insert-spaces options are ignored. Range and on-type formatting are now available, and the VS Code extension enables format-on-save for.gsfiles by default. -
chan Tis respelledchan[T](ADR-0174 D2). The element type moves inside brackets likesequence[T]andmap[K, V];in chan[T]/out chan[T]are the receive-only / send-only handles (ChannelReader<T>/ChannelWriter<T>),chan[T]?is a nullable channel andchan[T?]a channel of nullable — the(chan T)?grouping carve-out is gone. The legacy spelling is rejected withGS0567, which names the exact replacement. -
make(chan T[, n])is retired in favour ofchan[T]()(rendezvous — capacity 0, Go's unbuffered channel),chan[T](n), andChan.Unbounded[T]()(the wave-1 behavior ofmake(chan T), now named).GS0566names the replacement per site; note the semantic change for the no-capacity form. -
close(ch)is retired in favour of the memberch.Close()(GS0566). Closing twice throws (Go's panic);Dispose()is the idempotent close, sousing letworks.len(ch)/cap(ch)becomech.Length()/ch.Capacityon a channel you constructed. -
Receiving from a closed channel no longer raises and swallows
ChannelClosedException— the zero value is delivered directly (about 400× faster on that path). -
import Gsharp.Extensions.Gois gone (ADR-0174 D13): the concurrency syntax is the language, the namespace and its marker type are deleted, and the gate diagnosticsGS0316/GS0317are retired. The concurrency library lives in the implicitly importedGsharp.Concurrencynamespace (/noimplicitimportsdisables it). -
len,cap,append, anddeleteare retired (ADR-0174 D13). A call reportsGS0566naming the member replacement for that site:xs.Length,m.Count,m.Remove(k),ch.Length()/ch.Capacity;appendhas no member spelling — a slice is a fixed CLR array, so keep a growableList[T]and.Add;capon a slice is removed outright (its capacity was its length). A user-defined function of the same name is an ordinary call. -
A nullable argument at a non-nullable G#-declared parameter is now an error even when the parameter's type is still open (issue #3988). Passing a
chan[T]?to achan[T]parameter, a([]T)?to a[]Tone, or aT?to a reference-constrained[T class]one used to compile with no diagnostic at all, while every closed spelling of the same call reportedGS0154. It was not only a missing message: anilreached the non-nullable slot and failed at run time. All of these now reportGS0154, which names the parameter and both types; the remedies are!!at the call site, declaring the parameter nullable, or narrowing the value first — either an ordinary!= nilcheck or an assignment from a non-nullable value, both of which now lift these shapes (assignment narrowing did not, for a slice, array or channel whose element was still open). An UNCONSTRAINEDT?is deliberately unaffected — it erases toNullable<T>and keeps the value-type rules. -
A nullable
map,sequenceorasync sequenceat a non-nullable parameter is now an error, in every spelling (issue #3997). Two holes closed together, because closing either alone would have left the two spellings disagreeing the other way round. First,map[K, V]?,(sequence[T])?and(async sequence[T])?at a non-nullable parameter of their own type compiled with no diagnostic at all whenever the element was still an open type parameter — the shapes were missing from the list of "reference types whose CLR backing is unavailable while binding" that #3985 and #3988 filled in for channels, slices and arrays, so the null-safety gate declined to gate them; anilreached the non-nullable slot and threwNullReferenceExceptionat run time. Second, and contrary to the issue's own diagnosis, the CLOSED sequence spellings were not being rejected either:(sequence[int32])?at asequence[int32]parameter, and a plain importedIEnumerable[int32]?at anIEnumerable[int32]parameter, were both classified as an IDENTITY conversion by thesequence[T]≡IEnumerable[T]alias rule (issue #3093), which compared the two sides ignoring reference nullability. The enumerable shapes were the only reference types in the language exempt from Kotlin-model null safety. All of these now reportGS0154(orGS0155at an initializer), naming the parameter and both types; the remedies are the usual ones —!!, a nullable-typed parameter, or narrowing with!= nilor an assignment from a non-nullable value. Note the migration this implies for unannotated imported libraries: anIEnumerable<T>returned by a C# assembly with no nullable annotations arrives asIEnumerable[T]?(see Unannotated imported reference types are nullable by default, 0.3), so it now needs!!or a narrowing check at a non-nullablesequence[T]orIEnumerable[T]parameter — exactly as that same library'sstringreturn already does at astringparameter. The alias itself is untouched in the directions that drop nothing:sequence[T]andIEnumerable[T]still convert to one another as identity, and both still reach anIEnumerable[T]?slot, so LINQ flowing into asequence[T]parameter is unaffected. Measured blast radius across all 192.gsfiles in the repository and the SDK's own G# sources: zero. -
A bare
nilliteral at a non-null-tolerant parameter is now an error even when the parameter's type is still open (issue #4010). Non-null-tolerant means a slot whose CLOSED spelling already rejectsnil— every shape except a G#-declared class or interface, a reference-constrained[T class], and any?-annotated type, all of which keep accepting it. This is the other half of #3988, which taught the same clause to look at a nullable annotation: anilLITERAL has no annotation to inspect, so thenilargument intakesChan[T](nil),takesSlice[T](nil),takesMap[K, V](nil)ortakesSeq[T](nil)was still passed through unchecked whenever the substituted parameter type mentioned a type parameter — while every closed spelling of the same call reportedGS0154. It was unsound, not merely silent: forwardingtakesMap[K, V](nil)out of a generic and instantiating it at[string, int32]compiled, IL-verified, and threwNullReferenceExceptionwhen the callee readm.Count. All of these now reportGS0154. Three shapes the issue did not name change with them, each of which its closed sibling already rejected: an UNCONSTRAINEDTparameter, a fixed array[N]T, and a function type(T) -> T.nilstill reaches every null-tolerant open shape — a reference-constrained[T class], aT?, achan[T]?, a G#-declared open class or interface — and the remedies are the usual ones: annotate the parameter, or pass a real value. Measured blast radius across all 180.gsfiles undersamples/,src/Sdk/,bench/,e2etests/andtest-assets/: zero. -
Two constructions of one generic type no longer structurally project to each other (issue #4014).
let zs List[string] = xsfor anxs List[int32]used to compile, IL-verify, run, and print0— the ADR-0148 projection planner answered yes becauseList<int>andList<string>share a parameterless constructor and a settableCapacity, so the "conversion" built a NEW, EMPTYList<string>, copied a capacity, and dropped every element. The argument position of the same pair behaved identically. A projection between two constructions of one generic definition is now refused (GS0490at an assignment,GS0154at an argument), because the state the type argument names is exactly what a member-by-member copy cannot carry. The rule is scoped to CLR-backed pairs whose CLOSED types differ, so identity is untouched, a same-compilation G# generic is untouched — a userclass Box[T]still projectsBox[int32]toBox[int64]and carries the value, because a G# class's public fields ARE its state — and an ordinary projection into a generic CLR target from a DIFFERENT source type still works. The refusal is narrow in a second way: it applies only where the two constructions' public member SURFACES are type-identical, so the difference lives in state the author cannot name. Where the surface does carry it — an importedBox[T]whose settableValueisT— nothing changes, and ADR-0148 §B explicit object spread can still supply it (Box[string]{ ...boxOfInt, Value: "replacement" }still compiles); a spread that does NOT supply it still reports the sameGS0490naming the member. One pair that was already rejected changes diagnostic id: this pair at an IMPORTED CLR parameter moves fromGS0155toGS0159, because the candidate is now never applicable rather than applicable-then-unconvertible. Measured blast radius across all 180.gsfiles undersamples/,src/Sdk/,bench/,e2etests/andtest-assets/: zero. -
A generic instantiated over a fixed-length array is a distinct type per length (issue #3962).
List[[3]int32]andList[[4]int32]used to be the SAME type and converted to one another silently:[N]Tis backed by the plain CLRT[], so both spellings resolved to oneList<System.Int32[]>symbol before any conversion ran. The declared length is now kept beside the erased CLR shape, exactly as a same-compilation element or a named tuple already was, so the two reportGS0155/GS0154naming both lengths. This holds through a variant interface (IEnumerable[[3]int32]does not convert toIEnumerable[[4]int32]despiteIEnumerable's covariance, andIComparerlikewise contravariantly) and for a static generic receiver (EqualityComparer[[3]int32].Defaultis not anEqualityComparer[[4]int32]). A length that MATCHES is still one type, a covariant widening over the same length still widens, and an array recovered from METADATA still converts either way — a G#List[[3]int32]reaches a C#List<int[]>parameter, since reflection records no length to disagree with. The SLICE half of the same erasure is closed by #4024, below. -
A fixed-length array's declared length is part of its type (issue #3998). This is the bare rule underneath #3962, and it was wrong in both directions:
[3]int32converted implicitly to[4]int32,[]int32converted implicitly to[3]int32, andvar lit [3]int32 = [2]int32{1, 2}was accepted withlit.Lengthreporting 2.[N]Tis backed by the plain CLRT[]— the same type as[]Tand as every other length — so these pairs fell through to the general CLR reference-assignability rule; the #2516 invariance guard that stops array covariance fired only for a slice or imported-array SOURCE. The rule is now stated as a widening:[N]Tconverts implicitly to[]T(dropping a known length for an unknown one is a representation no-op, so a fixed array still reaches every[]Tslot), and nothing converts implicitly INTO a[N]Texcept a[N]Tof the same length. Rejected pairs reportGS0156(orGS0154at a call argument) and keep their explicitcast[…], exactly as slice covariance does; the cast reinterprets rather than converts, so a[3]int32cast to[4]int32still reports.Length3. An array recovered from METADATA is exempt in both directions — a G#[3]int32still reaches a C#int[]parameter and a C#int[]return still fills a[3]int32slot. The same change also closes two gaps it found.[2]Footo[]Foofor a same-compilationFooused to be REJECTED (GS0154/GS0156), because the widening path existed only for a CLR-backed element; it now widens like every other fixed array. And a covariant element used to reportGS0155— no conversion at all, socast[[]Base](d)did not compile either — and now reportsGS0156with a working cast, matching what[]Derived->[]Basehas had since #2516; element invariance for implicit conversions is unchanged. Zero of the 175.gsfiles undersamples/,src/Sdk/,bench/,e2etests/andtest-assets/relied on any rejected conversion. The member-slot gap this left is closed by #4012, below. -
An explicitly-implemented interface member is no longer a candidate for an ordinary call (issue #4013).
List[int32]().Add("x")COMPILED and threwArgumentExceptionat run time:List<T>implements the non-genericSystem.Collections.IList, whoseAdd(object)accepts anything, and the member-lookup walk that exists to surface DEFAULT interface methods admitted abstract interface members too — so when the argument did not fit the genericAdd(T), resolution fell through to the explicit implementation and silently widened the call site toobject. In C# such a member is not part of the implementing type's surface and is reachable only through an interface-typed reference; it now behaves the same way in G#. A class receiver keeps only genuine DEFAULT interface methods from that walk (they are non-abstract, and the class need not implement them at all); an INTERFACE receiver is unaffected, since a base interface's members really are members of the derived one, and an IMPLICITLY implemented member is on the class surface and still binds. Reaching the explicit member still works through an interface-typed receiver —cast[IList](ys).Add("x"). An inapplicable argument now reportsGS0159, which is what an imported overload with no interface sibling (Stack[int32]().Push("x")) already reported; where the explicit member was the ONLY candidate of that name the call used to compile and now reports the newGS0577, which names the member, the interface that declares it and how to reach it, rather than reading like a misspelling —map[string, int32]{}.Contains(k)is that shape, sinceDictionary<K, V>implementsContainsonly explicitly. Measured blast radius over every.gsfile undersamples/ande2etests/: 27 such members were being offered, every one of them alongside another candidate of the same name on the type's own surface, and the corpus still compiles unchanged. Two call shapes are deliberately NOT narrowed, because the compiler still depends on the member there, and both keep the hole: an ADR-0117 collection literal (and the #3096 spread form), which fills aDictionary[K, V]through the explicitICollection<KeyValuePair<K, V>>.Add; and a call whose argument carries a same-compilation type, whereIList.Add(object)is an erasure escape hatch wherever two erasures of one type disagree (List[System.Action[Mode]].Add((item Mode) -> …)for a same-compilation enum presents the receiver asList<Action<object>>and the lambda asAction<int>). Both turn on something the call site has rather than on the member, so the reported hole — a genuinestringat a genuineint32— is closed;List[int32]{"x"}and the same-compilation-argument form went on compiling and throwing, and are closed by #4028 below. -
A generic's member slot over a fixed-length array follows the retained
[N]T, not the erased CLR shape (issue #4012). Two gaps, one family. First,List[[3]int32]().Add([4]int32{…})was accepted, withxs[0].Lengthreporting 4:List[[3]int32]keeps[3]int32in its type arguments (#3962), but the helper that rewrites an imported member's parameter through them admitted a type argument only when it needed symbolic projection — and a[3]int32has a perfectly realint32[]CLR type, which is exactly the problem, since that type is shared with[]int32and with every other length.Add's parameter therefore stayed theint32[]reflection reports off the erasedList<int32[]>. It is now the[3]int32the receiver carries, so a differing length reportsGS0156(and[]Fooat a[3]Fooslot does too), withcast[…]still available exactly as at the bare level.Stack[[3]int32]'sPushisolates the rule —List[T]also exposesIList.Add(object), which accepts anything (#4013). Second,List[[3]Foo]().Add([3]Foo{…})for a same-compilationFooreportedGS0159 "Cannot find function Add"and was simply unreachable: an argument with no CLR type of its own is ridden through to its erasure so overload resolution can rank candidates, and[]Tand[N,M]Thad that ride-through while[N]Tdid not — so resolution never ran at all. It now presents asobject[], exactly as[]Fooalready did; what a genuineobject[]parameter accepts is unchanged. The third sub-case of the issue —List[[]int32]converting toList[[3]int32], a slice type argument erased before any comparison can see it — is a different mechanism and is closed by #4024, below. -
A generic instantiated over a slice is a distinct type from one instantiated over a fixed-length array (issue #4024).
List[[]int32]converted toList[[3]int32], silently — the last open sub-case of #4012, and the residue #3962, #3998 and #4012 each declined in turn. The cause was one step earlier than any conversion rule: #3962 taught the binder to RETAIN a[N]Ttype argument symbolically, soList[[3]int32]carries[3]int32, but a slice argument was still erased toT[]. That leftList[[]int32]carrying an EMPTY symbolic argument vector — which is exactly what aList<int[]>recovered from METADATA carries, and a metadata array's shape genuinely is unknowable — so the metadata carve-out correctly kept the lenient CLR comparison and there was nothing left to compare. Both G# array spellings are now retained, at the same four gates (declared type, construction, fully-qualified construction, static generic receiver), so the two reportGS0155/GS0154naming both spellings, in both directions, at any nesting depth, in a value position of a multi-argument generic (Dictionary[string, []int32]), and through a variant interface. Note this deliberately does NOT mirror the bare rule, where[3]int32still widens implicitly to[]int32: a generic's type argument is matched by identity, not convertibility, soListbeing invariant inTmakes the two instantiations as unrelated asList[Derived]andList[Base]. The sharpest case is a COVARIANT interface — aList[[3]int32]at anIEnumerable[[]int32]parameter is now refused, even thoughIEnumerableis covariant and read-only and the bare element conversion is implicit — which is the same line #3962 drew when it refusedIEnumerable[[3]int32]toIEnumerable[[4]int32], through the same shared predicate. What does not change: a member slot over a slice keeps the bare widening (List[[]int32]().Add([3]int32{…})still binds), because[]Tand the CLRT[]describe the same shape and need no symbolic rescue; member parameters, indexers and iteration over aList[[]T]are byte-for-byte as before, since a retained slice is still not a substitutable type argument. One member position DOES move, and it is the only breaking change here beyond type identity: a generic's METHOD RETURN follows the retained slice, because that path projects on a nonempty symbolic vector without consulting substitutability. SoStack[[]int32]().Pop()now surfaces[]int32rather than the erasedint32[], and assigning it to a[3]int32goes from accepted to rejected — which is what #3998's bare rule already required of every other position, the old acceptance being the metadata carve-out firing on a type that only looked reflected because the slice had been erased. The explicitcast[[3]int32](…)still works. The indexer is unchanged and is pinned as such; the #1354 nullable-flags path is untouched in substance, because an ANNOTATED slice ([]string?) was already retained before this change and never took that branch, so the arguments newly diverted off it are exactly those with no annotation to lose — the emittedNullableAttributebyte arrays forList[[]string],List[[]string?]andList[string?]are identical before and after; and an array recovered from METADATA is still exempt in both directions and against both spellings — a G#List[[]int32]and a G#List[[3]int32]both still reach a C#List<int[]>parameter, and a C#List<int[]>return still fills either slot. Zero of the 180.gsfiles undersamples/,src/Sdk/,bench/,e2etests/andtest-assets/change a single diagnostic. -
A
map[K, V]reaches the interfaces itsDictionary[K, V]backing implements, not only the class (issue #4011).var d IDictionary[K, V] = mreportedGS0155whenever the key or value was still open, while the closed spelling of the very same conversion (map[string, int32]at anIDictionary[string, int32]) was accepted — the one row where the two spellings disagreed, and it disagreed in the direction that rejects working code. #3987 taught the lattice that amap[K, V]IS aDictionary[K, V]by shape once an element is open, and stopped at the concrete class; the interfaces had no rule of their own, and the reflective assignability rules that would otherwise answer need a CLR type the open map does not have. Amap[K, V]now converts implicitly toIDictionary[K, V],IReadOnlyDictionary[K, V], andICollection/IReadOnlyCollection/IEnumerableofKeyValuePair[K, V](and tosequence[KeyValuePair[K, V]], the alias spelling of the last) whenever the elements match, in that direction only — anIDictionary[K, V]is not necessarily aDictionary[K, V]. The conversion emits no IL: the value already is that interface. One side effect, at a G#-declared parameter: a(map[K, V])?at a non-nullableIDictionary[K, V]now reportsGS0154instead of being silently accepted, because the null-safety gate (#3999) declines to blame nullability when the non-nullable form would not reach the parameter either — and until now it would not. The closed spelling of that row already reportedGS0154, so the two now agree. At an imported parameter nothing changes and nothing disagreed: both spellings are accepted, before and after, which isBindClrParameterConversions' general leniency about a CLR parameter's declared nullability rather than this gap. -
A closed generic type clause whose type argument violates the definition's constraint is now an error (issue #4032).
class Bad : Handler[string], where the importedHandler<TOptions>declareswhere TOptions : SchemeOptions, compiled with NO diagnostic and threwTypeLoadException: GenericArguments[0], 'System.String', on 'Handler`1[TOptions]' violates the constraint of type parameter 'TOptions'the first time the program touched the type.Type.MakeGenericTypevalidates constraints for a live runtime definition and does not for one loaded by aMetadataLoadContext, which is where every/referenceassembly lives; the imported generic METHOD path has compensated for exactly that since #750, and the generic TYPE path never did. Every user-facing construction of a closed imported generic now reportsGS0152naming the type argument, the type parameter and the constraint that failed — the four type-clause spellings (a base clause, a field or local type, a function parameter or return type, a nested type argument) and the three expression spellings that never pass through a clause at all: a direct constructor callHandler[string](), a static member through a closed receiverHandler[string].Describe(), and an object literalHandler[string]{…}. Only CLOSED instantiations are asked: a type argument that still mentions a type parameter is erased to anobjectplaceholder, and asking a constraint of a placeholder is how #4016's projection broke a SIBLING parameter's constraint, so an open instantiation is left alone. A same-compilation class is NOT skipped — the check reads the symbol's own base chain rather than itsobjectsurrogate — soclass MyOptions : SchemeOptionsstill satisfieswhere TOptions : SchemeOptions. The GS0149 half of #4032 as reported (a SATISFYINGclass MyHandler : Handler[MyOptions]rejected as "not generic") did not reproduce on565b9a04across eleven shapes and four reference kinds; those shapes are pinned as green fixture rows. Not covered, each pre-existing, measured on the parent and filed separately: a G# generic class that derives from a constrained imported base without forwarding the bound (class H[T] : Handler[T]) still compiles and emits IL that ILVerify rejects withUnsatisfiedMethodParentInst(issue #4037, the forwarded spellingclass H[T SchemeOptions] : Handler[T]verifies); a DEPENDENT constraint over two distinct same-compilation classes (Coupled[A, List[Bee]]againstwhere U : IList<T>) was still accepted because both erase to the sameobjectplaceholder (issue #4041, closed in this same release by answering a dependent bound on the symbolic vector; the same mismatch spelled with imported types was already caught here); and an object literal of an imported generic over a same-compilation type argument does not bind at all, reportingGS0157(issue #4042, the constructor spelling of the same instantiation works). One violation reports ONE diagnostic at every spelling. The binder PROBES the same receiver several times before committing to a reading of it — a qualified accessor is walked three times through three different entry points, an unqualified one twice — so the first form of this check reported the identicalGS0152three times forSystem.Nullable[string].Valueand twice forHandler[string].Describe()— a count that was a function of how many internal paths the binder happened to take; the report is now made once per expression, in the shared checker, so a future caller cannot reintroduce the fan-out. The qualified spelling also used to FOLLOW the accurate error with a false one:GS0149 "Type 'Nullable' is not generic"is a last-resort fallback the qualified walker reached whenever a closed construction failed for any reason, andSystem.Nullableis of course generic — it is the argument that is wrong. That fallback is now suppressed when the failure has already explained itself, soSystem.Nullable[string].ValuereportsGS0152alone. A genuinely non-generic type named with type arguments still reportsGS0149. Two spellings still emit a CONSEQUENTIAL follow-on rather than a duplicate — an unqualified receiver (Nullable[string].Value) falls through to ordinary index-expression binding and adds twoGS0125s, andHandler[string]()adds aGS0130— tracked as issue #4051.-after(d)andtick(d)round a delay or period UP to a whole millisecond (ADR-0174 D8/D9 erratum 46, issue #4008).System.Threading.Timertruncates aTimeSpanto whole milliseconds, which armed a timer at an interval SHORTER than the one asked for — and, below 1 ms, at a period of zero, which per theTimer.Changecontract disables periodic signalling. Atick(d)with any0 < d < 1mstherefore produced exactly one tick and then went silent for ever, with no error and no diagnostic: measured over a busy-polled 500 ms window, periods of 0.1, 0.5 and 0.9 ms each produced 1 tick against 1 ms's 426, and none of the three reached 20 ticks in 10 s. Both timers now arm a strictly positive interval rounded up to the next whole millisecond, so the interval armed is never shorter than the one requested. Two visible changes for existing programs: a sub-millisecondticknow keeps ticking, at 1 ms; and a fractional period above the cliff slows to the next millisecond instead of running fast — a period of 1.5 ms ticked at 1 ms (426 in 500 ms) and now ticks at 2 ms (249), and 2.5 ms ticked at 2 ms (249) and now at 3 ms (166). (There is no duration literal in G#; both timers take aTimeSpan, so these aretick(TimeSpan.FromMilliseconds(1.5))and so on.)aftermoves with it, so a sub-millisecond delay is no longer truncated to an immediate fire (mean latency over 200 samples: 0.9 ms was 0.007 ms and now lands with the 1 ms row at ~1.19 ms).after(0)is still immediate, andtickstill rejects a period of zero or less withArgumentOutOfRangeException— the same shape Go's ticker panics on. Rejecting a sub-millisecond period was the alternative and was declined: it would maketickpartial on inputs Go'stime.Tickeraccepts, and would say so as a runtime exception inside a program that had been running. -
An explicit type argument naming a BASE of the real element is refused rather than compiled to unverifiable IL (issue #4026).
Probes.CountAny[ImportedBase](List[Derived]())COMPILED, ran, printedany:0— and ILVerify rejected the assembly (StackUnexpected: aList<Derived>found where aList<ImportedBase>is expected). The type argument is genuine, so the MethodSpec really is closed overImportedBaseand the parameter really is the INVARIANTList<ImportedBase>; only the argument's erasedList<object>surrogate made it look applicable, and the #3989/#4006 check that exists to refuse exactly that was waived because the slot's open declaration readsList<T>and so counted as "possibly erased". It is not erased when the call site PINNED it: a slot whose open declaration mentions only method type parameters the call supplied with types that have a CLR identity of their own is no longer exempt, and the call reportsGS0159— the same diagnosticList[Derived]at a genuineList[ImportedBase]parameter already reported. Themapspelling of the same mistake is refused with it, and so is a call that pins one type parameter genuinely beside one it does not (TakeMap[ImportedBase, Derived](map[Derived, Derived]{})), whose genuine invariant KEY mismatch used to be waived along with the whole parameter. Nothing else narrows: a type argument that is itself an erasure SURROGATE (a same-compilation class arriving as its imported base, which is what #4016 made the placeholder produce) is not genuine and keeps the exemption, so every #4016 spelling goes on binding, and a genuinely COVARIANT slot still takes a base type argument —Probes.CountSeq[ImportedBase](List[Derived]())at anIEnumerable<T>parameter, andProbes.CountArray[ImportedBase]([]Derived{…})at aT[]one, since CLR reference arrays are covariant. -
A collection literal, and a call carrying a same-compilation argument, no longer reach a widening explicitly-implemented interface member (issue #4028). #4013 closed this for an author-written call but had to cut two exemptions at candidate COLLECTION — which is memoized on
(Type, name)and cannot see arguments — soList[int32]{"x"}andDictionary[string, int32]{"a": "x"}went on compiling and throwingArgumentExceptionfrom insideIList.Add(object)/IDictionary.Add(object, object), as didList[int32]().Add(someUserTypedValue). The discrimination is now made at applicability, on the collected candidate list, where the receiver's and the arguments' real types are both in hand: an abstract member of a NON-GENERIC interface is kept only when there is a genuine erasure to repair — either the RECEIVER carries a same-compilation type, so its own members' parameter types are surrogates that cannot judge anything (List[System.Action[Mode]].Add((item Mode) -> …)), or the receiver's own same-named member WOULD have taken the call on the arguments' real types (a[]Celsiusspread into aList[float64], whose user-defined conversion the erasure hid). Everything else reportsGS0159, which is what the author-writtenList[int32]().Add("x")already reported. A GENERIC interface's member is never affected: on a constructed class receiver it is closed over the receiver's own type arguments and cannot widen, soDictionary[K, V](){ ...pairs }goes on filling throughICollection<KeyValuePair<K, V>>.Add. -
A user-defined implicit conversion is applied at a call on a generic collection, not only in the spread form (issue #4036).
List[float64]().Add(someCelsius)compiled to verifiable IL and threwArgumentExceptionfrom inside the BCL, while the spreadList[float64](){ ...someCelsiusSlice }printed the value — the same pair of types, two answers. A same-compilation struct has no CLR type while binding, soCelsiuspresents to overload resolution asobject; against aList<double>that is an IDENTITY match forSystem.Collections.IList.Add(object)and no match at all forAdd(double), so the widening member won and theCelsiuswas boxed and handed over as-is. The spread form escapes because it types its item placeholder as the collection's ELEMENT type and converts BEFORE bindingAdd, so applicability never sees the erased argument. The two forms now AGREE rather than share a path: where the receiver's own non-widening surface would have taken the call and some position needs a user-defined implicit conversion to do so, that position is converted before applicability runs. #4028's decision is untouched — the widening member is still KEPT whenever a conversion exists; it simply stops WINNING, because a realdoublebeats a boxed one. Six neighbouring forms close with it, two of them previously silent rather than loud: the collection literalList[float64]{ celsius },Dictionary[string, float64]().Add("a", celsius)andlist.Insert(0, celsius)threw;list.Contains(celsius)answeredFalseandlist.IndexOf(celsius)answered-1; andlist.Remove(celsius)reportedGS0124becauseIList.RemoveisvoidwhileICollection<T>.Removereturnsbool. Named arguments bind correctly at both halves of the decision: the candidacy filter #4028 added, and this repair, each compared SOURCE-order arguments against DECLARATION-order parameters, solookup.Add(value: someCelsius, key: "a")asked whether aCelsiusfitskey: stringand reportedGS0159for a valid call — pre-existing on the candidacy side, measured. Both now reorder through the resolver's own named-argument mapping, the same one #4026 reached for. Still refused, unchanged: an argument with no conversion to the element type (List[int32]().Add(someCelsius),List[int32]().Add("x"), or a reorderedDictionary[string, int32]().Add(value: someCelsius, key: "a")) reportsGS0159. -
A same-compilation user-defined implicit conversion now participates in ordinary imported CLR method overload resolution (issue #4054).
Math.Abs(someCelsius)reportedGS0159even though the sameCelsius -> float64operator already worked at a G# parameter and, since #4036, at a generic collection member. The argument has no CLR type while its assembly is being emitted, so the reflection resolver saw only itsobjecterasure and rejectedAbs(double)before the conversion binder could apply the operator. Imported static, instance, inherited and extension method calls now retain the symbolic argument only for one narrow callback: classify whether the candidate parameter is reachable through the existing implicit user-conversion resolver, including its ambiguity rules. The winning argument is still converted by the established CLR-parameter conversion pass. Erased arguments gain no general applicability, explicit-only operators remain inapplicable, and imported arguments stay entirely on the existing CLR classifier, so their conversion precedence is unchanged. The rule covers ordinary, overloaded, generic andparamsmethods and both value- and reference-returning conversion operators; unrelated conversion targets remain ambiguous and a target with no conversion still reportsGS0159. -
A constraint violation on an unqualified generic receiver no longer cascades into false diagnostics (issue #4051). #4032 made gsc validate declared generic constraints at every construction site, and at the qualified spellings the resulting
GS0152stands alone. At the unqualified ones it did not: the resolver returned "not a type" after it had already REPORTED, so the binder re-readName[Arg]as an ordinary index expression and lookedNameandArgup as values — telling the authorGS0125: Variable 'Nullable' doesn't exist.andGS0125: Variable 'string' doesn't exist.about a predefined type keyword. The resolver now says whether a failure already explained itself, and each caller stops instead of inventing a second reason. Twelve spellings reportGS0152exactly once and nothing else: the eight in the issue plus a static-field read, write and compound assignment (GS0125 x2each) and the generic-name receiver shapeHandler[string?](GS0124+GS0159). The constructor spellingHandler[string]()is one resolver over — it addedGS0130 Function 'Handler' doesn't existabout a type that plainly exists — and now claims the call as a refused CLR construction. Nothing individually correct is suppressed:GS0152is still reported at every spelling, and every satisfying instantiation goes on compiling, IL-verifying and running. Not covered, each pre-existing, measured on the parent and filed separately: a fully-qualified generic CONSTRUCTOR callSystem.Nullable[int32]()reportsGS0157 Cannot find type System, about a namespace (issue #4055); and a static compound assignment through a generic receiver over a same-compilation type argument (Handler[MyOptions].Field += "z") emitsHandler<object>and throwsTypeLoadExceptioneven though the constraint is satisfied (issue #4056, the generic-CONSTRUCTION placeholder erasure of #4016/#4041 one receiver shape over — since fixed, and listed under Fixed below). -
A receiver clause always declares an extension now — the
extensioncontextual keyword,GS0103, andGS0314are all retired (ADR-0182, issue #4240). ADR-0024 and ADR-0079 gavefunc (r T) M() { ... }an ownership-dependent meaning: an instance method (with a softGS0314nudge toward the in-body form) when the current package ownsT, an extension otherwise, and an outrightGS0103rejection for a same-package non-aggregate receiver (an enum, an interface, a non-struct alias). ADR-0165 (v0.4) patched the enum/owned gap with a contextualfunc extension (r T) M()marker, which worked but reintroduced exactly the kind of ambient contextual keyword the language has been retiring elsewhere (:=, the=named-argument separator, thedelegatetype-alias spelling) — and collided with ordinary use: the website's own Trail sample usesextensionas a plain parameter name, which the Prism and VS Code grammars had to special-case to avoid mis-highlighting.func (r T) M() { ... }is now unconditionally an extension, for anyT— owned or not, class, struct, or enum — exactly like today's non-owned case: parameter zero is the receiver, no interface conformance, no private-member access, no entry in the receiver type's own method table. Owned instance methods are declared inside the type body ONLY; there is no receiver-clause spelling for one anymore.GS0103andGS0314are retired outright — the ownership-dependent binding rule they policed no longer exists — and the reserved IDs are pinned inDiagnosticIdUniquenessTestsso they cannot be reused, the same way ADR-0174 retiredGS0316/GS0317. Thefunc extension (...)marker is itself a retired spelling: the parser recognizes the shape and reports the newGS0587rather than silently accepting it, then recovers by parsing the declaration exactly as ifextensionhad not been written — one clean diagnostic, no cascade.extensionis an ordinary identifier again everywhere else:func extension(x int32) int32 { ... }(a plain function namedextension) andfunc (s string) extension() string { ... }(an extension method namedextension) are both unaffected, and the website/VS Code grammars drop their carve-out. The one genuinely silent behavior change: source still on the pre-ADR-0182GS0314-warned spelling (an owned-type receiver-clause method never migrated to in-body) now binds as an extension instead of an instance method. Interface-conformance loss,overrideloss, and private-member access all fail loudly at compile time under the new binding — but a receiver-clause method on an owned struct that mutates a field now silently mutates a throwaway by-value copy instead of the caller's struct, with no diagnostic (the same hazard any struct extension method on a non-owned type already has; owned struct receivers now inherit it too). This repository has no surviving instance of the hazard — the Oats sweep already migrated every in-treeGS0314trigger to the in-body form when that warning shipped in v0.4 — but source outside this tree that never migrated should be checked before upgrading. Operators are the one deliberate carve-out and are unaffected:func (a T) operator +(b T) T { ... }has no in-body spelling (ADR-0079), so an operator on an owned struct/class keeps attaching directly to that type exactly as before; only the "explicit extension operator on an owned type" combination ADR-0165 allowed has no replacement spelling, and nothing in-tree used it.cs2gs's enum/owned special case (RequiresExplicitExtensionReceiver, added for ADR-0165) is deleted outright — every C# extension method now translates to the same plain receiver-clausefunc, regardless of receiver ownership or kind. -
Several imported members now keep a
?they declare, so code that used a nil value as non-null may now report a diagnostic (ADR-0193 Phase 2, issue #4363). The affected positions include a duck-typed enumerator'sCurrent, an awaiter'sGetResult(), and imported CLR operators. The list and the remedy (narrow, assert with!!, or make the destinationT?) are in the Fixed entry "Several imported members now keep the nullability they declare" below. -
A derived class can no longer reach a
protectedinstance member through a receiver typed as the base class (issue #4453). Insideclass Derived : Source,s.f,s.M(),s.Pands.E += hwiths Source(or withsof a sibling subclass) compiled, and the output failed IL verification (FieldAccess/MethodAccess) because the CLR, like C#'s CS1540, allows that access only through the derived type. It now reports GS0379 for fields, methods, method groups, properties and events, in reads, writes, compound assignments, lambdas, null-conditional accesses and initializers.this,base, bare names, receivers of the derived type or its subclasses, and staticprotectedmembers are unaffected. Remedy: type the receiver as the derived class, or expose the member through a non-protected accessor on the base. -
cs2gs now rejects C#
#ifand#elif(issue #4632). G# has no conditional compilation. cs2gs used to translate only the arm active under the build's preprocessor symbols, so the other arms disappeared without notice. A Release-only test assertion was lost this way in the self-migrated tree. Every#ifand#elif, including those nested in inactive arms, now reports theCS2GS-CONDITIONAL-COMPILATIONtranslation error at its source location, and the app fails the Translate stage.- Remedy: rewrite the condition as a runtime check of whatever the arm actually depends on, such as a configuration setting or an environment variable, or delete the arm that is not needed. A
#if DEBUGthat guards behavior only valid for optimized code can testDebuggableAttribute.IsJITOptimizerDisabledon the assembly. This repository's allocation tests do that, but it reflects the optimizer setting, not theDEBUGsymbol. cs2gs migrate --allow-conditional-compilationdowngrades each site to a non-fatal warning in stderr andtranslate.logand translates the active arm. It exists for a pinned external corpus that cannot be edited, and covers only the Translate stage's project sources. A#ifin an analyzer test snippet, an orphan source, or a library's.Testsproject translated for parity is still an error.
- Remedy: rewrite the condition as a runtime check of whatever the arm actually depends on, such as a configuration setting or an environment variable, or delete the arm that is not needed. A
Added
-
abstract classandshared classmodifiers (ADR-0195, issue #4674).abstract class Walker { … }is inheritable and cannot be instantiated even when it declares no abstract member (Walker()andWalker{}areGS0386).shared class Helpers { … }is the spelling of a C#static class: it is emitted CLRabstract sealed, cannot be instantiated, and its body is the shared member list, so every field,const, property, event andfuncdeclared in it is shared,init { }is its static initializer, and ashared { }block inside it is rejected (GS0617, which also coversinit(…), primary constructors,deinitandopen/overridemembers;GS0618onopen/sealed/abstract/data;GS0619on a base type; every part of a partial shared class carriesshared,GS0479). Both are contextual and valid only on aclasshead. cs2gs now emitsabstract classfor a C#abstract class(previously dropped, leaving an instantiableopen class) andshared classwith flat members for a C#static class(previously an instantiable class with ashared { }block), so a migrated library keeps the CLR shape of those public types; non-static classes keep their trailingshared { }block. -
cs2gs capture-test-oracleand per-test-name test parity (issue #4633). A migrated test project used to pass parity on its exit code and a count of[Fact]methods, so[Theory]rows and renamed or dropped cases went unnoticed.- The new verb lists each C# test project's cases.
cs2gs validate --csharp-test-oracle <dir>andcs2gs migrate --csharp-test-oracle <dir>compare the migrated run's TRX with that list, name for name.- Justified differences go in
tools/cs2gs/selfmig-test-name-baseline.json. cs2gs validatenow requires the oracle, or an explicit--count-only-test-parity.- The mirrored test budget ceiling is now 120 minutes (was 90).
-
cs2gs migrate --sdk-version <v>and--sdk-pin global-json(issue #4631). A repository migration can now pin an exactGsharp.NET.Sdkversion, such as a published release, instead of the newest locally built package, and can write that pin undermsbuild-sdksin the migrated repository's root and nestedglobal.jsonfiles instead of into every generated project file. All nested scopes use the same version while retaining their other settings.cs2gs validatefollows the pin recorded in the migrated tree, rejects missing or conflicting nested pins, and accepts--sdk-versionto check it. SDK attributes, explicit SDK elements, and SDK imports participate in pin rebinding and validation without dropping unrelated SDKs in translated or excluded projects. Global-json migration canonicalizes existing root pins. Copied native.gsprojfiles use the same pin; template payloads, including project/item templates and theirglobal.jsonfiles, remain untouched. Per-project validation rejects bare G# SDK declarations instead of silently resolving a different SDK, and rejects root or nested global pins that would mix pin modes. Bootstrap projects are rebound even with existing root SDKs, without dropping unrelated SDK declarations. Isolated stage-4 parity projects retain the resolved version, including when the exact package must be restored from NuGet instead of a local build. -
cs2gs reports every fail-fast
!!it puts on a value stored into generic storage (issue #4612, option 1).- What: nullable-oblivious C# can pass a possible null to
list.Add(x),items.Append(x), a delegateInvoke(x),dict[k] = v, a lambda returning intoFunc<..., TResult>, an array element (new[] { a, b },arr[i] = v), a collection initializer or expression element, or an iteratoryield, and the null is stored. cs2gs keeps its fail-fast policy and asserts the value, so the migrated code throws where the C# ran. - Where: each such site produces a
CS2GS-GENERIC-STORE-BRIDGEline in the app'stranslate.log. The line gives the location, the kind of slot (inferred or explicit method type argument, constructed generic member, delegate argument or result, type-parameter variable assignment or tuple element, array element, collection element, iterator element), the target, the destination slot's nullability, whether the result depends on that slot (using the containing type for constructor results), and the value. Embedded analyzer-test snippets retain this diagnostic identity and each distinct inner source span, including identical messages across namespace units. Nested tuple paths retain declared type-parameter classification and the actual leaf destination type, including nullable tuple envelopes at every nesting level, expandedparamselements, block-bodied/async lambda delegate return contracts, and substituted method type arguments. Synthesized tuple-key leaf suffixes survive value truncation. Whole-array or whole-collection writes toparamsparameter variables and direct carriers passed toparamsindexers are not expanded element stores. stderr gets one count per app. - Effect: generated code is unchanged. Explicit C#
value!assertions are also reported through null-preservingas,??,?., checked/unchecked wrappers, and value-producing=/??=assignments feeding generic arguments, elements, declarations, or method/delegate results, even when the destination accepts null, including nullable span and builder collection expressions. Ordinary assignment bridges also resolve the outer generic store even when their immediate target is concrete. Distinct store metadata and synthesized tuple-key member paths remain separate at a shared source span; exact speculative repeats of the same store and leaf still report once. A bridge whose target cannot be resolved is reported asunknown-target, so the list over-approximates rather than missing a site.
- What: nullable-oblivious C# can pass a possible null to
-
go2gs analyzenow provides the M0 typed Go inventory (ADR-0191, issue #4333). The separately versioned Go helper pinsgolang.org/x/tools/go/packagesand records schema-v1 package/test variants, module and build provenance, source bytes/spans, stable typed identities, exact constants, scopes, selections, calls, method sets, generic instances, embeds, generators, dependencies, diagnostics, and blockers. Analysis is offline and fail-closed: automatic toolchain downloads, ambient workspaces,GOPACKAGESDRIVER, generators, target binaries, and network module resolution are disabled. Exit zero means the requested inventory completed, whilemigrationReadyremains independent and false in M0. Migration blockers preserve typed nil/interface, byte-string, map, panic/defer/recover, fixed-array, and concurrency sites without choosing their later G# representation. Profile flags are allowlisted against tool execution/path overrides, and the selected Go executable supplies its own verified GOROOT. Preload-only failure inventories now obey the same final record bound as loaded inventories; package-input drift covers every Go-recognized extension independently of CGo selection; and validation recomputes source-manifest identity and UTF-8 claims while indexing scope ownership linearly. Version and GOROOT discovery use captured build metadata and the verified path/handoff rather than executinggo versionorgo env. The captured executable must identify as nativecmd/go; on Linux it must also be static ELF (including an internally linked static PIE) with no interpreter, dynamic imports, RPATH, or RUNPATH. Scripts and wrappers are rejected, and child PATH contains only its private stage. The Linux analysis worker applies a 2 GiBRLIMIT_DATAceiling before package loading so x/tools' internal command buffers remain process-bounded. Darwin preload now requires a native executable Mach-O with the matching CPU and executable file type. Non-Linux preload checks can publish deterministic toolchain/source mismatch blockers without loading packages; a matching profile still fails closed before publication. M0 now usesgo/packagesonly for metadata withCGO_ENABLED=0, then parses captured source and type-checks it in-process without compiled-file/export requests. It never executes the GOROOT compiler, assembler, linker, cgo, vet, a C/C++ compiler, pkg-config, or helper. It independently records profile-selected CGo/native source inputs and emits deterministic blockers; legacy profile compiler fields must be empty. The selectedcmd/go, captured GOROOT, and helper runtime/build information must self-report the same canonical final-release Go version label before preload. These labels are provenance metadata rather than authentication; executable and captured content hashes are authoritative identities, while validation checks structural and relational consistency. Unsupported labels and nonemptygoExperiment/goDebugsettings or helper build-timeGOEXPERIMENT/DefaultGODEBUGoverrides fail bootstrap rather than silently using different in-process parser/type-checker semantics. Entry package patterns are limited to canonical module-relative paths (with an optional final/...recursion), preventinggo/packagesquery forms or absolute paths from selecting source outside the captured mirror. Source capture and verification reject symlinks in every path component, bound directory reads and all traversed entry types before sorting, and share a deterministic traversal budget with local replacements. Profile bootstrap now requires a stable regular file no larger than 256 KiB before decoding or applying the configured timeout. Blocker messages now redact private filesystem roots and are UTF-8-normalized and bounded before stable-ID derivation, so incomplete inventories remain checkout-independent and each ID names the exact published message. Redaction respects path-component boundaries, selects nested roots before their parents, and recognizes Windows drive/UNC case and separator aliases without over-redacting lookalike prefixes or remote URIs; absolute,localhost, drive-authority, and configured UNCfile:URI paths are redacted, while unknown or encoded authority forms, unmatched drive paths, dot-segment aliases, and malformed or path-bearing query/fragment text fail closed. Strict path components also reject malformed whitespace, punctuation, and embedded drive tails, includinglocalhosttext outside the actual file authority. Operational local-replacement rewriting makes only the mirroredgo.modowner-writable; persistent descriptor-relative temporary-file open failures now abort cleanup instead of being retried. Structural type identities include positional ownership for unexported anonymous fields and interface methods, and compiled-file records publishgo/types' effective per-file language version. Profile andgoFlagstags share one canonical selection set, architecture feature levels expand to cmd/go-equivalent cumulative tool tags, and complete artifacts require a typed*ast.Fileroot for every compiled file. Output locking, invalidation, and publication stay relative to one held directory handle across ancestor renames. The output directory must remain exclusively controlled by go2gs; the lock excludes cooperating writers and observed identity drift fails, while active same-UID mutation is explicitly outside the portable threat boundary. Hashes detect provenance drift but do not authenticate such a peer. Span validation reparses captured bytes to verify exact raw and line-directive-adjusted paths, lines, and columns. Diagnostics are path-stable, initialization order is explicit, and schema validation rejects missing fields, count mismatches, stale payload-derived deterministic record IDs, including package IDs derived from semantic-profile plus authoritative GOROOT/toolchain and actual module/replacement identities, and type IDs scoped to that semantic profile. This separates GOOS/GOARCH/tag and local-replacement-content inventories without losing within-profile canonical type deduplication. Validation also rejects unknown file roles, native/embed role-flag disagreement, mismatches betweencompiledrole and owning-packagecompiledFileIds, noncompiled-file syntax trees, detached nodes, parent cycles, and complete artifacts without a loaded main-module package/source ownership graph. Self-embedded sources retain their compiled roles and embed links. Each node-bearing file must be one*ast.File-rooted tree. Blocker IDs are indexed during construction so bounded deduplication remains linear. These checks reuse producer contracts and establish internal consistency, not artifact authenticity. The pinned cliamp profile reports an actionable mismatch rather than substituting the installed Go toolchain. -
ADR-0191 now defines conservative Go interface/address lowering and staged performance gates (issue #4513). Concrete values snapshot at conversion, pointers retain their selected location, existing and converted interfaces preserve original Go dynamic identity, and value receivers copy per call. Wrapper reuse requires explicit proof that evaluation timing, typed-nil/comparability behavior, mutable adapter state, and observable CLR interop identity cannot change; address reuse similarly preserves dynamic bindings, slice owner/index selection, append reallocation, side effects and throwing behavior. The prerequisite spike merged in #4514 now requires non-empty Go/JIT/NativeAOT semantic parity, paired checksums, raw samples/allocation totals and exact provenance. Its runner has ADR-0154 failure mutants, while
--no-aotis explicitly exploratory. The allocation fixes for issues #4511 and #4512, implemented by PRs #4515 and #4516, are enforced by the integrated prerequisite gate; semantic completion and later package/application performance readiness remain separate. This is policy and native-mechanism evidence, not ago2gstranslator implementation. -
@GeneratedRegexworks in native G# projects (ADR-0192 follow-on 2, ADR-0145 amendment). Declare the method as a partial method in asharedblock,shared { @GeneratedRegex("\\d+") private partial func Digits() Regex; }, and the SDK runs the real Regex source generator throughgsgen. The generated implementation becomes a G# implementing part that pairs with your declaring part, so calls run through the generatedRegexsubclass. The same applies to other generators that implement partial methods, such as@LoggerMessage. -
A
partial data classorpartial data structcan hold partial methods, and@GeneratedRegexworks in one (ADR-0192 amendment). This is the G# spelling of a C#partial recordwith a[GeneratedRegex]method, e.g.public sealed partial data class GitHubUrl(Owner string, Name string) { shared { @GeneratedRegex("^https://...$") private partial func Pattern() Regex; } }.gsgennow shows generators a data type as a C#recordwhen its inheritance chain allows it: it has no base class other than record-rendered data types, and no plain class derives from it. Otherwise the type is still a C#class. Either way, the generated part getsdataso it merges with yours. A positional parameter list a generator adds to a data type that already has one is reported asGS9209. Before, the build failed withGS0479.GS0607andGS0608now say that a data class or data struct qualifies.- The implementing part is spelled with your declaring part's header, so
System.Text.RegularExpressions.Regexorcount intpair as well asRegexorcount int32. A parameter-name mismatch between your declaring part and the generated implementation is reported asGS9208instead of being paired. - The generator's helper types keep their own package,
System.Text.RegularExpressions.Generated, so a type of yours namedUtilitiesno longer collides with them. - A pattern that backtracks inside a loop, such as
(foo|ba+r)+\w*?baz, builds too: issue #4422 lets the generated helpers pass&base.runstack, and cs2gs carries the generator's!across as a scoped@SuppressDiagnostic("GS0612").
- The implementing part is spelled with your declaring part's header, so
-
cs2gs migratetranslates[GeneratedRegex]to a G# declaring part, and the__generatedRegex_rewrite is gone (issue #4301, ADR-0192 follow-on 3, ADR-0143). A C#[GeneratedRegex(...)] private static partial Regex Digits();becomes@GeneratedRegex(...) private partial func Digits() Regex;in the type'ssharedblock (an instance method stays an instance member), and the migrated project's build runs the real Regex generator throughgsgen, exactly as a native G# project does. The old translation constructed a cachedRegexin a private__generatedRegex_Digitsfield instead.- The pattern, options, match timeout and culture name are re-spelled from their constant values, positionally, so a
constpattern or a namedmatchTimeoutMilliseconds:argument migrates as a literal. - Culture-sensitive IgnoreCase (
RegexOptions.IgnoreCasewith a culture name, or an inline(?i)withoutCultureInvariant) now migrates; the old translation reported it as unsupported. - The generated C# implementation is dropped, even when it is a committed file.
- A C#
partial recordorpartial record structkeeps the declaring part in the translated G#partial data classorpartial data struct. - A top-level-statements program's partial
Programclass that declares a[GeneratedRegex]method stays a class instead of being hoisted to top-level funcs, and its private members becomeinternalso the G# top-level statements can reach them. - A
[GeneratedRegex]method of a nested type is reported as unsupported because gsgen implements only top-level types; move it to a top-level partial type. The partial-property form is still unsupported because G# has no partial properties.
- The pattern, options, match timeout and culture name are re-spelled from their constant values, positionally, so a
-
A
fixedstatement can pin a fixed-size buffer field directly (issue #4378, ADR-0125 amendment).fixed p *int8 = h.Bone.Name { … }— or a barefixed p *int8 = Name { … }inside the declaring struct — bindspto the buffer's first element and keeps the buffer's containing storage pinned for the whole block, the G# spelling of C#'sfixed (sbyte* p = Name) { … }and the sameint8& pinnedIL csc emits. This unblocks the Raylib-csBoneInfo/ModelAnimationinterop pattern:cs2gsnow translates suchfixedstatements instead of reporting them as an Unsupported translation gap. As in C#, only a buffer in movable storage (a class field, an array/slice element, arefparameter, or a struct method's receiver) can be pinned; a buffer reached through a local, a by-value parameter or a pointer dereference is already fixed and reports the newGS0507(C#'s CS0213) — use it directly as a*Tinstead. -
A fixed-size buffer field can be used by its bare name inside the declaring struct (issue #4377).
Name[i],Name[i] = vandreturn Namenow decay the buffer to its*Tfirst-element pointer exactly asthis.Namedoes, instead of failing withGS0116("is not indexable"). -
G# source can declare a scope nullability-oblivious:
--nullability=oblivious,@Obliviousand@NullabilityEnabled(ADR-0186 §9, step 5). They exist socs2gscan translate a nullability-oblivious C# project without inventing a nullability nobody stated; hand-written G# has no reason to use them.- What changes. Inside an oblivious scope the top level of an unadorned reference type written as the type of a slot means the platform type
T!rather thanT: the declared type of a field, property, event, parameter, return, local,forvariable, inlineoutdeclaration or lambda parameter. Every nested position stays exactly as written: type arguments, array and slice elements, map keys and values, tuple elements, function-type parameters and returns, and the like. Sovar xs List[string]there has typeList[string]!: the slot's top level is platform, and the element is stillstring. Likewise[]stringis[]!string. A nil stored into an unadorned element is therefore an error, as outside the scope; writeList[string?]for a container that carries nils.T?still meansT?, and value types and open type parameters are unchanged. (The owner reversed the first cut, which also made nested positionsT!, because §3 rule 3 then rejected every container handed between an oblivious scope and an enabled declaration: about 2,700 of them in the self-migration corpus. Imported oblivious metadata is unaffected; its nested positions are stillT!.) - What does not. Base-type / interface lists, generic constraints, explicit-interface qualifiers and
@DllImport/@LibraryImportsignatures are exempt; the top level of acatch, pattern orif letbinding stays non-null. - Where it applies.
--nullability=obliviousmakes a whole compilation oblivious;@Obliviouson a type, function, property, field, event, parameter, local or block does it for one declaration, and@NullabilityEnabledopts one back out. The nearest annotation wins. Both are compiler-intrinsic (no attribute type, no metadata of their own); a malformed one is GS9307. - Metadata. An oblivious declaration emits explicit oblivious nullability metadata (byte
0) for its top level, and its nested positions' own state (soList[string]is{0, 1}). It re-imports asList[string]!from another assembly. Acscconsumer reads the top level as it reads a#nullable disablemember, and the element as non-null.
- What changes. Inside an oblivious scope the top level of an unadorned reference type written as the type of a slot means the platform type
-
A get-only auto-property can be assigned in its declaring constructor (issue #4350). As in C#,
prop Length int32 { get; }may be written — plainly, throughthis., with a compound operator, or with++— inside aninit(...)of the declaring type on the instance being constructed; the store goes straight to the backing field and no setter is emitted. Everywhere else, including a derived constructor and a lambda inside the constructor, it stays read-only (GS0127). cs2gs therefore translates a C# instance get-only auto-property to{ get; }instead of{ get; init; }, so migrated assemblies no longer gain a publicinitsetter the C# assembly never had (found comparing the migratedGsharp.Runtime.ValuesABI, whereSlice<T>.Length/Capacityhad acquired setters). Virtual, override, and static get-only auto-properties keep the previous spelling. -
A struct member can return a reference into its own instance state, with
@UnscopedRef(ADR-0184, issue #376, found restoring the fixture PR #4288 had to work around). G# has advertised@UnscopedRefsince ADR-0058, but what shipped only ever clearedIsScopedon aref structreceiver — which governs the by-VALUEGS0219check, not the ref-safe-context thatreturn ref this.<field>actually depends on.HasFunctionLocalRefScope's parameter case readp.IsScoped || p.RefKind == RefKind.None, and a receiver'sRefKindis alwaysNone, so the second disjunct was unconditionally true and the annotation was inert for its entire stated purpose; there was no property/indexer spelling at all, because accessors are built withdeclaration: nulland the old recogniser read raw declaration syntax.@UnscopedRefis now recognised by CLR TYPE IDENTITY (System.Diagnostics.CodeAnalysis.UnscopedRefAttribute, ADR-0084 §L5) off the bound attribute list, works on afunc, aprop, and an indexer of any struct orref struct, and is emitted as a realCustomAttributerow — on the MethodDef for thefuncspelling and the PropertyDef for theprop/indexer spelling — so a C# consumer of the assembly sees the same contract gsc enforced. The emitted body isldarg.0; ldflda; ret— the same sequencecscproduces for[UnscopedRef] public ref int Slot() => ref this.total;;ldarga.0there would yield a pointer to the receiver's own pointer slot, which would compile and verify and return garbage, so the emit test asserts the opcodes rather than just that it compiled. Because it is a real attribute type, it now requiresimport System.Diagnostics.CodeAnalysis; without it the binder reportsGS0198and the member is not un-scoped. cs2gs also hoists a C# ACCESSOR-level[UnscopedRef]up to the G# member-level annotation (C# treats both placements identically; G#'s code model has no accessor-attribute slot, so it used to vanish silently) —test/Core.Tests/Fixtures/UnscopedRefIndexerFixture.csis restored as a real.csfile and migrates again.- #4664 follow-up: The fixture restoration above records the ADR-0184
milestone. Its CLR import contract is now embedded C# source data in
test/Core.Tests/TestData/NativeMetadataContracts.cs.txt, compiled by the existing RoslynCSharpFixturehelper and passed to importer tests as an explicit physical native assembly so the oracle stays native when the test harness migrates. ADR-0184's UnscopedRef and escape semantics are unchanged.
- #4664 follow-up: The fixture restoration above records the ADR-0184
milestone. Its CLR import contract is now embedded C# source data in
-
A struct member's
thisis writable storage, in every instance member (ADR-0184 D2).ParameterSymbolderivesIsReadOnlyfrom itsRefKind, and a receiver'sRefKindisNone, soRefCapabilities.IsReadOnlyStorageclassified everythisas READ-ONLY — which is backwards, since the CLR passes a struct's receiver asref S.return ref this.Totaltherefore failed the lvalue/readonly gate and reportedGS0253("the operand of 'return ref' must be an lvalue") instead of ever reaching the escape check. The assignment binder already carried the correct exemption for member WRITES (ReceiverVariableIsThis, issue #947); the same fact is now on the symbol, where the static ref-capability classifiers can see it. Not conditional on@UnscopedRef— a value-type receiver is writable whether or not the member is annotated. A REFERENCE-type receiver is deliberately not exempted. -
awaitis legal in a plainfunc, and colours it (ADR-0174 D4'sawait g()row, issue #3954). Awaiting makes the awaiting function suspending, so colourless Go-style code can await an ordinaryTask/ValueTaskwithout becomingasync func. Where inference may not change a signature — an entry point, anopen/override/interface member, an accessor, a constructor, an operator, an iterator, a function literal, anunsafeorfixedbody —GS0574asks forasync func(an observableTask[R]) orsuspend func(implicitly awaited,R).GS0132keeps only the case where no enclosing function exists. -
An awaitable means in G# what it means in C#. The compiler awaits for you only where the syntax is a channel operation —
ch <- v,<-ch,select, channelfor..in— never because a library method returns a task.ReceiveBatch/SendBatchare ordinaryValueTask[int32]calls: writeawait ch.SendBatch(items), and.AsTask()on one names the task exactly as in C#. -
Gsharp.Runtime.Channels: a C#-authored channel runtime bundled with the SDK (tools/channels/) and auto-referenced by every project.Chan<T>is a rendezvous-capableChannel<T>subclass with Go-exact close semantics, a two-value receive, batch transfer, and the transactionalselectwaiter protocol;gsccopies it beside an emitted program that references it. -
Directional channel types and the D2 operation matrix: foreign BCL channels, readers, and writers flow into
chan[T]/in chan[T]/out chan[T]with no adapter — and back out, so achan[T]is accepted wherever a C# API asks for aChannel<T>, aChannelWriter<T>, or aChannelReader<T>(issue #3976). The lattice reads the types rather than the spelling, so a plain BCLChannel<T>now converts implicitly to a writer or reader target — thechan[T]->out chan[T]row written in CLR names, and the same conversion C# has always had throughChannel<TWrite, TRead>'s implicit operators. This holds when the element type is still open: insidefunc f[T](ch chan[T]), the channel reaches an imported constructor or method takingChannel<T>/ChannelReader<T>/ChannelWriter<T>(issue #3876), where it previously reported that no overload was applicable. -
A
: base(...)initializer argument whose type carries an open type parameter now resolves against the base's constructors (issue #3984).class MyList[T] : List[T] { init(items []T) : base(items) {} }reported thatListhad no one-argument constructor; the argument was never ranked at all.[]T,map[K, V],chan[T]and a tuple with a symbolic element all bind in that position now, and a base call that really is inapplicable reportsGS0267(naming applicability) rather thanGS0214claiming an arity the base does declare. Two shapes that used to compile become errors, because they were emitting unverifiable IL: an argument whose element does not match the base's type argument, such as[]stringor(int32, object)passed to aBase[T]'sIEnumerable<T>/(int32, T)parameter. -
A C#-authored generic extension on
ChannelReader<T>orChannelWriter<T>is now found on thechan[T]spelling callers actually write (issue #3877), so a C# library can publish an extension onChannelReader<T>/ChannelWriter<T>and have it bind on a whole channel. Method type inference contributes the channel's element through the direction lattice; the lattice itself is unchanged, so anout chan[T]still finds no reader surface, anin chan[T]no writer surface, and the element still has to match. The batch surface (D10) drops the fourChannel[T]overloads that existed only to work around this. -
A C#-authored generic method or CONSTRUCTOR over a
ChannelReader<T>orChannelWriter<T>also binds when the channel's element type is declared in the same program —chan[Pair]for a G#struct Pair, as well as an openchan[T]inside a generic function (issue #3982). Applicability compares the channel against the candidate at the same level of erasure, and the winning candidate is closed over the real element, so the emitted call carriesPairrather thanobject. The lattice is unchanged: anout chan[T]still finds no reader surface, anin chan[T]no writer surface, and the element still has to match. A genuineChannelReader<object>parameter stays out of reach, so a library that declares one beside its generic overload does not change which one a channel call picks. -
A nullable channel reaches a non-nullable imported
Channel<T>parameter whatever its element type (issue #3985).chan[T]?there used to reportGS0155where the closedchan[int32]?— and astring?at a non-nullablestringparameter — was accepted; both spellings now behave the same way, so!!is no longer needed at such a call, and the same now holds at a generic method's or generic class'sChannelReader<T>/ChannelWriter<T>parameter. This is a CLR-boundary rule only: passing achan[int32]?to a G#-declaredchan[int32]parameter still reportsGS0154, and direction and element are still enforced. -
A nullable channel also reaches a non-generic imported
ChannelWriter<T>/ChannelReader<T>parameter (issue #3992), so the last slot where achan[int32]?behaved differently from achan[int32]is gone:Plain.W(c)used to reportGS0159for the nullable form and bind for the non-nullable one. Applicability had already been ignoring the annotation wherever the CLR shapes were related (achan[int32]?reaches aChannel[int32]parameter exactly as astring?reaches astringone); only the fallback that ranks a candidate on G#'s structural shapes still re-asked the question on the annotated type, which is why the ADR-0174 direction view — the one relation the CLR does not carry — was the only one affected. Nothing becomes applicable whose non-nullable form was not already applicable, and the same limits hold: direction and element still decide, and a G#-declared parameter still reportsGS0154. -
A nullable channel reaches a non-nullable imported
Channel<T>parameter whatever its element type (issue #3985).chan[T]?there used to reportGS0155where the closedchan[int32]?— and astring?at a non-nullablestringparameter — was accepted; both spellings now behave the same way, so!!is no longer needed at such a call, and the same now holds at a generic method's or generic class'sChannelReader<T>/ChannelWriter<T>parameter. A nullable channel still does not reach a non-genericChannelReader<T>/ChannelWriter<T>parameter (issue #3992). This is a CLR-boundary rule only: passing achan[int32]?to a G#-declaredchan[int32]parameter still reportsGS0154, and direction and element are still enforced. -
A
map[K, V]reaches aDictionary<K, V>parameter, and a tuple aSystem.ValueTuple<…>one, when an element is still open (issue #3987).map[K, V]ISDictionary<K, V>(ADR-0104) and a G# tuple IS aValueTuple<…>(ADR-0158), and with closed elements the compiler said so — but the rule compared the two sides' CLR types, and an open element leaves neither side with one to read, sofunc makeMap[K, V](entries map[K, V]) MapHolder[K, V] { return MapHolder[K, V](entries) }reportedGS0155against the parameter it already matched. Both spellings are now recognised by shape, in either direction and at any call position. One shape that used to compile becomes an error, because it was emitting unverifiable IL: a tuple whose element does not match the target's type argument, such as(int32, object)passed to a(int32, T)parameter. -
An argument whose type carries a same-compilation or open element no longer binds to a genuine
objectslot (issue #3989). A same-compilation type argument erases toobjectwhile binding — that erasure is what lets[]Pair{…}.Count()reach LINQ — but at the erased level it was indistinguishable from a realobject, soList[Pair]bound to a parameter that genuinely isList<object>and emitted IL ILVerify rejects; the covariant sibling (IEnumerable<object>) went further and threwEntryPointNotFoundExceptionat run time, because CLR variance does not reach a value-type element. Applicability now asks whether this slot'sobjectCOULD have come from erasure — the same question #3982's channel gate asks, with the opposite answer acted on — and refuses the bind when it could not. What the erasure exists to serve is untouched: an inferred generic slot (Count[TSource](IEnumerable[TSource]),M[T](List[T])), a parameter that genuinely isobject, a reference element riding real covariance, and every non-generic target (System.Array,IEnumerable,ICollection,IList,IDictionary) all bind exactly as before. A library that declares anobjectoverload beside a generic one now picks the generic one instead of the overload that cannot hold the argument. -
An erased CLASS surrogate no longer binds to the imported base it erased into (issue #4006). A same-compilation class has no CLR identity while binding, so it is presented as its IMPORTED BASE — and at that level
List[Derived]was indistinguishable from a genuineList<ImportedBase>. Both spellings of the confusion emitted IL ILVerify rejects, becauseList<T>andDictionary<K, V>are INVARIANT, and each reached the parameter by a different route:map[string, Derived]matched a genuineDictionary<string, ImportedBase>by IDENTITY, which #3989's gate declined to question because it only looked for a nestedobject;List[Derived]was not applicable by CLR shape at all, and was instead admitted by the ADR-0148 structural-projection fallback, which asked the projection planner directly and got a nonsense "yes" (twoLists share a parameterless constructor and aCapacity) for a pair the conversion layer refuses outright — so nothing was emitted and the raw list was pushed. One gate now covers both: the erased-argument check derives its surrogate set from the argument itself rather than looking only forobject, and applicability asks it on the structural-projection arm too. Everything legitimate is untouched: covariance still binds (IEnumerable<ImportedBase>, and[]Derivedat anImportedBase[]parameter, since CLR arrays really are covariant), a genuineList[ImportedBase]orDictionary[string, ImportedBase]still binds, an inferred generic slot (M[T](List[T]), and an imported delegate's erasedInvoke) is never second-guessed, and a real ADR-0148 projection at an imported method parameter still binds and emits. The same-compilation ENUM surrogate had no such hole —List[MyEnum]at aList<int>parameter was already refused, and still is. -
A symbolic tuple at an imported generic method's parameter now emits verifiable IL (issue #4000). Inside
func firstOf[T](pair (int32, T)), the callProbes.FirstOf[T](pair)compiled, ran, and printed the right answer while emitting IL ILVerify rejects: the argument was converted against the ERASED parameter shapeValueTuple<int32, object>— boxing the element and rebuilding the tuple — and handed to a call closed over the realT, which wantsValueTuple<int32, T>. The erased-slot recovery that already handles a bareT, aT?, a same-compilation type, a lambda and a channel now handles a tuple too, so the argument is identity and nothing is rebuilt. This holds for the open element in either position and nested one level down. Nothing else changes: a parameter that GENUINELY is(int, object)still boxes and rebuilds, and themap[K, V]sibling of the same call, which was already correct, is unaffected. -
A
map, slice or fixed-length array of a same-compilation class reaches an inferred generic slot when the type arguments are written explicitly (issue #4016).Probes.CountAnyMap[string, Derived](entries)reportedGS0159while theListspelling of the same call bound, and while the very same map call bound when the type arguments were INFERRED. Two projections of one class disagreed: every structural spelling presents a same-compilation class as its IMPORTED BASE (class Derived : ImportedBasearrives asImportedBase), but an explicit type argument closed the candidate over a flatSystem.Objectplaceholder — so aDictionary<string, ImportedBase>argument was ranked against aDictionary<string, object>parameter, which is invariant, and no candidate applied. The placeholder now uses the same erasure the arguments use, so the two sides agree; this is the correction #3087 already made for symbolic TUPLE type arguments. The issue's own boundary was narrower than reported and is corrected on it: inference always bound, a same-compilation class with NO imported base always bound (its erasure was alreadyobject), and the failure was nevermap-specific —[]Derivedand[N]Derivedat aT[]slot failed identically. Nothing is loosened: #4006's rejections stand, so an erased class surrogate still does not satisfy a GENUINE (non-erased)List<ImportedBase>orDictionary<string, ImportedBase>slot in either spelling. -
GS0548(advisory:chan[T]()is a rendezvous channel),GS0549/GS0550(send/receive through the wrong directional handle),GS0554(a channel-receive binding form with the wrong number of targets),GS0555(while let v = chwhere a receive was meant). -
go { … }(ADR-0174 D14): the block form ofgospawns the block as a goroutine, capturing the enclosing locals (per iteration for afor … invariable) —go func() { … }()without the ceremony. -
scopeis completed (ADR-0174 D5/D6): everygoinside a scope reports to the scope's frame (noTaskper goroutine); the block binds an implicitctx(Gsharp.Concurrency.Context) that the first goroutine failure cancels promptly; exit follows the documented precedence table (ScopeExceptionwith the cause first, a failing body rethrown unwrapped) and, inside a suspending function, is awaited rather than blocked. A nested scope'sctxis linked to the enclosing one (ctx.Parent), andreturnfrom inside a scope still counts as returning on every path. A freegooutside any scope is fail-fast on an unhandled exception (GoroutineRuntime.UnhandledGoroutineExceptionto observe). -
A small concurrency library (ADR-0174 D9):
after(d)andtick(d)are selectable timers —case <-after(d)is the timeout arm, and a select receive arm now accepts anything selectable, not only a channel — andmerge(a, b, …)fans several channels into one receive-only channel that closes when the last input does. They live in the implicitly importedGsharp.Concurrencynamespace and are called by bare name; a program that declares its ownafterkeeps its own. -
Batch channel operations and
chunks(ADR-0174 D10). A channel handle gainsTryReceiveBatch/TrySendBatch(aSpan[T], never waits) andReceiveBatch/SendBatch(aMemory[T], can park, and returns aValueTask[int32]youawaitlike any other — issue #3954 — a destination that survives a park cannot be ref-like).chunks(ch, n)reads a channel in batches:for batch in chunks(input, 1024)is ordinary channel iteration wherebatchis aReadOnlyMemory[T]that the receiver owns, so one lock acquisition and one park are amortized across the batch. A batch cut short by a close or a cancellation returns the count it moved rather than throwing, so a retry cannot duplicate elements.GS0562warns when the channel is a rendezvous, where batching is correct but pointless. -
async let(ADR-0174 D15):async let name = exprstartsexpras a child of the enclosingscopeand bindsnameto its eventual result, read asawait name. Both children of twoasync letbindings run concurrently; the binding is a value of typeR, never a handle, so a spawn cannot outlive the block that owns it. A binding you never read has its child cancelled and joined at block exit, and a failure nobody read still reaches the block'sScopeExceptionrather than being dropped; a failing child does not cancel its siblings. New diagnostics:GS0551(no enclosingscope),GS0559(never awaited),GS0569(read withoutawait). -
Three more
selectarm shapes (ADR-0174 D8). Every arm may carry awhenguard, evaluated once when the select is entered; a false guard keeps its arm out of the select entirely, which is how G# spells Go's "set the channel tonilto disable this case".case await taskandcase let v = await tasklet aTaskorTask[T]race the channels on the same waiter.case cancelledreplaces Go'scase <-ctx.Done(): the ambient context's cancellation becomes an arm instead of an unwind. New diagnostics:GS0556(a guard that is not abool),GS0557(acancelledarm with no context to observe, so it would be unreachable), andGS0564(one channel both sent to and received from by the same select). Cancellation also now reaches aselectwritten in a callee with noscopeof its own, through the caller's context. -
selectis rebuilt on one registered waiter (ADR-0174 D8): arms are attempted in uniform-random order like Go's, so two ready arms are both chosen instead of the first one written always winning; a select with no ready arm registers on every arm at once and parks the state machine rather than blocking a thread onTask.WhenAny; and winning is the transfer, so a value can no longer be stolen between choosing an arm and running it. Aselectinside a cancelledscopeunwinds. -
Cancellation unwinds parked channel operations (ADR-0174 D7): a receive, send, or channel loop inside a
scopeparks on that block'sctx, so the first goroutine failure collapses siblings that are waiting on a channel instead of leaving them parked. An operation that already committed keeps its value — cancellation wins only before the transfer. Hosts can retune the budgets and observe the diagnostics throughGsharp.Concurrency.GsharpRuntime(DeferGraceBudget,ScopeStallTimeout,DeferGraceExpired,ScopeStalled). Cancellation crosses calls: a suspending function receives the caller's context as a trailing optional parameter, so an operation inside a callee unwinds with the caller's scope. Declaringctx Contextin a signature uses that parameter instead, and a C# consumer calls the signature as written or passes aContextexplicitly. -
defercleanup is shielded (ADR-0174 D7): a deferred body runs under a cancellation-immune context, so cleanup that drains a channel or signals completion still runs while the block around it unwinds. A grace budget bounds it (GSHARP_DEFER_GRACE_MS, five seconds by default) so cleanup cannot hold cancellation up forever;GsharpRuntime.DeferGraceExpiredreports an abandoned one. Cleanup outside any scope costs nothing. -
Suspension is inferred (ADR-0174 D4): a plain
functhat performs a channel operation, or calls a function that suspends, is compiled as a suspending function — aValueTask[R]state machine labelled[Suspending]— with no keyword, as a fixed point over the call graph. Go-shaped pipelines keep their shape and stop holding threads. Inference stops atasync func,open/override/abstract methods, interface members and their implementations, constructors, accessors, operators, P/Invoke, iterators,Dispose, and function literals; a call to a suspending function from one of those blocks through the runtime's root bridge and warns withGS0558. Every emitted G# method that suspends now returnsValueTask/ValueTask[R]instead ofR— a binary change for C# callers of such functions. -
suspend func(ADR-0174 D4, the declared form): a suspending function compiles to aValueTask[R]-returning state machine on the pooling builder, carries[Suspending], and is awaited implicitly by every G# call site — callers writelet v = take(ch)and seeR; an explicitawait take(ch)is accepted as the same thing. From a function that is neither suspending norasyncthe call blocks the thread andGS0558warns; the entry point is the silent root. Inference of suspension for plainfuncfollows in a later step of Phase 3. -
Debugging through suspension (ADR-0174 P3-8): every async or suspending kickoff carries
[AsyncStateMachine]and[DebuggerStepThrough], soEnvironment.StackTraceand debuggers name the logical function instead of<f>d__1.MoveNext, and the Portable PDB carries the async-method-stepping blob (yield/resume offsets per await) that lets a debugger step over a parked channel receive onto the next source line. Fixed: bodies rewritten by the compiler — everyasync funcbody, and now every suspending one — carried no line information at all, so breakpoints inside them never bound; each source line of such a body now maps into the state machine. -
Channel operations inside an
async funcsuspend instead of blocking (ADR-0174 D4, first step): a receive, two-value receive, send,for v in ch, orwhile let v = <-chin anasync func(or async lambda /async sequence) parks the state machine, not a thread — hundreds of parked receives no longer pin thread-pool threads. A channel operation inside alockbody keeps the blocking lowering (the monitor is thread-affine). -
Observable completion (ADR-0174 D3): the two-value receive
let (v, ok) = <-ch(orv, ok = <-ch) distinguishes a delivered zero value from a closed channel;while let v = <-ch { … }andfor v in ch { … }loop until the channel is closed. Achan[T?]element that isnilis delivered, not mistaken for close. -
Mutual-recursion groups now also work for NON-generic local functions, not only generic ones (issue #4219 umbrella remainder).
let first[T] = func(x T) ... { ... second(x) ... }/let second[U] = func(x U) ... { ... first(x) ... }— two or more consecutive generic local functions calling each other, forward or in a cycle — has compiled since the original #4219 milestone (b84bf800) and its #4221/#4223 follow-ups; the plain (non-generic) spellinglet first = func(x int32) int32 { return second(x) }/let second = func(x int32) int32 { return first(x) }still reportedGS0130("undefined variable") on the forward reference, becauseStatementBinder's consecutive-declaration-group mechanism required a[T, ...]list to opt in. The SAME prepare-then-bind signature-registration machinery (LambdaBinder.PrepareGenericLocalFunctionDeclaration, generalized to tolerate a null type-parameter list) now also groups a run of TWO OR MORE consecutive non-genericlet name = func (...) ... { ... }declarations, registering every member's signature before any member's body binds. A LONE non-generic local function (no sibling to form a run with) is untouched and keeps its existing delegate-valued-variable representation — this is a deliberate compatibility boundary, not an accident: blindly hoisting every ordinarylet/varclosure would change name resolution and mutable-recursion semantics for the overwhelming majority of existing code, so only the unambiguous "two-or-more named local functions in a row" shape opts in. Capturing outer state composes:ClosureEmitter's capturing-direct-call-local closure-dispatch path (GenericLocalClosures, built for #4221's generic case) is widened fromFunction.IsGenerictoFunction.LocalDeclaration != null, so a capturing non-generic group member dispatches through its closure'sInvokemethod exactly like a generic one, and shared/mutated captured state (via the pre-existingCaptureBoxingRewriter) is visible across sibling calls. One narrow, deliberately-deferred limitation: converting a CAPTURING group member's bare name to an explicit delegate value (let cb (int32) -> int32 = first) now reportsGS0126("not a variable") instead of working, because a capturing direct-call local has no single stable closure instance to bind a bare-name conversion to (each direct call it makes materializes its own) — this matches the pre-existing behavior of a capturing GENERIC local function, but is a narrowing relative to the old non-generic delegate-variable baseline for that one shape; a NON-capturing group member's bare-name conversion is unaffected and still works (ExpressionBinder.IsMethodGroupCandidateUsablenow also accepts a non-generic, non-capturing direct-call local). A run mixing generic and non-generic members remains out of scope (unchangedGS0130on the cross-kind forward reference), matching the existing generic/non-generic mixed-group exclusion. -
A
ref-returning function LITERAL is now supported as a direct-call local function (issue #4219 umbrella remainder, workstream B — distinct from #4224's scope, a NAMED ref-returning call/property usable as storage).let at = func(values []int32, index int32) ref int32 { return ref values[index] }declares a local function whose own return kind isref, reusing the named-function ref-return machinery in full (FunctionSymbol.ReturnRefKind, the existingreturn refstatement binder, #4224's alias/storage machinery for a call result, and the existing escape/lifetime checker —var ref alias = at(xs, 0); alias = 42mutates the original storage,let v = at(xs, 0)reads a value, andfunc() ref int32 { var x = 1; return ref x }still reportsGS0254for escaping function-local storage). Aref-returning literal always routes through the direct-call local-function path — standing alone or grouped with another ref-returning sibling — because a CLR delegate'sInvokecannot be made to return by ref without a dedicated synthesized delegate shape; only a non-genericlet name = func (...) ref T { ... }declaration is accepted. Deliberately deferred, and reported as the newGS0588rather than silently accepted or crashing: converting a ref-returning function (literal-backed local OR named) to a delegate/function-type VALUE —DelegateTypeSymbol/FunctionTypeSymbolcarry no return-ref-kind, so the existing method-group/delegate-conversion machinery would otherwise silently narrow a by-ref function to a by-value one instead of rejecting the conversion — and a GENERIC ref-returning local function (let f[T] = func (...) ref T { ... }), whose MethodSpec/signature-encoding interaction with a by-ref return has not been verified. -
A type parameter may be constrained by another type parameter (issue #4043). C#'s
where TDerived : TBasehas a G# spelling —[TBase, TDerived TBase]— that the parser already accepted and the binder then rejected withGS0153: Type 'TBase' cannot be used as a type-parameter constraint because it is neither an interface nor a class. The constraint resolver walked an interface branch, a user-class branch and an imported-class branch and simply fell off the end for a type parameter. All three placements are now bound and emitted: a generic function's own list (func Dependent[TBase, TDerived TBase]()), a generic type's own list (class Pair[TBase, TDerived TBase]), and a generic method's parameter bounded by its ENCLOSING type's parameter (class Box[T] { func Accept[U T](u U) }). The bound projects onto aGenericParamConstraintmetadata row namingVAR(n)orMVAR(n)— the encodingcscemits for the same C# source, measured by reflecting over both outputs — so the assembly IL-verifies. A type argument that does not equal, derive from or implement the argument supplied for the bounding parameter reportsGS0152naming the bounding parameter; where the relation has no closed answer because either side still mentions an unsubstituted type parameter, the instantiation is accepted, since the CLR only loads it once it is closed. A dependent bound deliberately does NOT propagate the bounding parameter's reference-type nature:[TBase class, TDerived TBase]does not makenillegal atTDerived, because the CLR constraint row carries no such implication and the looser reading is the #4027 unverifiable-ldnulldefect one release later. A circular chain ([T T],[A B, B A]) reports the newGS0581, mirroring C#'s CS0454. This was the single compile error the self-migration gate reported fortest/Core.Tests. -
The G# analyzer API gains an operation-block registration and the operation surface the ADR-0193 funnel rules use, and
cs2gsmaps the Roslyn equivalents (issue #4436, ADR-0169).AnalysisContext.RegisterBoundBodyActionruns a rule once per function body with aBoundBodyAnalysisContext(the analogue ofRegisterOperationBlockAction). Bound nodes gainChildNodes,Descendants()andDescendantsAndSelf(). Two analyzer-facing bases,BoundMethodReferenceOperationExpressionandBoundPropertyReferenceOperationExpression, span the user and CLR forms of a method group or property access. Symbols gainGetAttributes(), andFunctionSymbolgainsAssociatedSymbolandMethodKind.cs2gsnow translates the matching Roslyn members: local references, simple assignments, variable declarators,foreachlocals, inlineout var,istests, type/declaration/recursive patterns andtypeof. Previously an analyzer that used them stopped atCS2GS-GAP. Each row is checked by a parity test: Roslyn runs the analyzer over a C# corpus, and the translated analyzer, compiled by gsc, must report the same diagnostics over the translated corpus. Registering several kinds that map to one G# kind (the three Roslyn pattern kinds) now runs the rule once per node instead of once per kind. -
Bound-node analyzer actions now see the same operation tree as operation-block actions (issue #4457).
RegisterBoundNodeActionand translated RoslynRegisterOperationActionrules enter lambda and local-function bodies, visit a local assignment's target before its value, and still treat a plainx is Tas one type-test operation. Nested nodes retain the enclosing member as their containing function, matching Roslyn.
Fixed
-
Property writes through tuple elements update the stored struct (issue #5023).
tuple.Item1.Value = "v"previously changed a copy and silently lost the update. Tuple-element receivers now keep their storage through nested tuples, field and array chains, explicit address/dereference pairs, compound assignments, control-flow right-hand sides, and suspension. Receiver components run once, before the right-hand side; read-only storage remains protected. Native and CLR ref-returning owners are captured once for writes and diagnosed if their managed address would have to survive suspension; non-suspending opaque borrows retain their once-captured address. Capturable array/reference owners stop borrowed-address propagation. Conditional and nested-field borrows preserve the selected storage across suspension without retaining a managed pointer. -
The SDK's assembly-info task and four compiler helpers no longer assert values the C# null-tested (issue #4932). The cut-over turned C# code that passed a value on and then checked it for null into a
!!assertion before the check, so these places threw a NullReferenceException where 0.4 did not:WriteGsharpAssemblyInfoTask, whenOutputFileis a filesystem root andPath.GetDirectoryNamereturns null. The task now skips creating a directory, as the C# did, and fails at the write.ClrTypeUtilities.SafeGetEventIncludingInterfaces,SafeGetPropertyIncludingInterfacesandSafeGetMethodsIncludingInterfaces, given a nil type. They now return nil, nil and an empty array.TypeSymbol.AreRuntimeEquivalentIgnoringReferenceNullability, given a nil operand. Two nils now compare equal, and nil against a type compares unequal.
-
cs2gs translates Oahu again, and its report renders a failed run (issue #4918). The cut-over turned three null-tolerant C# locals in cs2gs's own sources into
!!assertions. Two of them were in the checks that decide whether a constructor statement, or a static constructor's right-hand side, reads the type's own members. They threw a NullReferenceException on the first simple name that binds to no symbol, such as thenameofinnameof(executor). That crashed the translate stage for Oahu'ssrc/Oahu.Cli.App, every project depending on it then failed to compile, and thecs2gs-apps-nightlyoahu leg went red. The third was in the HTML report, where a compile error that carries a line but no file crashedreport.htmlgeneration with the same exception. All three locals are now nullable and checked: an unbound name counts as "not a member", and a location with no file renders without one. -
@ThreadStatic(and every other@annotation) on asharedfield is now emitted (issue #4903). gsc bound the annotation but never wrote theCustomAttributerow for a field declared in asharedblock orshared class, so a@ThreadStaticfield silently became one process-wide slot. The migratedGsharp.Runtime.Channelspooled its per-threadSelectWaiterin such a field, so concurrent selects raced and threwArgumentOutOfRangeExceptionfromCollectGates. The runtime now keeps that per-thread state inThreadLocal<T>until the pinned bootstrap SDK carries the emitter fix, avoids a boxed enumerator and a per-call comparison delegate on the ready-select path (back to zero steady-state allocation), and the runtime test projects now compile against the in-treeGsharp.Runtime.Channels/Gsharp.Runtime.Valuesinstead of the SDK package's copy. -
Per-thread compiler and runtime state is back in
@ThreadStaticfields (issue #4909). With the bootstrap pin on a release that carries the #4911 emitter fix,DeclarationBinder'sconformanceMatchingDepthand theGsharp.Runtime.ChannelsSelectWaiterpool,SelectRandomgenerator andInlineBudgetcounters drop theirThreadLocal<T>workarounds and return to@ThreadStaticsharedfields, which avoids aThreadLocallookup on every select. New tests check that each field is emitted with[ThreadStatic]and really is per-thread, and fail if the SDK package's copy ofGsharp.Runtime.Channelsreplaces the in-tree build under test. -
cs2gs preserves the public/protected ABI of translated C# records (issue #4675). Translated records retain
IEquatable<Self>, non-sealed record classes get their protected virtualPrintMembers, copy-constructor visibility follows record sealing, and getter-only/init-only properties keep their C# metadata and initializer values. Bare positional data-struct declarations retain empty collection zero values without invoking ordinary initializers; explicit construction still runs those initializers exactly once. Omitted positional arguments are prepared before local-slot planning, preserving non-primitive and open-generic defaults in both data structs and data classes. Named constructor expression trees do not acquire anonymous member metadata or ambiguous inherited property lookup; anonymous literals retain their ordered property members. Required managed-reference slots cannot escape through record zero initialization, including constructed generic property storage. Hidden, inherited, and source-interface-constrained expression-tree property bindings retain their actual declaring owner, including constructed generic interface constraints and substituted read signatures. Nested in-type collection-zero helper values are checked only for actual definition-owned field writes, retaining validated ordinary generic constructors without crediting newly required storage. Staged expression-tree lambda arguments retain required quotes.- Ordinary generic constructor initializers are checked at their constructed member types without replacing explicit array elements; positive and unknown allocations cannot leave required handles nil, while constant zero-length allocations remain valid. Explicit positional record properties retain initializer evaluation, default storage values, and private init-accessor metadata rather than being replaced by a synthesized public property.
- Explicit positional auto-properties retain private backing-field visibility and readonly metadata. Semantic parameter names remain distinct from escaped emitted-name reservations; null initializers and properties with escaped positional names keep their independent default storage.
- Positional accessor metadata comparisons use explicit Roslyn enum equality, preserving cs2gs own-source translation without changing record storage or accessor behavior.
- Preserved partial records share semantic primary-constructor ownership; explicit get-only positional properties retain independent default storage. Ordinary generic scalar initializers are checked and credited at their constructed type without crediting absent, default, or incomplete nested values. Unwritten auto-property backing storage receives no constructor credit.
- Nested explicit literals check effective backing storage and actual constructor-owned initializer writes at their constructed types. Initializer functions are validated separately for each constructed owner, so an earlier value-type instantiation cannot hide a later required-handle default.
- Constructor initializer expressions are checked even when a literal later overwrites their results, retaining final supplied-storage coverage without skipping executed calls or array-element checks. Recursive initializer analysis is bounded by constructed owner and initializer identity, so conditional recursive class construction no longer overflows the compiler stack.
- Constructor initializer validation reuses completed checks only for the same constructed owner, construction site, result-storage policy, and state-machine context. Shared-child graphs no longer cause exponential analysis; indefinitely growing generic recursion reports source-anchored GS0604 after 64 active initializer levels instead of overflowing the compiler stack. A focused analysis regression now measures exactly one initializer-body visit per owner/expression in a shared-child graph, independently of the native compiler timeout controls.
- Initializer function bodies retain construction-site diagnostics even when repeated constructions share the same function and owner. Context-aware initializer checks provide deduplication and recursion protection; ordinary function validation remains deduplicated. Closed-generic public-field collection-zero children remain bound and planned when the in-type constructor does not write their storage, including when a private collection sibling selects that constructor. Synthesized nested ordinary-constructor values are checked at their constructed types: an existing initialized handle cannot mask a newly required generic field or property. Private fixed-array zero helpers also check required element initialization. Unsupported closed-generic private-field zero construction fails with GS0472 instead of leaving a null collection or emitting an illegal external field store; real definition-owned private-field stores remain supported.
-
cs2gs hosts an extension on an external receiver on its owner even when the owner has a private nested type (issue #4676). A C# static class with a private nested aggregate used to keep its extension methods as in-owner helpers and publish a public forwarding companion on the package's
<Program>, so the migrated GSharp.Core exported an extra public<Program>type with sevenTypehelpers and the owner's own methods lost their extension marker. Those extensions are now lifted once with their real body and hosted on the owner through@ExtensionOwner, which reaches the owner's private members. Extensions on an owned receiver (a source type in the same namespace, or an enum), and extensions whose signature or attributes name one of the owner's private nested types, keep the previous scheme (a method whose signature names a private type cannot be public API; one that only has such an attribute keeps the in-owner helper and its forwarding companion, which leaves off only the attributes that name the private type). A bound method group of an async extension (Func<Task<int>> f = "x".M;) now reaches a gsc gap that the old scheme hid (#4699). -
cs2gs preserves named BCL delegate identity (issue #4679).
Predicate<T>and other non-Func/Actiondelegates retain their names in type positions, includingtypeof, casts, type patterns, inferred types, and nested generic arguments, instead of silently becoming an incompatible structural delegate. Imported nested delegates also retain constructed owners (Outer[string].Callback[int32]); source-declared nested delegates keep their lifted top-level names. The independent compiler lookup failure for escaped nested CLR type names is tracked in #4754. -
Lifted value-to-reference conversions now preserve nil and skip the operator (issue #4741). A nullable
Token?converted through an operator takingTokenand returningBoxpreviously called the operator on the default token and could return a non-null box for nil. ExplicitBox(value)/Box?(value)and contextual implicit conversions into nil-accepting targets now evaluate the operand once, bypass the operator on nil, and call it once on present input. The effective reference result is nullable, so non-nullable destinations remain rejected. Source-declared G# and imported CLR operators that explicitly accept a nullable operand take precedence over the lift, actually run on nil, and retain their own result contracts. Imported nullable-value operands now match their actualNullable<Token>parameter instead of its erased underlying type; remaining imported nullable-value signature gaps are tracked in issue #4737. -
Constructor tuple and multi-assignment preserve
thisfor getter-only auto-property stores (issue #4734). Assigningthis.First, this.Second = (3, 7)now emits verifiable readonly backing-field stores in classes and works in structs without looking for nonexistent setters. The backing fields remaininitonly; other receivers and index arguments still capture their locations before the right-hand side, and readonly assignments outside the declaring constructor remain rejected. -
gsgen projects semantic class abstractness consistently with emitted metadata (issue #4674). Generators now see
IsAbstractfor classes with unimplemented declared or inherited abstract members, not only an explicitabstractmodifier. Generated partial parts retain their own explicit modifiers rather than making temporary stub abstractness permanent, preserving instance Regex generation and source constructor visibility. Shared classes continue to project as C# static classes. Constructor projection also preserves actual access and signatures: inferred-abstract implicit constructors stay public, declared-abstract implicit constructors stay protected, primary constructors retain their real overloads, and forwarding primaries gain no phantom parameterless constructor. Shared classes do not expose the emitter's private placeholder constructor to C# generators. cs2gs reports a retained shared-class mapping only when that declaration survives translation; an extension-only holder whose methods move into their owning type no longer gets a misleading retention diagnostic. Generator-authored abstract modifiers also survive the generated partial data-part rewrite, whether the C# stub projects the data class as a record or as a class. -
cs2gs preserves parenthesized iterator values (issue #4705). Expressions such as
yield return (x + y) * zand string literals that can become spliced concatenations now use a typed, collision-safe local immediately beforeyield, avoiding G#'s tuple-yield ambiguity without changing deferred evaluation. The local uses the same nullable-promotion rules as the iterator element. The decision uses expression nodes rather than printer text, and source identifiers are collected once per syntax tree. -
A G# class implicitly converts to interfaces inherited through an imported .NET base (issue #4731). The same nominal relation now governs binding and emission, including intervening G# bases, substituted generic arguments and interface variance through sequence aliases in either direction. Emission retains the actual element type at imported generic calls instead of casting to their erased reflection envelope. Nested invariant generic arguments remain distinct through sequence aliases and annotation carriers rather than collapsing to the same erased CLR shape. Closed imported value wrappers retain genuine interface covariance when boxed, while symbolic wrapper arguments cannot bypass the shared relation through erased CLR probes. CLR argument conversions target the actual substituted method signature, including boxing symbolic value wrappers, rather than their erased parameter probes. This preserves existing CLR reference-annotation applicability and the natural
Action/Funcidentity inside constructed arguments, while rejecting genuinely incompatible reified slots before emission. Constrained instance and static calls retain inferred method arguments through the receiver-aware parameter reader, and imported map-interface arguments retain their invariant key/value relation and existing nil policy. A CLR-inferred method closure does not invent non-null reference annotations for an unrecovered symbolic method slot, including partially recovered vectors and invalid slot positions; recovered nullable, non-nullable, explicit and receiver-projected contracts remain authoritative. Symbolic sequence and async-sequence boxing, unboxing and checked casts share interface recognition and reified signature-based opcode tokens. Implicit and checked conversions preserve reference identity and inherited interface dispatch without structural adapters or redundant interface declarations. Sealed classes stay sealed, and unrelated sealed-class conversions remain rejected. -
cs2gs promotes null-bearing tuple elements in iterator signatures (issue #4718). A conditional yield such as
choose ? (text, 1) : ("x", 2), withtextinitialized to null, now declaressequence[(Text string?, Code int32)]instead of promising a non-nullText. Iterator methods, property/indexer getters and synthesized yield locals use the same tuple-position evidence; leaves proven non-null by a guard remain non-null unless an intervening or loop-carried write, unstructured jump, writable capture (including an enclosing-body write between resumptions), or escaping writable reference invalidates that proof. Read-onlyinarguments and aliases preserve the proof. Source interface and override contracts whose return declarations contain matching tuple slots, including generic methods, async iterators, properties/indexers and other implementations of the same slot, stay synchronized through the same nested tuple paths. Positional record parameters and their synthesized properties share that evidence too. Constructed generic interface/base arguments and their other uses, including sibling-project references, share the same tuple-position evidence; reordered and nested arguments preserve their actual positions and source tuple labels (issue #4727). Covariant shapes do not borrow potentially reordered paths. Unaffected elements and runtime evaluation stay unchanged. -
A non-
openclass may declare aprotected override, and cs2gs keeps the inheritance shape of public C# classes (issue #4674).protectedon a member of a non-openclass is stillGS0380, except on anoverridemethod, property or event, whose accessibility the base member dictates; asealed class Lowerer : BoundTreeRewritercan now keep overriding the base's protected hooks while staying CLR-sealed. cs2gs used to seal every class with no in-project subclass, which silently sealed public types that other assemblies (analyzers, for GSharp.Core) derive from. It now emitsopenfor every C# class that is notsealedand is reachable from another assembly, and keeps a C#sealedclass (including a sealed record, whose synthesizedprotectedmembers never neededopen) non-open. Migrated output therefore gainsopenon public non-sealed classes in every project. -
A library no longer exports an empty public
<Program>type per package, and a lifted nested delegate keeps the reach of its container (issue #4676). Every package of a G# assembly gets a synthesized<Program>host type that carries its top-level functions, and it was alwayspublic, so a library of types only exported one empty public host per namespace (22 in the GSharp.Core built from its G# translation, none in the C# build), visible to reflection-based tooling. The host is nowpubliconly when it carries a public top-level function or the compilation has an entry point (an executable keeps every host public); otherwise it is emitted non-public. Function literals and local functions hosted there do not count. cs2gs lifted apublicdelegate nested in aninternalclass to a top-levelpublicdelegate; it now emitsinternalunless the delegate and every type enclosing it are public. Aprotectedorprotected internalcontainer also bounds the lifted delegate tointernal, preserving legal inside-owner and derived uses without exporting a globally public type. -
A ref-returning local function that captures outer state no longer emits a program that crashes with
AccessViolationException(issue #4580). gsc emits a capturinglet At = func (i int32) ref int32 { return ref data[i] }as theInvokemethod of a closure class. That method was declared to return by value while its body returned a managed pointer, and the caller stored the result as one, so writing through the alias crashed the process.Invokenow carries the literal'sref/ref readonlyreturn, for captured locals, parameters,thisfields, nested literals and literals inside struct members.- A ref-returning literal inside a struct member that returns a reference into
thisnow reportsGS0254. The closure holds a copy ofthis, so the reference would alias the copy, not the caller's struct. Before, an@UnscopedRefmember letreturn ref this.ncompile, along with forwarding forms such asreturn ref this.Slot(), and writes through the reference were silently lost. Remedy: return a reference into heap storage (an array element or a class field), or make the literal a member function.
- A ref-returning literal inside a struct member that returns a reference into
-
cs2gs no longer adds redundant empty-array allocations (issue #4684). An empty collection expression targeting a rank-one array (
T[] a = []) translates toSystem.Array.Empty[T](), as csc lowers it. Pointer and function-pointer elements retain non-generic literals; function-pointer arrays preserve their exact element signature in allocation metadata. A bare non-nullable class instance array field gets the cached initializer only when every constructor is proved to overwrite it before any read or escape, on a sealed, object-based type without a finalizer. Other fields retain their original fresh zero value and identity. This removes both throwaway arrays from the migratedManagedLocationKey, restoring 40 bytes per key instead of 88 without changing constructor assignments or explicit array allocations. -
Synthesized names and orderings in
/deterministicemit no longer depend on object identity (issue #4663). Several lowering passes numbered synthesized types while walking the program in an order derived from object identity: capture boxes (<>__Box_<name>_<n>), iterator and async state machines of same-named methods, managed-location helpers and interpolation temporaries. Adding an unrelated file, reordering files within a package, or building with a differently-built compiler renumbered them, so the emitted assembly changed. Synthesized names now follow the established MethodDef source key, with structural package, nested type and overload identities breaking ties. The<Module>initializer calls[ModuleInitializer]functions in that same deterministic order instead of an unspecified one. This does not make the assembly independent of input order in general: packages are kept in first-seen syntax-tree order and emit assigns each package'sMethodDefrows in that order, so reversing files that belong to different packages can still change the assembly. -
cs2gs keeps each C# file's header comment (issue #4653).
- Before: a license or copyright block at the top of a C# file disappeared, because it sits before the first
usingornamespacerather than on a declaration. - Now: it opens the translated G# file with its text unchanged except that trailing whitespace is trimmed from each line and line endings are normalized to
\n, followed by a blank line and thepackagedeclaration. A file split into several packages repeats it in each. - The rule: the header is the run of
//and/* */comments at the start of the file, ending at the first blank line, preprocessor directive, documentation comment or token. Comments directly above a first declaration still stay on that declaration.#region,#pragma,#nullableand other preprocessor lines are still dropped.
- Before: a license or copyright block at the top of a C# file disappeared, because it sits before the first
-
A type can now implement the static abstract and static virtual members of an imported .NET interface (issue #4614). The runtime never matches a static interface member by name, and gsc bound static interface members only for G#-declared interfaces. So
struct SD : IS { shared { func Z() int32 { ... } } }over an importedinterface IS { static abstract int Z(); }failed to load with aTypeLoadException, asharedmember overriding a static virtual's default was ignored, andclass DZ : BaseS, IS2, where the importedBaseSalready implementsIS, silently ranBaseS.Z. gsc now binds each such member, including static properties, static events and members inherited through a base interface, to the type's static member of the same name and signature, as the C# compiler does, except that the member may have any accessibility (C# requires an implicit implementation to be public; G# already applies this rule to its own interfaces). An explicit-interface member such asfunc (IB) Z()no longer counts as an implementation of another interface'sZ, for G# interfaces as well. A type that declares onlyfunc (IB) Z()while also listingIAwith a staticZnow reportsGS0331; before, the explicit member was silently bound toIA.Ztoo. -
A type that lists an imported .NET interface now also declares that interface's base interfaces, so interface dispatch reaches its members (issue #4601). The runtime re-maps only the interfaces a type itself lists. gsc listed the declared interface alone, so in
class Derived : Base, IDerivedI, where the importedBasealready implementsIDerivedI's baseIBaseI, aValuedeclared onDerivedwas silently ignored when called throughIBaseI: the program ranBase.Value. The same happened forCollection[int32], IList[int32]with anICollection[int32]member, and for diamond and generic base interfaces. gsc now emits the full interface closure, as the C# compiler does. Every G#-compiled type that implements a .NET interface with base interfaces gets additionalInterfaceImplmetadata rows. -
The self-migrated cs2gs translator no longer throws while building an anonymous function's control-flow graph (issue #4611).
CreateControlFlowGraphappended each block's possibly-null branch value to its operations and filtered the null afterwards. The migration bridges a maybe-null value entering generic storage with a fail-fast!!, so the migrated translator threw on the first block without a branch value, failing 16Cs2Gs.Testsin the self-migration. The translator now yields the branch value only when it is present. -
Computed property accessors are emitted as
SpecialNamemethods (issue #4589). gsc set the CLRSpecialNameflag only on auto-property, abstract, interface and indexer accessors, so theget_/set_methods of computed properties, explicit-interface properties andsharedproperties looked like ordinary methods to reflection, and gsc's own reference-assembly output (which already set the flag) disagreed with the implementation assembly. Every property accessor now carriesSpecialName, as C# emits it, so tools that recognize accessors throughMethodInfo.IsSpecialNametreat them as accessors. G# code that references a G#-compiled implementation assembly no longer sees thoseget_/set_methods as callable methods, which matches C# libraries and gsc's reference assemblies, and@UnscopedRefon such a computed property is now honored. -
A class may re-list an interface its imported base class already implements (issue #4593).
class BuildTask : Task, ICancelableTask(MSBuild'sTaskimplementsITask, whichICancelableTaskextends),class Bag : Collection[int32], IList[int32]andclass S : MemoryStream, IDisposableused to reportGS0187for every interface member the base supplies, such asITask.BuildEngineorIDisposable.Dispose. As in C#, the base's implementation now satisfies those slots at run time; members the class declares itself are still matched and checked. Re-listing an interface the base does not implement still requires every abstract member. -
A tuple over an imported class now has a usable CLR type, so arrays of it convert to
IEnumerable[...]and infer generic arguments (issue #4591). The compiler closed the hostValueTupleover a type loaded from the reference assemblies, which produced a type nothing could reflect on.let e IEnumerable[(int32, Type)] = xswas an internal compiler error (GS9998), andAssert.Single(calls)over[](OpCode, Method MethodBase?)silently picked xunit's non-genericobjectoverload, so the followingcall.Item1reportedGS0158. The tuple is now built in its elements' own load context, including theNullable<T>of a nullable value-type element such as(int32, DateTime?). This was a compiler bug, not a cs2gs one; it blocked the self-migratedtest/Compiler.Tests. -
cs2gs preserves typed null casts through parentheses (issue #4721).
(string)(null)now emits the samedefault(string)as(string)null, instead of an unbindablestring((nil))conversion. Nested parentheses, other reference targets, and nullable value targets use the same typed-null path; non-null operands retain their existing conversions. -
cs2gs no longer synthesizes an import for a type it maps only to analyze an array receiver (issue #4556). The managed-reference array analysis maps a receiver's array type to inspect its element shape, and that mapping recorded the element's namespace as an import even though nothing from it is printed. In self-migrated Core.Tests, a
ParameterInfo[]receiver importedSystem.Reflection, so the file's bare compilerBinderbecame ambiguous withSystem.Reflection.Binder(GS0547). Analysis-only mappings now leave the import set alone; a printed reference still adds its import. -
cs2gs asserts a
foreachbinding over a source iterator whose element it promoted to nullable (issues #4578 and #4577). In a nullable-oblivious project, an iterator that yields a null-tainted value renderssequence[T?], so G# infersfor x in Iterator(...)asT?. cs2gs still treated the loop binding as non-null and emittedx.Member,F(x)and lambda captures such as.OfType[T]().Where((s T) -> s.SpanStart > x.SpanStart)with no!!(GS0158, GS0154; gsc reported the failed lambda as "Cannot find function Where"). The binding now asks the same promotion question the iterator's own declaration asks, so each non-null use gets the!!that matches C#'s throw-at-dereference behavior. -
Generic inference now finds interfaces inherited through an imported type's direct interfaces (issue #4559). A G#-emitted assembly lists only a type's direct interfaces, so
First(ProjectedCollection[int32]())over a class declaring onlyIReadOnlyCollection[string]failed with "Cannot infer type argument 'T'" instead of inferringT = stringthroughIEnumerable[string]. Inference now walks interface inheritance transitively, including through base classes. Conflicting interface projections are still rejected. -
@UnscopedRefis recognized when a project builds against reference assemblies (issue #4584). gsc compared the attribute's assembly identity only with the host runtime's (System.Private.CoreLib), so an SDK build, which binds theSystem.Runtimereference assembly, ignored a genuine@UnscopedRefand reportedGS0589on a struct member that returned a reference to its own state. The BCL identities (System.Private.CoreLib,System.Runtime,mscorlib,netstandard) are now accepted by name and public key token; a same-named assembly with a different key is still rejected. The same recognition covers@ScopedRefandRefSafetyRules, so in SDK builds imported APIs, including third-party libraries built for .NET 7 or later, now follow the C# 11 ref-safety rules (and explicitscoped) instead of being treated as legacy, which can change which ref-safety diagnostics appear on calls to imported members. -
cs2gs preserves C# dereference and concatenation behavior for nullable conditional, switch, and coalescing values (issue #4555). A receiver such as
(argument.Value is Type type ? type.FullName : argument.Value).ToString()now emits one G#!!assertion instead of a migrated call that gsc rejects withGS0159, and the same value used as a string-concatenation operand converts with the null-safe?.ToString()so nil concatenates as the empty string. Already asserted and statically non-null receivers remain unchanged. -
cs2gs no longer emits
__local_{owner}_{name}lifted-local helpers (issue #4302). Consecutive homogeneous recursive groups that use generics,params,ref/out/in, or static non-generic, non-ref readonlyref returns now remain nativelet Name = func ...declarations under their source names. Optional-parameter helpers reached only from a nullable recursion group fold into that group, with omitted defaults materialized at calls. The compiler's deliberately unsupported mixed generic/non-generic group still uses a member helper to preserve valid C# behavior, but the helper keeps the source name and adds only a short collision suffix. The self-migrationliftedLocalCeilingis ratcheted from 3 to 0. -
cs2gs no longer emits
__deconNidentifiers for nested tuple deconstruction (issue #4300). G# tuple bindings are flat, so nested declaration and assignment shapes still use recursive carrier locals, but those carriers now derive readable names from their source leaves, avoid collisions within each translated body, and do not drift when unrelated methods are inserted earlier in a file. Simple immutable declaration leaves bind their actual names directly, while structural or mutable leaves retain only the carriers their semantics require. Nested declarations now route through that recursive lowering instead of silently discarding inner bindings. The real migration pipeline gates nested declarations and assignments, mixed existing/new/discard leaves, all-discard arms, value-position assignments, storage-target evaluation order, compilation, IL verification, and runtime parity. -
Language-server
TryGetwrappers now preserve their non-null success contract (issue #4560). The migrated server now narrows document content after a successful lookup instead of retainingDocumentContent?and rejectingcontent.SyntaxTreewithGS0158; failed lookups still leave the output nullable. -
cs2gs uses G# discard parameters for parameterless C# anonymous methods (issue #4297). A C#
delegate { ... }converted to a delegate with parameters now emits repeatable, non-referenceable_slots instead of__anonNnames. The targetInvokesignature still determines arity, types, and ref kinds, while captured outer locals — including names such asobj,__anon0, and reserved spellings — cannot be shadowed by parameters the C# body could never name. -
Value-type nullable signatures no longer match their non-nullable underlying type (issue #4446). Overrides and interface implementations now reject
int32?where the slot declaresint32, and hierarchy-projection inference keepsIBox[int32]andIBox[int32?]ambiguous instead of collapsing them. The distinction also holds inside arrays, tuples, constructed types, user structs, enums, andstruct-constrained type parameters. Reference nullability remains signature-compatible where conformance rules require it, and exact nullable pairs remain equal. -
cs2gs preserves Roslyn's maybe-null type for locals inferred from
default(T)(issue #4445). Direct locals, aliases, conditional and coalescing flows, tuple declarations, whole-tuple defaults, declaration expressions, nested deconstruction, and loop clauses now emitT?when Roslyn marks the inferred local maybe-null, so passing that storage toref T?no longer producesGS0612. Value-type substitutions, explicit local types, non-null initializers, discards, anddefault(T)!keep their previous forms. -
Generic method inference through a base type or interface preserves nested platform and nullable positions (issue #4479, ADR-0186 §3).
Ob.Strings().Select(s -> s)over an oblivious C#List<string>now infersTSource = string!, and a G# generic takingsequence[T]infers the same element instead of erasing it tostring;[Nullable(2)]elements likewise remainstring?. Exact-definition inference, non-null and value-type elements, variance, and ambiguous hierarchy projections are unchanged. -
Concrete arrays and value tuples reached through an imported receiver keep nested declared nullability (issues #4402 and #4449, ADR-0193).
Counter[T].Add(T, KeyValuePair[string, object?][])no longer erases the nullableobject?tag value, and an oblivious concretestring[]is consistently a platform array of platform elements ([]!string!) whether read from a field, property, method return, parameter, or indexer. Unannotated netstandard2.0 positions such asFile.ReadAllLinesandTaskFactory[T].ContinueWhenAll(Task[])retain both positions. Positions containing type parameters still preserve receiver substitution, including value-type arguments. -
Nested attribute arrays retain their static element type. A
[]Typenested in an[]objectargument and containingtypeofof a same-compilation type now reaches metadata and source generators asType[], rather than being exposed as the compiler's internalobject[]placeholder (issue #4369). -
cs2gs preserves definition-side defaults when a C# partial method falls back to one implementation (issue #4398). When different file imports, parameter names, generated parts, or another incompatibility prevent a G#
partial funcpair, the surviving implementation now inherits the defaults C# callers observe from the definition. Defaults are spelled in the implementation file's scope, while its parameter names, attributes, signature, and body remain unchanged. -
cs2gs keeps C# top-level statements able to reach members of a retained
Programclass (issue #4472). WhenProgrammust remain a class because it contains a nested aggregate, exports a member, is preserved for a referencing project, or declares aGeneratedRegexmethod, private members reachable from C# top-level statements are emittedinternal, and nested type references are qualified throughProgram. Private-only programs still use the unchanged canonical hoisted output. -
Captured locals and parameters whose types are private nested types no longer fail assembly loading (issue #4526). Capture boxes are now emitted inside the declaring type's CLR access domain, including generic and deeply nested generic owners, while preserving the complete enclosing generic-parameter vector. Managed-location helpers nested in generic owners likewise retain their enclosing and helper-specific generic arity. This fixes migrated code that compiled successfully but threw
TypeLoadException: Access is deniedwhen a lambda captured a private nested record struct. -
Composite literals and source indexers now enforce accessor accessibility before emit (issue #4474).
Source{ P: 1 }now checksP's setter rather than the property's broader visibility, and source indexer reads and writes now check the getter or setter they invoke. Unrelated code receives the establishedGS0472/GS0379diagnostics for private/protected accessors; declaring and derived contexts remain allowed, subject to the existing protected-receiver rule. -
@LibraryImportpreserves native-transition attributes (issue #4410). Method-level@SuppressGCTransitionand@DefaultDllImportSearchPaths(...)now move from the managed outer stub to the hidden inner P/Invoke where the runtime observes them.cs2gstranslates the corresponding C# attributes instead of reporting them as unsupported. -
Writes through nested fields of a constructed generic struct now persist inside closures (issue #4384). A lambda assigning through
w.C.NwherewisWrap[string], or through the same generic struct chain beneath a captured class field, failed withGS9998because emitter addressability checks looked for the construction-substituted field symbol in a cache keyed by the open definition. Field storage is now classified from its receiver chain, while the existing generic-aware token resolver maps the field back to its definition, so the compiler emitsldfldarather than spilling and mutating a copy. -
A property override whose type differs from the base property's is checked, and a covariant override is read through its own accessor (issue #4481). The emission of a C# 9 covariant property override (a MethodImpl row and
PreserveBaseOverridesAttribute) is #4508's fix; this change covers what surrounds it. An override whose type is unrelated to, or wider than, the base type used to compile silently and now reportsGS0185. So does a narrowing that has no CLR covariant form: a narrowing with a setter, over a base property that has a setter or no getter, or by-ref. A difference in reference nullability alone, at any nesting depth (List[Sym]overList[Sym?]), is still the same slot. The narrowing test now runs after every type, interface base clause and interface closure is bound. Before, a narrowing to a type declared later in the compilation, such asPropertySymbolin the migratedGSharp.Core, or to a source interface, was wrongly rejected. A read of a covariant override on a receiver whose chain mixes generic and non-generic classes (GenericLeaf[int32] : Access : Node[Sym]) used to call the base's accessor, which returns the wider type, and failed IL verification. It now calls the accessor of the class that declares the bound property. -
cs2gs no longer silently drops property or indexer accessor attributes (issue #4294, ADR-0184). An unsupported attribute on a
get,set, orinitaccessor now emits the non-fatalCS2GS-ACCESSOR-ATTRIBUTE-DROPPEDwarning to stderr and the run'stranslate.log, naming the source location, instead of disappearing without notice.[UnscopedRef]on a getter keeps its existing, semantics-preserving hoist to the G# member; no broader attribute mappings are inferred. -
Taking a
suspendmethod as a delegate now emits a context-capturing thunk (issue #4412, ADR-0174 D7). Direct instance,base.M, imported, structural-function, and named-delegate conversions no longer bind the delegate directly to a MethodDef whose hidden trailingContextparameter is absent fromInvoke; the generated delegate now passes IL verification and carries the ambient context from its conversion site. A declared or inferred suspending method with aref,out, orinparameter now reportsGS0422instead of emitting a state machine that copies an address into a value field. -
A
suspendmethod can now override a G# base method imported from another assembly (issue #4413, ADR-0174). Override matching now compares the source-visible parameter and awaited return types instead of counting the hidden trailingContextparameter and physicalValueTaskreturn as part of the G# signature. The emitted override keeps that physical ABI and an explicit MethodImpl, so virtual dispatch andbase.M()work across the assembly boundary. -
String indexing, string length, and rectangular-array length now match
csc'scallvirtopcode choice (issue #4313). The three specialized emitter paths used non-virtualcallforString.get_Chars,String.get_Length, andArray.get_Length, unlike other reference-receiver member accesses. Valid non-nil operations return the same values, and the emitted IL now has C#'s explicit null-checking instruction semantics without promising a particular stack-frame presentation after JIT inlining or tiering. -
cs2gs method-group wrappers now keep source-derived parameter names through collisions and reserved words (issue #4298). Instead of replacing such names with unrelated
__argNidentifiers, the translator uses the established underscore suffix (value_,value__,params_); a synthetic fallback remains only when no usable source name exists. -
IL verification no longer fails valid code that has a
catch … whenfilter inside afinallyblock (issue #4489). The repo-pinneddotnet-ilverify10.0.8 reports[StackUnderflow]at the first instruction of an exception filter whose start lies inside afinallyorfaulthandler (upstream dotnet/runtime#134711). Roslyn's IL for the same C# fails the same way, and the code runs correctly. This kept the nightly self-migration red ontest/Core.Tests. The cs2gs ilverify stage and the Compiler.TestsIlVerifiernow drop that one error. They drop it only when the verified assembly's metadata shows that the reported offset is a filter start and that the innermost handler enclosing it isfinallyorfault.StackUnderflowis not ignored as a category, so every other reported stack underflow still fails verification. Known limitation: ilverify stops checking a method at its first error, so in a method where this error is dropped, the IL after the filter start is not verified until the upstream fix lands. -
Capturing rich objects now reuse one managed capture location per dynamic lexical binding (issue #4512, ADR-0189). Constructing another rich object over an existing mutable capture now allocates only the fresh object, while separate loop iterations and factory calls still create separate cells and locations. Snapshots, explicit managed aliases, nested/generic captures, pre-base initialization, and rich-object identity are unchanged.
-
Array and native-slice
managedlocations no longer allocate their identity key until equality or hashing observes it (issue #4511, ADR-0188). Immediate-use, retained, and directly-created readonly element handles now allocate one 40-byte handle instead of the previous 80-byte handle-plus-key graph on the recorded 64-bit profile. The handle still retains its selected owner immediately; first identity observation safely publishes one immutable key per handle, and warmed identity/dereference operations allocate zero. -
cs2gs widens by-ref storage along with a promoted
outorrefparameter (issue #4482). When cs2gs widens an obliviousout/refparameter toT?, gsc now requires the storage passed to it to match (GS0612). An array element passed byout(new string[n]thenout a[i]) now makes the array[n]string?, and a local passed byrefwidens the same wayoutlocals already did. -
cs2gs emits managed-reference array types with nullable elements (issue #4525). C# arrays of
ManagedRef<T>andReadOnlyManagedRef<T>now usemanaged[T]?elements in allocations, declarations, fields, parameters and returns, including rectangular arrays. Explicit, implicit, rectangular and collection-expression initializers preserve nil elements without inserting!!; element reads,foreachbindings and generic results receive!!only when consumed by a non-null operation without a G# null guard. This matches CLR default initialization and prevents both gsc'sGS0604rejection and translator-introduced null-reference exceptions. Fixed generic storage that cannot adopt the projected nullable element now reports an Unsupported diagnostic instead of inserting!!. -
cs2gs no longer asserts a conditional or switch arm where C# takes a null as a value (issue #4452). An arm feeding an interpolation hole, a string concatenation, the operand of
as, or a switch scrutinee used to get!!, and it threw where C# formats, casts or matches the null. A C#!into a local that cs2gs widens toT?is now erased too, instead of becoming a!!. -
A nullability-oblivious value passed to an imported method's non-null parameter is now checked at the call (issue #4451, ADR-0186 §4). A
string!argument to a C# method, constructor or constrained interface call whose parameter is non-nullstringwas passed through with no check, because both sides are the same CLR type. Such an argument is now checked, and a nil throwsNullReferenceExceptionat the call, naming the expression. This is a runtime change: code that passed a nil into an enabled C# parameter now fails there. A parameter the callee leaves oblivious or declares nullable (string?) still accepts a nil unchecked.- Explicit type arguments at an oblivious generic (ADR-0193 amendment). The reported shape, an oblivious
List<T> WrapList<T>(T value)called asOb.WrapList[string](x), now bindsTto the platform typestring!: the call returnsList[string!]and takes a nil unchecked, as the C# does (Ob.IsNil[string](nil)returnstrue). A nil element read through the list is checked where it is used. Before,Twasstring, so a nil entered a non-nullList[string]and failed later with an unattributedNullReferenceException.
- Explicit type arguments at an oblivious generic (ADR-0193 amendment). The reported shape, an oblivious
-
A container with nullability-oblivious elements no longer converts to a supertype that promises non-null elements (issue #4420, ADR-0186 §3 rule 3).
List[string!]toList[string]was already rejected, but the same container passed as anIEnumerable[string]or anIReadOnlyList[string], or an obliviousstring[]passed as one, was accepted. The receiver then read a nil element as a non-nullstring, with no check anywhere. These conversions now reportGS0154orGS0155at G# conversion sites; imported CLR call parameters remain a known exception tracked by #4480. A view whose element is nilable (IEnumerable[string?]) and a non-generic one (IEnumerable,object) still convert. To pass the elements as non-null, copy them through a checked conversion, for example into aList[string]element by element. -
An object initializer on an imported type now finds a friend assembly's
internalproperty or field (issue #4468).Options{Hidden: 1}reportedGS0158 Cannot find memberfor aninternalmember of an assembly that named this compilation in anInternalsVisibleTo, althoughoptions.Hidden = 1bound. Onlyassemblyaccessibility is admitted, as on the assignment path;private,protectedandprivate protectedmembers stay hidden, and a non-friend assembly still sees none of them. This kept the migratedtools/cs2gs/Cs2Gs.Testsfrom compiling in the nightly self-migration. -
An async function's physical
TaskorValueTaskreturn now keeps every nullability position in metadata (issue #4465, first exposed while fixing #4287). gsc originally wrote no nullability for an async kickoff return and relied on the enclosing default, which could make another assembly readTask[Rep]asTask[Rep]?or lose a nestedRep?. The first fix made every referenceTaskenvelope non-null, which still laundered an@Oblivious asyncdeclaration's platform top level. Signature and metadata emission now share one physical return symbol: an enabledTask[string?]re-imports asTask[string?], an oblivious one asTask[string?]!, and suspending or explicitlyValueTaskfunctions retain the corresponding value-task layout. Non-generic tasks, value-type results, tuple names, and return attributes are unchanged. -
Three more positions now check an oblivious
T!value with the attributed nil message, and named deconstruction accepts one (ADR-0186 §4, issues #4323, #4324, #4325). An audit of every place a platform value can appear as a receiver or value found three gaps:- Invoking a delegate-typed field or property.
x.F(),Type.F(), or a field of an@ObliviousG# class used to throw an unattributedNullReferenceExceptionfrom inside the invocation when the delegate was nil. Invoking a local (f()) or a parenthesised callee ((f)()) already named the source. - An
await forsource. A nil obliviousIAsyncEnumerablenow fails with the same message as afor … insource. - Named deconstruction.
let { X = x } = obliviousCall()reportedGS0164where the same code on a plainTcompiled. It now compiles and checks its source, as positional deconstruction already did.
The other positions checked are index reads, writes and compound writes; member reads, writes, compound and
??=writes;await;lock;throw; returns; and positional deconstruction. They already behaved correctly, and a regression test now covers each of them. Positions that accept nil insert no check: interpolation, string concatenation,using, aswitchscrutinee, andas. - Invoking a delegate-typed field or property.
-
A by-reference argument may now differ from its parameter only in reference nullability, as in C#, with the new
GS0612warning (issue #4422).&base.runstackat aref []int32parameter, where the reference pack declaresRegexRunner.runstackasint[]?, was rejected withGS0154. That blocked the[GeneratedRegex]runner for any regex that backtracks inside a loop.[]?int32and[]int32, orList[string?]andList[string], are one runtime type, so the storage is now accepted.GS0612warns in the direction a nil can flow through the shared storage: any difference forref, nullable storage at a non-nullin, a nullableoutparameter over non-null storage, and any nested difference. A platform (T!) position never warns. The same-compilation shapes (&swiths string?) used to compile with no diagnostic at all; they now get the warning too. Breaking: a value-typeint32?isNullable<int32>, not an annotation, and passing its storage toref int32(or[]int32?toref []int32) is nowGS0154. It used to compile, and the callee's store overwrote theNullable'shasValuefield. The same holds for imported callees:Interlocked.Increment(&x)orInt32.TryParse(s, &x)withx int32?compiled to unverifiable IL and is nowGS0154. Overload resolution follows the rule too: withP(ref int32)andP(ref int32?),int32?storage selectsref int32?, andstring?storage prefers an exactref string?overref string.@SuppressDiagnostic(ADR-0175) now also suppresses compiler warnings such asGS0612(never errors) in gsc and the language server, and cs2gs emits@SuppressDiagnostic("GS0612"), scoped to the one statement, where it drops a C#!fromref x!, so the[GeneratedRegex]runner builds with no warning.GS0612reads declared types rather than C#'s flow state, so it can fire where csc is silent. -
Events of a generic class now work (issue #4391). For
open class GB[T] { event Got EventHandler[T]? }, the generatedadd_Got/remove_Gotcast the combined delegate toEventHandler<object>. They failed IL verification, and the first subscription threwInvalidCastException. They now cast toEventHandler<T>, like C#'s field-like event accessors. Subscribing through a constructed receiver (g.Got += hon aGB[string]), or by bare name from a class derivingGB[string], now takes anEventHandler[string]handler rather than rejecting it as not anEventHandler[T]. -
Delegate conversions now preserve the exact target identity (issues #4381 and #4433). A method group or stored function value passed to a user assembly's own
System.Func<T>constructed the BCLFuncwith the matching structural signature, and a function literal passed directly toEventHandler[T]on a constructed G# receiver used the naturalAction<object, object>shape. Both produced ILVerifyStackUnexpected; delegate combination could also throw at run time. Conversions now retain the target assembly and substitute the constructed receiver before materializing the delegate. -
base.Mconverted directly to a delegate now calls the base implementation (issue #4393). In a generic class,let f (T) -> string = base.Namein a member body bound the derived override. In a class deriving a constructed generic base (class DC : Base[string]), the conversion failed IL verification (DelegateCtor) and threw at run time. The delegate now targets the base method, instantiated as the derived class names it, as Roslyn'sldftndoes. -
Subscribing to an event you cannot access is now an error (issue #4394).
s.Changed += hon aprivateorprotectedevent compiled from an unrelated class. It now reports GS0472 or GS0379, asbase.E += halready did, on every subscription path:obj.Eand bareE,+=and-=, through a class-constrained type parameter, and for source and imported events. An imported event that exists but is out of reach now reports GS0472 or GS0379 instead of GS0158 ("cannot find member"). A class deriving an imported type can now subscribe to itsprotectedevents through its own instance (this.E += h), as in C#. -
Generic type inference no longer infers a platform type
T!from an argument (issue #4443, ADR-0186 §3 as amended). Passing a value from nullability-oblivious code to a generic method, as inWrap(x)orEnumerable.Repeat(x, 2)wherexisstring!, inferredList[string!], which does not convert to a declaredList[string](GS0155).- The rule. A
T!argument now contributesT, and aT!argument alongside aTone also givesT. - The check. The argument is checked at the call, so a nil throws
NullReferenceExceptionthere, naming the expression. - What keeps
T!. Only the argument's top level is affected: the element of an obliviousList<string>still infersstring!. Arefargument, and an argument to a parameter an oblivious library method declares, keep their exact type. - Inference over such a list (
First(xs)againstFirst[T](xs List[T]) T) now works; it used to inferstringand then reject the argument. An annotated list from enabled metadata reads the same way:Firstover aList<string?>now infersstring?, as C# does, where it used to infer a non-nullstringwith no diagnostic.
- The rule. A
-
A platform-typed value of a G#-declared type now resolves imported calls like the plain type does (issue #4444, ADR-0186 §1 and §5a). In an
@Obliviousscope a parameter typed with a G# class isNode!.List[Node].Add(n), aList[Node]{n}initializer,Enumerable.Repeat(n, 2), and a method group of such a function passed toSelectall reported GS0159 "Cannot find function", although the same calls overstring!bound. They now bind the same method as forNode, and a nil argument is checked where it is coerced to the non-null parameter (§4). -
A user-defined method group with a nullable base-class parameter now converts contravariantly to a delegate with a derived-class parameter (issue #4289). For example,
func Check(a Animal?) boolnow satisfiesFunc[Dog, bool], matching the existing non-nullable conversion and the imported-method-group path. The reverse null-unsafe direction remains rejected: a method takingAnimaldoes not satisfy a delegate that may passDog?. -
cs2gs asserts a member chain through a sibling project's member that it widened to
T?(issue #4356 follow-up). cs2gs renders some fields and propertiesT?for reasons only the declaring project's source shows:[AllowNull](#3694), or the project's own== nulltests on the member (#1072). Another project in the same migration sees that member through the declaring project's reference assembly. It therefore readcompilation.DebugInformation.Formatorx.Definition.Namebare, and gsc rejected the chain withGS0158once its stated-nullable carve-out was removed. cs2gs now asks the declaring project's own compilation, so the consumer emits the!!that the widened declaration requires. This had turnedsrc/Compiler, its five dependants andtest/Core.Testsred in the nightly self-migration. -
A nullable argument to an imported generic
T?parameter now infersTas its underlying type (issue #4425). Calling a referenced assembly'sstatic T Required<T>(T? value) where T : classwith aType?argument inferredT := Type?, so the call returnedType?and assigning it to aTypefailed withGS0155. The same method declared in G# source already inferredT := Type, as C# does. Inference now reads the slot's[Nullable(2)]through ADR-0193'sNullabilityImportRule, as the inverse of the rule that widens that slot. A value-type argument is unaffected: an unconstrainedT?given anint32?still infersT := int32?. -
cs2gs no longer asserts a conditional or switch arm whose whole expression flows into a nullable target (issue #4450). The arm's own type used to decide the assertion. So
string emittedName = i >= 0 ? null : argument.Name;became… else { argument.Name!! }inside astring?local, and it threw on the null C# stores there. An arm now takes the whole expression's target: a local, assignment, field, property, parameter or return. An arm flowing into a non-null target keeps its!!. This was the nightly self-migration'sCs2Gs.Teststest-parity failure in the migratedCSharpToGSharpTranslator.LibraryImport. -
cs2gs maps a
Deconstructout-parameter to its member case-insensitively (issue #4382). A positional subpattern whoseDeconstructout-parameter differed in case from the member it reads (out Inner? pfor propertyP) was reported as unsupported and translated to a bare type test, soo is Outer({ X: 0 })becameo is Outer. cs2gs now follows the C# conventionout T xtoX: it maps the out-parameter to the one property or field whose name matches case-insensitively and whose type is the parameter's, including inherited members and fields. The lowering keeps its nil guard and reads a nullable slot once, inispatterns and switch arms. An ambiguous match, a member of another type, no matching member, a member that a derived type hides from an inheritedDeconstruct, a member or getter that isn't accessible, or an extensionDeconstructis still reported loudly, and the diagnostic now says why. Aname:label on a positional subpattern is no longer emitted as a member access, and a binding under a nested typed positional pattern over a nullable slot (Outer(Inner(var a, _))) now compiles. -
cs2gs no longer asserts
!!on reads of nullability-oblivious CLR metadata (ADR-0186 step 6, PR 0). A member of an assembly that says nothing about nullability, and that the migration does not itself translate, is the platform typeT!in G# since ADR-0186 step 3. gsc inserts the nil check itself wherever such a value is used as non-null, so the translated!!only duplicated it, and the polish pass could not remove it. Those reads are now emitted bare. A nil still throwsNullReferenceExceptionat the same point. Three cases keep the!!: a member of another project in the same migration, whose G# type cs2gs decides; a value whose type would otherwise be inferred asT!(for example, a local's initializer, or an argument that fixes a generic method's type argument); and a read of a container type such asstring[]orList<string>, where the!!is still needed for the value to convert to an enabled container (for example,lines = File.ReadAllLines(path)against netstandard2.0, #4449). SettingCS2GS_NULL_FORGIVENESS_TELEMETRYto a file path now makes cs2gs write its per-rule!!counts there when it exits. -
base.M()on asuspendbase method now calls the base implementation (issue #4392). Asuspendoverride that calledbase.M()on asuspendbase method dispatched the call virtually, re-entered its own override and crashed the process with a stack overflow; the output passed ILVerify. Overload resolution returns a call to a suspending method already completed (the implicit await, or the blocking bridge in a plain member), and the base-call binder only recognised the bare call, so it never marked it as a base call. The inner call is now marked and completed again. The base call also receives the hiddenContextargument, and it is forwarded non-virtually from a function literal, agooperand or anasync letinitializer through the same<>n__Nforwardersasyncbodies use.go base.M()andasync let x = base.M()are now accepted instead ofGS0137. A non-virtual base method that inference makes suspending now makes itsbase.M()caller suspending too. A suspending method of a base class from another assembly, called throughbase.M()or unqualified, is now awaited instead of staying typedValueTask[R]. A call to such an imported suspending instance method, whether throughbaseor on an ordinary receiver, now also receives the caller's context, so cancelling the caller's scope reaches it. Previously it ran underContext.None. -
Imported nullability reads more consistently across the paths that read it (ADR-0193 Phase 1, issue #4363). One internal rule now decides what an imported position's nullability becomes, and every metadata reader calls it. A new differential test compares the readers across every public generic member of the reference BCL, netstandard2.0, FsCheck and a
csc-built fixture. That test found these defects, which are now fixed:- A field, property or event of an imported generic type now reads its type argument's nullability. For example,
Tuple[string].Item1from a netstandard2.0 library used to read asstring!. It now reads asstring, as the same member's methods already did (issue #4361, on the paths its fix did not reach). A generic event's handler type used to lose its declared nullability entirely. - A static property of a generic type keeps the receiver's
?.EqualityComparer[string?].Defaultis nowEqualityComparer[string?], notEqualityComparer[string]. [Nullable(2)] Tmarks only the argument's own position as nullable.FirstOrDefault()over anIEnumerable[List[string]]now returnsList[string]?. Some reading paths used to returnList[string?]?. A value-type argument no longer receives a?on its inner positions.- An unconstrained type parameter is no longer widened at a
T?-returning member. Insidefunc F[T](xs List[T]),xs.Find(p)now has typeT, notT?. This matchesvalues.Min().T?on an unconstrainedTwould meanNullable<T>for a value-type substitution. This follows ADR-0193's ruling on the case, which #4385 revisits. - A
ref Treturn through a generic receiver is nowref Trather than a platform-wrapped reference. - The platform type
T!is no longer constructed over a type that has no reference nullability. This coversT?!and value types. As a result, an obliviousSystem.Tupleresult now reads as a tuple rather than(T1, T2)!. - Three known reader disagreements remain, each tracked separately:
- A field, property or event of an imported generic type now reads its type argument's nullability. For example,
-
An argument written without
inat aninparameter is passed by readonly reference, as in C# (issue #4400, ADR-0060 §15).this.In(9),base.In(9), the same call in a lambda, and every call to an imported C# method, constructor, base constructor or extension with aninparameter used to push the value where the callee expected an address, which failed IL verification (StackUnexpected) and threwInvalidProgramExceptionat run time. The free-function, delegate and constructor forms rejected the call withGS0242, and the importedinindexer withGS0155. Now the argument is converted to the parameter type, then passed as the address of the variable, field or array element itself, or of a readonly temp for a literal, call result or converted value.GS0242is retired, and cs2gs no longer turns an rvalue such asScale(x + 1)into the non-lvalue&(x + 1). A plain value at aref/outparameter of a method call now reportsGS0235instead of emitting unverifiable IL. -
Several imported members now keep the nullability they declare (ADR-0193 Phase 2, issue #4363). Some compiler paths converted a member's CLR type directly and never read its
[Nullable]metadata, so a declaredstring?arrived asstringand a nil value reached a non-null variable unchecked. Every such path now reads through the same funnel the ordinary member readers use. Affected positions:- the
Currentof a duck-typed enumerator, infor x in …andawait for; - the result of an awaiter's
GetResult(); - the
Invokesignature of a generic delegate used through a type parameter (Maker[T]); - a static member reached through a generic receiver;
- a static call on a generic class;
- the operands and result of an imported CLR operator;
- structural projection into an imported CLR type.
Remedy for code that now reports a diagnostic here: the value really can be nil, so narrow it, assert it with
!!, or give the destination a?type. An oblivious position (from an assembly that states no nullability) now reads as the platform typeT!on these paths, as it already did elsewhere. A new internal analyzer,GSA0007, keeps compiler code from converting a member's type without reading its nullability. - the
-
An event's handler-type nullability now survives a round trip through metadata (found implementing ADR-0186 step 5). gsc wrote no
[Nullable]on Event rows, which is where bothcscand gsc's own importer read an event's handler nullability, so every gsc-emitted event re-imported as wholly non-null:event Changed Action[string?]came back asAction[string]. Event rows now carry[Nullable]whenever any position is not plain non-null, exactly as fields and properties do. -
Ref returns through a struct receiver now follow C#'s scoped-
thisrule (ADR-0187 / issue #4350, resolving an item ADR-0184 recorded as future work). A member of a concrete struct that is not@UnscopedRefcannot return a reference into its own receiver (GS0589 already enforces that), so its receiver no longer limits where the returned reference may go.prop this[i int32] ref readonly T -> inner[i]over a struct field now compiles, as doesfunc k(in a Acc, ref y int32) ref readonly int32 { return ref a.Pick(ref y) }, which csc also accepts. A writablerefhanded back by such a member stays writable through alet/inreceiver, sovar ref alias = h.Valueandh.Value = 55are accepted and write the storage the reference names.@UnscopedRefmembers, type-parameter and interface receivers, and a ref struct receiver's value scope keep their existing checks (GS0254 / GS0591 / GS0499). -
Generic operators declared on a nullable receiver bind against their owner (issue #4350).
func (left Box[T]?) operator ==(right Box[T]?) boolnow opensBox'sTexactly likefunc (left Box[T]) operator ==, instead of reportingGS0113forT. -
Construction syntax is no longer shadowed by a same-named extension function (issue #4350).
Slice[T](owner, 0, n, n)constructsSlice[T]even when an extensionfunc (s Slice[T]) Slice[T](lo int32, hi int32, max int32)exists, matching C#, which never considers extension methods fornew. A free function of the same name still shadows the type, as before. -
An in-body conversion operator is declared on its enclosing type (issue #4350).
func operator implicit(value Slice[T]) ReadOnlySlice[T]insidestruct ReadOnlySlice[T]used to be attached to the source typeSlice, and when that type's ownsharedblock bound later the operator disappeared from the assembly. It is now emitted on the enclosing type, as in C#. -
Imported generic implicit conversions stay symbolic inside generic code (issue #4350).
Span[T] -> ReadOnlySpan[T]andMemory[T] -> ReadOnlyMemory[T]used to call the erased<object>operator and fail IL verification. An inlineout varpassed to an inferred generic CLR method (MemoryMarshal.TryGetArray(memory, out var segment)) now also keeps the symbolicArraySegment[T]. -
User types may overload indexers (issue #4350). A struct, class or interface may declare several
this[...]indexers that differ by parameter types or count, including multi-parameter ones. Access picks the best overload, as in C#. -
cs2gs keeps struct
overridemembers and every indexer argument (issue #4350). A struct'soverride func Equals(obj object?)used to loseoverride, which silently hid the override so boxed equality fell back toValueType.Equals.slice[index, fromEnd]used to drop every argument after the first, and a long constant operand (2L * capacity) was narrowed toint32. -
gsfmtno longer splits a C-stylefor init; cond; post { … }loop header across three lines, one per clause.SyntaxFacts.IsBreakRequiredBetweentreats every;as a statement terminator that forces a following line break — true for an ordinary statement, but not for the two separators insideForClauseStatementSyntax, which are list separators like,rather than terminators.GSharpFormatter's layout builder now recognizes a;whose direct parent isForClauseStatementSyntaxand treats it the way it already treats a comma: no forced break, aDoc.Linewrap point that collapses to one space when the header fits and indents by 4 when it does not, and (viaIsContinuationBoundary) the layout re-derives the break rather than preserving an author-written one, so the fix collapses hand-formatted three-line headers already in the repository, not only newly-formatted ones. A related spacing defect surfaced in the same code path: an empty initializer clause (for ; cond; post {) left the separator glued directly to theforkeyword (for;) because the pre-existing "no space before;" rule assumed a;always closes a preceding token; it now special-cases a for-clause separator with nothing to its left.samples/Loop.gs,samples/VariadicDelegate.gs,samples/VariadicMethods.gs,samples/WhileAndLabeledLoops.gs, andsrc/Sdk/Gsharp.Extensions/Sequences/Sequences.gscarried the three-line form and are reformatted in the same change sogsfmt --check .stays clean. A second, unrelated gap found investigating the same class of defect: ADR-0179's own phase-7b measurement table lists&&/||/??/or/andchains together as a family that goes fully to zero reducible long lines, but only&&,||, and the pattern-disjunctionor/andhad a wrap point implemented — a??chain had none and could overflow the canonical 120-column width with no way to break.??now gets the identical treatment as&&/||. -
A native ref-returning function taking a by-value byref-like (ref struct, e.g.
Span[T]) parameter and returning a ref through it (return ref s[0]) is no longer unconditionally rejected (issue #4265, found reviewing #4263/#4224).func First(s Span[int32]) ref int32 { return ref s[0] }reportedGS0254("cannot return a managed pointer to function-local storage") regardless of wheres's backing storage actually came from — unlike C#, which allows exactly this (MemoryMarshal.GetReference[T](Span[T])-shaped BCL APIs rely on it). Root cause:StatementBinder.HasFunctionLocalRefScopehad no case at all forBoundClrIndexExpression— the node a CLR/imported indexer read such asSpan[T]'ss[0]binds to (wrapped in aBoundDereferenceExpressionbyConversionClassifier.AutoDereferenceRefReturn) — so it fell straight through to the fully-conservative default and reported unconditionally, never even inspectings. C# draws a sharp line here that the fix mirrors precisely rather than blanket-relaxing: a by-value ref-struct parameter's OWN storage (its fields) is a function-local copy exactly like any other by-value struct and still cannot be returned by ref (ref struct Acc { var Total int32 } func Bad(a Acc) ref int32 { return ref a.Total }still reportsGS0254, unchanged) — only the REFERENT such a value ENCAPSULATES (Span[T]'s backing pointer, reached only through its indexer/methods, never through a field) has caller scope, because the caller already guaranteed that referent's lifetime by constructing/passing theSpanin the first place. The newBoundClrIndexExpressioncase delegates to a newHasFunctionLocalReferentScopehelper — mirroring the file's existing pointer-parameter-dereference precedent exactly — only when the indexed target's typeTypeSymbol.IsByRefLike; a reference-type or ordinary-struct target keeps the previous storage-scope walk unchanged.RefCapabilities.SelectByRefArgumentsis split into two separate argument lists (byRefArguments, checked as before, and a newbyValueByRefLikeArguments, checked via the new referent-scope helper) so a forwarding chain through a by-value byref-like parameter (return ref First(s)from another ref-returning function takings Span[int32]) is escape-checked with the same precision as #4224's existingref/in/outforwarding, without aref/in/outargument that happens to be byref-like-typed ever being checked by the more permissive referent-scope rule (that would reopen the same hole from a different angle — confirmed against a repro during review:func Fwd(ref a Acc) ref int32 { return ref a.Total }called asFwd(ref a)from a by-valuea Accstill correctly reportsGS0254). Ascopedby-valueSpan[T]parameter forwarded this way still reportsGS0254; a non-scopedone compiles and correctly aliases the original storage.ReadOnlySpan[T]works the same way for aref readonlyreturn; a plain (mutable)refreturn from aReadOnlySpan[T]indexer is unaffected by this change and still reportsGS0253(its indexer only ever yieldsref readonly). Soundness hardening (found in review before this fix merged): the newBoundClrIndexExpressioncase originally trusted ANY byref-like CLR indexer target as encapsulating a caller-supplied referent, the waySpan[T]/ReadOnlySpan[T]genuinely do — but nothing in the CLR type system guarantees that. An externally-compiled ref struct can legally declare its indexer[UnscopedRef](System.Diagnostics.CodeAnalysis), a real C# 11+ pattern (nounsafe, no IL authoring) meaning the indexer returns arefinto the RECEIVER'S OWN storage rather than an encapsulated referent — exactly theAcc.Totalshape this fix's own guard exists to reject, just authored in a referenced assembly instead of natively. Without checking for that attribute, a G# function forwardingbuf[0]from a by-value parameter of such a type (e.g.func M(buf RingBuffer) ref int32 { return ref buf[0] }) compiled clean and the returned reference pointed at dead stack memory (confirmed reproducible: an intervening call's locals silently overwrote the "aliased" value on read-back). Real C# rejects the analogous code with CS8166 ([UnscopedRef]inverts the receiver's contribution to the call's ref-safe-context from safe-to-escape back to function-local for a by-value receiver).RefCapabilities.IsUnscopedRefIndexerGetternow inspects the resolved indexer for[UnscopedRef]viaCustomAttributeData(so it works correctly through gsc'sMetadataLoadContext-based reference loading, not just live reflection), checking BOTH the property and itsgetaccessor — C# allows the attribute on either, and an expression-bodied indexer ([UnscopedRef] public ref int this[int i] => ref field;) emits it on the property rather thanget_Item— and theBoundClrIndexExpressioncase falls back to the strictHasFunctionLocalRefScopetreatment whenever either carries it, matching C#'s behavior exactly (including still accepting the same member forwarded through a genuinerefparameter, where the referent legitimately is the caller's own storage). -
cs2gsno longer inserts a spurious!!non-null assertion on an intentionally-nullable delegate argument passed cross-project into a nullable-oblivious sibling project, which used to throwNullReferenceExceptionat runtime on a legitimatenil(issue #4262, found migratingDavidObando/Oahu). A nullable-ENABLED consumer project (e.g.Oahu.Cli.App) calling a delegate parameter declared in a nullable-OBLIVIOUS sibling project it references (e.g.Oahu.Core) — where the sibling's own whole-program taint fixpoint (ObliviousNullabilityAnalyzer, issue #2113/#2412) proves that parameter acceptsnilvia a transitiveif (convertAction != null)guard several calls downstream — gotconvertAction!!at the call site instead of a bareconvertAction, becauseObliviousNullabilityAnalyzer.IsTainted's three-argument (cross-project) overload bailed out immediately whenever the ASKING compilation (the consumer's own) was nullable-enabled, before its sibling-walk loop — which already correctly remaps into, and gates on the obliviousness of, each sibling independently — ever got a chance to run; #2412's cross-project machinery therefore only ever fired when the CONSUMER itself was oblivious, never when an enabled consumer called into an oblivious sibling. The entry gate now bails only when neither the asking compilation nor any sibling is oblivious. A second, narrower defect surfaced while fixing the first:IsTaintedCore's own whole-program fixpoint (Compute) reads raw syntax with no nullable-context guard of its own (an oblivious-only heuristic, since oblivious code carries no trustworthy annotation to read instead), so once the entry gate stopped preventing it from ever running against a nullable-ENABLED compilation, aT?local legitimately initialized tonullin the ENABLED consumer's own source could self-taint a cross-project call target through an ordinary argument-to-parameter edge — an unsound false positive that happened to look correct for a genuinely-tainted target but would have silently suppressed the!!bridge for an oblivious sibling parameter with no null-check evidence anywhere.IsTaintedCorenow only ever consults a compilation's own cached fixpoint when that compilation is itself oblivious, and always still walks the sibling set. The identical entry-gate shape was also present, and is also fixed, onObliviousNullabilityAnalyzer.IsParamsElementTainted(issue #3888's cross-projectparams T[]element taint) — reachable the same way, fromTargetWillRemainNonNullableReference's variadic-carrier branch, for a nullable-enabled consumer's expanded-params-tail argument into an oblivious sibling; its own candidate loop already gated each candidate (including itself) on obliviousness before touching its cache, so only the entry bail needed widening there, not the innerIsTaintedCore-style guard.!!is a real runtime throw in G# (unlike C#'s erased!), so this was a silent, systemic correctness regression rather than a cosmetic one — every plain (non---export)oahu-cli downloadcall crashed post-migration. Found during review: the same widened-gate reachability exposed three further call sites that consulted cross-project taint evidence without requiring the queried declaration's own annotation to be genuinely oblivious (NullableAnnotation.None) first — unlikeShouldPromoteToNullableReference's ordinary path, which already required that.IsParamsElementTainted's candidate loop matches by a compilation-independent documentation-comment ID rather than remapping by symbol identity the wayIsTainted's sibling walk does, so an UNRELATED oblivious sibling anywhere in the run — one with no connection to the actual call site — calling the same targetparamsmethod with a literalnullin its own source could suppress the!!bridge for a genuinely non-nullable enabled target, or (viaPromoteParamsElementIfUsedAsNullable's declaration-side promotion) repaint that target's OWN emittedparamselement nullable. A third, structurally identical gap existed inPromoteAwaitedReturnIfTainted(anasync Task[T]declaration's return-type promotion), where an oblivious override or interface implementation of an enabled base/interface member could repaint the enabled member's own declared return type nullable. All three are now guarded:IsParamsElementTaintedrequires the queried parameter's own element annotation to beNonebefore consulting any candidate's cache, andPromoteAwaitedReturnIfTaintedrequires the awaited type's own annotation to beNonebefore consultingIsTainted. -
A local function nested inside a generic method or type can now reference the ENCLOSING type parameter, whether or not the local function declares type parameters of its own (issue #4223, completing the diagnostic-only stopgaps of #1940 and #2016).
func Outer[U](seed U) U { let Keep[T] = func(ignored T, value U) U { return value }; return Keep(1, seed) }and its non-generic siblinglet Keep = func(value U) U { return value }(arrow syntax included) both reportedGS0468even with zero outer-value captures, because the emitted hoisted method carried only its own declared type-parameter list and the enclosingUhad no corresponding CLR slot.UserTokenResolver.TryPromoteNonCapturingGenericLambda(the issue #2118 machinery that already promoted a zero-capture NON-generic local/lambda referencing an enclosing type parameter into a genuine generic method) is extended to also apply when the local ALREADY declares its own[T, ...]list: every referenced enclosing type parameter is cloned as an ADDITIONAL method type parameter appended after the local's own, withBuildMethodSpecForGenericCallsupplying the extra type arguments directly (they are exactly the in-scope originals, never inferable from the call's arguments, since the local's own signature still names the original enclosing parameter rather than the clone) alongside the ordinary inferred/explicit ones for the local's own slots. The generic-own-type-parameter path's type-parameter-list binding is also fixed to resolve a constraint against the ENCLOSING scope (let Keep[T IComparable[U]] = ...,Uowned by the enclosing method) — previouslyGS0113 "Type 'U' doesn't exist"before the reference even reached the enclosing-type-parameter check, because the constraint clause bound in a scope seeded empty rather than merged with the enclosing type parameters. Two further gaps surfaced only once the diagnostic itself stopped masking them. First,LambdaEnclosingTypeParameterCollector— the shared scanner this promotion and the pre-existing capturing-closure reification (issues #1477/#3933) both use — never checked an implicit-receiver call's STATIC OWNER TYPE, a method group's owner type, a field/property access's declaring type, or a function-pointer's owner type; a local function calling asharedmember of the enclosing GENERIC type by its unqualified name (return Public()resolving toHolder[Outer].Public()) therefore had itsOuterreference missed entirely, silently falling back to an invalidVARslot on the promoted (non-generic-owner) host —System.BadImageFormatExceptionat load. Second, and more insidious: promoting a CALLEE to carry an extra enclosing type parameter can require its CALLER — another zero-capture local function in the same pass, textually unrelated to the enclosing parameter — to ALSO carry it, purely so the call site has something to pass; a caller that only reaches the parameter indirectly (calling a promoted sibling whose extra parameter never appears in ITS OWN argument or return types) was left unpromoted, and its call site's missing argument fell back to whatever type parameter happened to share the same ordinal in the wrong context — a WRONG VALUE at runtime with no error at all, worse than a crash.TryPromoteNonCapturingGenericLambdanow runs to a fixed point over every zero-capture local function/lambda in a compilation unit (each pass re-derives a function's complete referenced set, consulting a newGetPromotedExtraslookup for every call it makes, until a full pass promotes nothing further), so a callee promoted late in one pass still reaches an earlier-processed caller on the next. The ASYNC route needed a third, independent fix:ReflectionMetadataEmitter.RegisterStateMachineEnclosingGenericsbuilt the async state machine's outer-type-parameter remap keyed by the promoted local's OWN (cloned) type parameters, never consulting the promotion's enclosing-TP-to-clone-ordinal remap the wayRegisterGeneratedGenericRemapsalready does for the analogous ITERATOR state machine (issue #810 + #2118 combined) — so a body reference to the original enclosing parameter fell through to a danglingMVarslot insideMoveNext(a non-generic member of the now-generic state machine),System.BadImageFormatExceptionat load. It now aliases each original enclosing type parameter onto the same class slot its clone occupies, mirroring that existing iterator fix exactly. Two combinations remain unsupported and correctly still reportGS0468. First: a CAPTURING generic local function (issue #4221/#4252's synthesized-closure-class emission path, a different mechanism from the zero-capture top-level MethodDef path this fix extends) referencing an enclosing type parameter in its own signature or an own-type-parameter constraint — confirmed by direct repro to otherwise compile clean and crash (TypeLoadException/invalid IL) the moment the enclosing method executed; composing enclosing-type-parameter reification with capture support is left for a follow-up, as the issue itself anticipates. Second, found during this PR's own adversarial review: a NON-generic, zero-capture, ASYNC local function (arrow syntax included) whose OWN PARAMETER type — not its return type or body, both of which are fully supported and covered above — references an enclosing type parameter. That shape routes through a pre-existing async "erased delegate" adapter (predates issue #2118, used whenever a function-literal's declared type mentions an open type parameter) rather than throughRegisterStateMachineEnclosingGenerics's reification, and the adapter's parameter-unboxing conversion has a confirmed defect for a value-typed instantiation:func Outer[U](seed U) U { let Local = async func (x U) U { return x }; return Local(seed).Result }, if allowed to compile, runsOuter("hi")successfully but throwsNullReferenceExceptionforOuter(42). The identical crash reproduces on an already-legal CAPTURING async local of the same shape, and on a zero-capture async local of the same shape nested inside a non-generic type (both confirmed onmainpredating this issue entirely — neither was ever GS0468-gated, so that pre-existing exposure is unchanged here), so the adapter defect itself is pre-existing and unrelated to #4223. What IS in this issue's scope: relaxing GS0468 unconditionally for the newly-enabled zero-capture case would have newly routed a previously-rejected program (this parameter-referencing shape was unconditionally GS0468 before #4223) through that same broken adapter, trading a compile-time diagnostic for a runtime crash — which the issue's own guidance rules out ("Relax GS0468 only for routes with proven valid emission").LambdaBinder.CheckAsyncNonGenericLocalFunctionEnclosingTypeParameterInParameterretains the diagnostic for exactly this narrow combination, matching this shape's pre-#4223 footprint exactly (including the existing non-generic-owner-type exclusion, so it does not newly reject the already-legal, already-crashing struct/class-nested case either). Fixing the erased-delegate adapter itself remains a separate, pre-existing defect, disclosed here rather than fixed. -
Merging #4223 onto #4221's transitive-capture reconciliation (issue #4261) surfaced one more gap in the "capturing generic local referencing an enclosing type parameter" exclusion above, closed as part of that merge. The exclusion is decided once, at a group member's own bind time, from its
CapturedVariablesas known then — butLambdaBinder.ReconcileGenericLocalFunctionGroupCaptures(#4221's fixed point) can still widen that set afterward, for a FORWARD reference to, or a call CYCLE with, a sibling that captures outer state, since that widening is only discoverable once every member in the region has bound. A member that looked capture-free (and therefore eligible for #4223's zero-capture reification) when its own gate ran could be found to capture transitively only after the fixed point converged, and the gate never re-ran:func Run[Outer]() int32 { var outer = 1; let first[T] = func(a T, y Outer) int32 { return second(a) }; let second[U] = func(a U) int32 { return outer } return first(1, default(Outer)) }—firstis declared beforesecond(a forward reference), sosecond's capture ofouterisn't folded intofirstuntil after both members bind — compiled with no diagnostic and crashed the CLR with an invalid-program error the momentfirstexecuted. Each generic local-function literal now caches the enclosing type parameter it referenced (if any) at its own bind time; once the region's fixed point converges, every member whose capture set grew from empty is re-evaluated against that cached reference and rejected withGS0468if it now captures anything, instead of silently reaching the emitter. The two mechanisms otherwise coexist correctly: a member that only captures (no enclosing-type-parameter reference) and a sibling that only needs enclosing-type-parameter reification (no capture) are unaffected by each other, including within the same consecutive-declaration region. -
A lambda assigned to a named delegate whose trailing variadic parameter carries a generalized carrier type (ADR-0173 / #3627, e.g.
...List[T]) no longer has its own parameter mis-bound as a slice OF that carrier (issue #4235).delegate D(a int32, xs ...List[int32]) int32followed bylet d D = (a int32, xs ...List[int32]) -> areportedGS0155: Cannot convert type 'List[int32]' to '[]List[int32]'on the assignment — with no invocation involved, since the mismatch was already there in the declared types. The delegate side correctly resolved its...List[int32]parameter to the carrierList[int32]itself viaVariadicCarriers.ResolveDeclaredParameterType, butLambdaBinder's two lambda-parameter binding sites (function-literalfunc(...)and arrow(...) ->) still had the pre-ADR-0173 unconditional...T->[]Twrap left over from #812, so the lambda's ownxsbound as[]List[int32]— a slice of the carrier, not the carrier — and the two sides disagreed. Both sites now route through the sameVariadicCarriers.ResolveDeclaredParameterTypehelper the delegate and every named-function/constructor declaration already use, so an explicitly-typed lambda variadic parameter resolves to the identical carrier (array,List[T],Span[T]/ReadOnlySpan[T], or one of the fiveIEnumerable-family interfaces) regardless of which side of the assignment declares it first. Target-typed inference (an omitted lambda parameter type filled in from the delegate) was already correct and is untouched. The real "array of carrier" shape (...[]List[T]) and a non-carrier generic element (...Dictionary[K, V], which keeps the ADR-0101 element interpretation) are unaffected. -
A default-parameter-value expression that forward-references a sibling type's static member no longer gives a misleading diagnostic that depends purely on declaration order (issue #4183, found while measuring #4166).
func M(x int32 = B.Value) int32— whereclass C(declaringM) is declared BEFOREclass B(declaringshared { var Value int32 = 42 }) — reportedGS0158: Cannot find member Value.even thoughB.Valueplainly exists; reversing the declaration order instead reported the semantically-correctGS0265: Optional parameter 'x' is invalid: ... must be a compile-time constant. Root cause:ConversionClassifier.BindAndAttachParameterDefaultValueran eagerly, inline, duringBinder.BindGlobalScope's single per-struct declaration-body pass — the same class of forward-reference gap field/const initializers had (#1070/#1194) before those were deferred. Default-parameter-value binding (covering primary-constructor, instance-method, static-method and explicit-init(...)-constructor parameters alike) is now deferred the same way, via a newpendingParameterDefaultValueBindingsqueue drained byDeclarationBinder.BindPendingParameterDefaultValues()once every struct's static-member surface exists compilation-wide. The drain runs BEFOREBindPendingBaseInitializers, per the ordering constraint the investigation identified: a: base(...)initializer readsParameterSymbol.HasExplicitDefaultValuewhile resolving against a base constructor's optional trailing parameters, so binding defaults any later would make that resolution see a not-yet-populated default. Both declaration orders now report the identicalGS0265— no cross-type field/const read is foldable into a compile-time constant today (a separate, orthogonal gap this issue does not fix), matching the equivalentcscbehavior for a non-constant static-field default. This also incidentally strengthens #4166's structural finding (already downgraded to "theoretical, no confirmed bug"): default-value binding now runs strictly afterDetectClassInheritanceCycles, rather than before it. -
A nullable-reference annotation on a tuple element nested inside an imported
async Task[...]return type is no longer dropped (issue #4159, found while investigating #4133/#4156).Store.GetNullableAsync() Task[IReadOnlyList[(Key string, Value string?)]], imported from a referenced (already-compiled) assembly, compiledlet x string = rows[0].Valuewith no diagnostic — the same-compilation (symbolic) equivalent already correctly reportedGS0155. Root cause, inNullabilityAnnotatedTypeSymbol.TransferTupleNames: a tuple nested one level inside another generic argument (IReadOnlyList[...]insideTask[...]) is represented LAZILY, andTransferTupleNamesonly matched a tuple sitting directly under oneNullablewrapper or a top-levelImportedTypeSymbol, so it silently returned the unnamed shape unchanged instead of materializing and naming the lazy nested generic; that same fix also covers a PLAIN (non-tuple) nullable-reference-typed awaited element —ExpressionBinder.TryGetTaskElementType's CLR-fallback path (traced directly: taken specifically when the awaitable's own type-argument tree contains no tuple needing eager reconstruction, e.g.Task[string?], as opposed to the tuple-containing case above, which instead takes the same method's symbolicGetTypeArgumentSymbolfast path) recovered the result via a naiveTypeSymbol.FromClrTypeinstead of the nullability-awareNullabilityAnnotatedTypeSymbolaccessor — so this fix is general to any reference-typed awaited element, not tuple-specific, and simply went unnoticed until a named tuple made the loss visible. This is the root cause referenced in #4133's own PR notes below (theGS0159symptom in a real-world corpus, mitigated there by treating two mutually-convertible bounds as mergeable rather than an unresolvable ambiguity); this fix addresses the underlying metadata-import defect directly. Two Copilot review findings on this PR surfaced two further, narrower edges in this same machinery:TransferTupleNames's nullable-wrapper case required BOTH the naive, name-bearing tree AND the flags-derived tree to already beNullable-wrapped to recurse, missing a tuple nested inside a nullable REFERENCE generic argument specifically (e.g.IReadOnlyList[(...)]?, the list itself nullable) since the naive tree never reliably wraps reference nullability the way the flags-derived one does — now keys off the flags-derived side alone; andGetTypeArgumentSymbolForClrType(the CLR-fallback path above) matched the outer awaitable's generic argument by closed-CLR-type equality alone, ambiguous for a custom (non-Task) awaitable with two type parameters that happen to erase to the identical CLR type — now refuses to guess and falls back to the unannotated result rather than ever return an annotation that might belong to a different type parameter. -
A bare method group whose declared parameter type is a BASE class/interface can now convert to a concrete delegate type over a DERIVED element type (issue #4165).
Name(x Base?) stringpassed asitems.Select(Name)overIEnumerable[Derived]— or directly,Func[Derived, string] f = Name— reportedGS0155: Cannot convert type '(Base?) -> string' to 'System.Func[Derived, string]';cscaccepts this as an ordinary contravariant method-group conversion.Conversion.IsFunctionToDelegateConvertibleclassified each parameter/return slot by comparing raw CLRTypes (ClrTypeUtilities.IsAssignableByName), which can only ever fire when the method group's own parameter type carries aClrType— never true for a same-compilationclass/interfacemid-binding, whoseTypeBuilderdoes not exist yet. (A CLR-imported base/derived pair, e.g. Roslyn'sISymbol/INamespaceOrTypeSymbol, was measured to never reach this classifier at all for either a.Select(...)argument or a direct delegate-variable assignment — some other resolution path handles that shape, so this defect was specific to same-compilation parameter types and never visible for an imported one.) Each slot now falls back toConversion.IsImplicitReferenceVariantSlot— the same reference-only, hierarchy-aware classificationConversion.Classify's own upcast rules already use — when the CLR-type check cannot even ask the question, covering both parameter (contravariant) and return (covariant) positions and both same-compilation and CLR-imported element types alike. A delegate closed over a same-compilation type argument (Func[Derived, string]) also erases that argument toobjectin the CLR-reflected signature mid-binding, so the real argument is recovered from the delegate's ownTypeSymbol.ConstructedTypeArguments(symbolic, not erased) by position before classification. Removing the parameter loop's unconditional rejection also surfaced a dormant sibling defect the pre-existingIssue1518NullableDelegateInferenceEmitTestssuite caught as a regression during this fix's own validation:NullableTypeSymbol.ClrTypeerases a nullable VALUE type (bool?) to its bare underlying type (bool), so the return-type identity check could no longer tell a lambda that actually returnsbool?apart from one returningboolonce the parameter gate that used to reject every same-compilation-parameter candidate outright stopped masking it —Select'sTResultoutput-type inference locked onto the non-nullable candidate first and the emitter builtFunc[S, bool]where callers still expectedFunc[S, bool?](an ilverifyStackUnexpected). The return-type check now usesNullableLifting.GetEffectiveClrType(identity for every other shape, including reference types) instead of the raw, erasingClrType, matching whatFunctionTypeSymbol.BuildClrTypealready does to avoid this exact erasure for the emitted delegate itself. The identical erasure gap exists on the PARAMETER side too (a method group declaring aT?parameter satisfying a delegate declared over the bareT, whichcscrejects withCS0123) but was not reachable by any currently-passing test and is tracked separately as issue #4184 rather than fixed here without a red witness. -
A bare method group whose declared parameter type is a nullable value type (
T?) no longer silently satisfies a delegate slot declared over the bare non-nullableT, and vice versa (issue #4184, the parameter-side sibling of #4165's return-type fix above).func HasValue(x int32?) bool -> x != nilassigned toFunc[int32, bool]compiled with no diagnostic and ran, silently dropping the nullable requirementHasValuedeclared;cscrejects the equivalent C# withCS0123.Conversion.IsFunctionToDelegateConvertible's parameter fast path compared the method's declared parameter type against the delegate's using the raw, erasingfnParamType?.ClrType—NullableTypeSymbol.ClrTypeis the bare underlying value type, soint32?was indistinguishable fromint32. The fast path now routes throughNullableLifting.GetEffectiveClrType(the same helper #4165's return-type fix uses), which re-wraps a value-type underlying inNullable[T]. That alone was insufficient for the REVERSE direction (a method declaring bareTwrongly satisfying a delegate'sT?slot — alsoCS0123undercsc, and measured as a live gap while building this fix's own T/T? matrix):Type.IsAssignableFromhas its own CLR-level special case whereNullable<T>.IsAssignableFrom(T)istrue(supporting the implicitT -> T?lift used in ordinary expression contexts), whichClrTypeUtilities.IsAssignableByNamewould otherwise honor here even though C#'s method-group parameter matching does not extend it to value types. A new narrow guard,IsClrNullableWideningMismatch, closes that reflection-level gap at this one call site without narrowingIsAssignableByNameitself, which many unrelated assignability probes elsewhere in the binder and emitter still rely on. The full T/T? parameter matrix (T -> T,T? -> T,T -> T?,T? -> T?) now matchescscexactly. The identical reflection quirk also affects the RETURN side'sT -> T?direction for a bare method group, but fixing it safely requires distinguishing a method group from a lambda at this shared call site (a lambda'sbool-returning body IS accepted bycscwhen target-typed againstFunc[int32, bool?]) — tracked separately as issue #4186 rather than fixed here without resolving that discriminator. -
A doubly-nested contravariant delegate parameter (
Action[Action[T]]) with an unresolvable bare method-group argument no longer crashesgsc(issue #4171, found verifying a Copilot review finding on #4156).NestedVariance.Pick(strings, NestedVariance.Handler)— wherePick[T](items IEnumerable[T], handler Action[Action[T]])correctly infersT = stringfrom the receiver, andHandler's ownAction[object]parameter does NOT satisfy the resultingAction[Action[string]]slot (cscrejects the equivalent call withCS1503) — crashed with an internalGS9998: NotSupportedException: Cannot encode signature for type '?' yet.during PE emission, rather than reporting a clean diagnostic. An unresolved bare method group is deliberately typedTypeSymbol.Erroras a "defer me" sentinel until overload resolution picks a candidate (ClrOverloadResolution.IsUnresolvedMethodGroupArgument), andExpressionBinder.Calls.Invocation's "an argument that already failed to bind cannot participate in overload resolution" guard treated ANYTypeSymbol.Error-typed argument as already-diagnosed and silently returned an undiagnosedBoundErrorExpressionwhen no candidate accepted the call — but a deferred method group is never a prior failure. The top-level declaration's inferred type became the bareTypeSymbol.Errorsentinel with zero diagnostics reported, reaching the emitter, which has no encoding for it. That guard now excludes an unresolved method-group argument, so the call falls through to the existingGS0159("cannot find function") diagnostic instead — a real, if generic, rejection with no crash. The doubly-nested shape with a handler that DOES satisfy the inferred delegate (Action[string]) continues to compile and run, matchingcsc. -
An annotation naming a type caught in an unresolved base-class cycle no longer hangs
gscand grows memory without bound (issue #4162).StructSymbol.GetHierarchy()walked the symbol-levelBaseClasschain with no cycle guard; the post-bind cycle detector (#973) normally rejects a genuineclass B : C/class C : Bcycle before anything else walks that chain, butGetHierarchy()is also reachable earlier, during declaration binding, viaDerivesFromSystemAttribute()→IsAttributeType(), whenever an annotation names a type that is itself part of an unresolved cycle. Without a guard the walk never terminated, so@Bon an unrelated class over a cyclicB/Cpair grew RSS from 3.6 GB to 18 GB in ten seconds rather than reporting a diagnostic.GetHierarchy()now folds avisitedcheck into its own walk — a repeat both detects the cycle and stops it — so the source now cleanly reportsGS0381for the cycle andGS0200for the resulting non-attribute type, with no change to any acyclic hierarchy's result. -
An explicit
init(...)constructor parameter typed as a same-compilation class no longer hangsgscwhen its enclosing class is part of a base-class cycle (issue #4164). A different, independently reachable unguardedBaseClasswalk than #4162's:BoundScope.TryLookupNestedTypeAliasIncludingInherited()loopedfor (var c = container.BaseClass; c != null; c = c.BaseClass)with no cycle guard, reached while binding an explicit constructor parameter's type clause —Binder.LookupType()→TryLookupLexicalNestedTypeAlias()→ here — before the post-bind cycle detector (#973) has run. A livedotnet-stacksample of the hanging process confirmed the same frame repeating indefinitely. A primitive-typed explicit parameter, or the same class-typed parameter on a PRIMARY constructor, never reached this path and were unaffected. The loop now folds in the samevisited-set guard #4162 used, soopen class B : C { init(value Widget) { } }/open class C : B { let W int32 }now cleanly reportsGS0381instead of hanging. -
cs2gs's ADR-0169 analyzer-translation mode can now translate a C# analyzer's own null-conditional-access detection, SOUNDLY, across the full idiom set — the type test, the registration guard, receiver/tail reads, chain walks, name extraction, bare casts, and descendant/ancestor walks (issue #4173). An earlier version of this fix (landed first in this same PR) covered only the type test and the registration guard, and had to remove a THIRD idiom a reviewer found unsound (a syntactic "is this single-hop" test that cannot actually be proven from analyzer source text alone — both a genuine single hop and an inner chain node satisfy the same syntactic shape). A follow-up design pass found the real structural correspondence: Roslyn'sConditionalAccessExpressionSyntax/WhenNotNullchain and G#'s own postfix-chain parse are NOT mirror images of each other (an earlier draft of this same fix assumed they were, and that assumption was wrong) —./?.continuations right-nest in G# too (throughAccessorExpressionSyntax.RightPart, exactly like Roslyn's ownWhenNotNullnesting, confirmed independently byAssignmentTargetSyntaxFacts.TryLiftTrailingMemberAccess's existing recursion), while[/?[/(continuations still nest the ordinary iterative way (throughIndexExpressionSyntax.Target/CallExpressionSyntax.Callee). A new src/Core utility,GSharp.Core.CodeAnalysis.Syntax.NullConditionalChain, walks this hybrid shape directly over real G# syntax trees at analysis time (IsNullConditionalHop,ChainTop,NextNullConditionalHop,TailIsMemberBinding/TailIsElementBinding,ReceiverIsMemberBinding/ReceiverIsElementBinding/ReceiverIsPlainMemberAccess, and Roslyn-exactHopSpan/ReceiverSpan/WhenNotNullSpanlocation math), and cs2gs now rewrites:expr is ConditionalAccessExpressionSyntax [x]toNullConditionalChain.AsNullConditionalHop(expr) is ExpressionSyntax [x](coveringa?.bANDa?[i]uniformly, closing a genuinea?[i]under-fire the first landing left in place);RegisterSyntaxNodeAction(handler, SyntaxKind.ConditionalAccessExpression)to a guarded wrapper registered for BOTHSyntaxKind.AccessorExpressionandSyntaxKind.IndexExpression;<x>.WhenNotNull is <Type> [designator]and<x>.Expression is <Type> [designator]to the matchingNullConditionalChainpredicate, with the designator narrowed to the real G# type the predicate proves;MemberBindingExpressionSyntax.Name/.OperatorTokenandElementBindingExpressionSyntax.ArgumentListtoRightPart/DotToken/Indices; a bare cast or variable-typedConditionalAccessExpressionSyntaxread to G#'s non-discriminatingExpressionSyntaxsupertype (safe because every DISCRIMINATING use is intercepted by the idioms above — trusting the registration guard for soundness, deliberately NOT trying to statically prove a handler is exclusively guard-registered, the same reasoning that made the removed idiom unsound); and.OfType<ConditionalAccessExpressionSyntax>()/FirstAncestorOrSelf<ConditionalAccessExpressionSyntax>()to an explicitNullConditionalChain.IsNullConditionalHopfilter. Every rewrite handles BOTH C# shapes a type test can take — a designatedis Type x/pattern-combinator form (IsPatternExpressionSyntax) and a BAREexpr is Typewith no designator, which parses as the olderBinaryExpressionSyntax/SyntaxKind.IsExpressionnode and never reaches pattern translation at all; missing the bare form would have made the new non-discriminatingExpressionSyntaxmap row silently vacuous for the commonest real-world spelling of a type probe. A sibling pre-existing bug is fixed alongside:MemberAccessExpressionSyntax→AccessorExpressionSyntaxandSyntaxKind.SimpleMemberAccessExpression→SyntaxKind.AccessorExpressionwere BOTH unconditional, but Roslyn's ordinaryMemberAccessExpressionSyntaxEXCLUDES a?.hop's tail (Roslyn gives that its own receiverlessMemberBindingExpressionSyntaxkind) — sonode is MemberAccessExpressionSyntaxand an unguardedSimpleMemberAccessExpressionregistration both silently over-fired on every?.hop's tail too; both now carry the sameIsNullConditional-discriminating fix, inverted..WhenNotNullread as a bare value (not the scrutinee of anistest) still has no G# counterpart and stays a loudCS2GS-GAP, as does<x>.Expression is ConditionalAccessExpressionSyntax(Roslyn only reaches a genuine CAE there through a parenthesized chain break, which cannot be told apart from an ordinary hop without risking a vacuously-true test — the same class of reasoning that sank the removed idiom). G#'s own null-conditional INDIRECT invocation (f?(x), a G#-only construct with no C# counterpart) is out of scope and does not dispatch through any of this. Round 3 (adversarial review of round 2, above) found the type-test fix still only recognized the TOP-LEVEL shape of a pattern (a bare/designatedDeclarationPatternSyntax/TypePatternSyntax/ConstantPatternSyntax, via one dedicated entry point) — anot,and,or, a switch-case label, or a nested property subpattern fell through to the ordinary native-pattern/boolean-lowering translation, which had no CAE/shared-node awareness at all, and silently over-matched: reviewer's reproduction, a designatedis not ConditionalAccessExpressionSyntax caeregistered onSyntaxKind.IdentifierNameand run over PLAIN (non-null-conditional)box.Name, reported 2 diagnostics where real Roslyn reports 0. Round 3 fixes this at the LEAF instead of enumerating more pattern shapes (the same enumeration mistake twice already): every pattern-position type-mapping call site (TranslatePattern,TranslateRecursivePattern,BuildNativePattern,TranslatePatternTest,TranslateNotPatternTest,TranslateRecursivePatternTest, the loop-condition guard emitter, and the statement-levelif let/guard-hoist family) now routes through two shared builders instead of calling the general type mapper directly. ForMemberAccessExpressionSyntax/ElementAccessExpressionSyntax(the latter a previously-unreported sibling of the identical bug — the map row carried no guard of any kind), the fix is a pure G# PATTERN leaf swap (AccessorExpressionSyntax/IndexExpressionSyntaxplus anIsNullConditional: falsesuffix), which composes for free undernot/and/or/parens/switch/nested subpatterns through the existing, already-tested pattern walkers — no new shape enumeration anywhere. ForConditionalAccessExpressionSyntax, whose faithful G# form is theNullConditionalChain.AsNullConditionalHop(x) != nilPREDICATE (not a pattern), a shape-agnosticDescendantNodesAndSelfscan — deliberately not a switch over pattern kinds, so a future shape can't defeat it the way the first two rounds' enumerations did — detects any CAE-mentioningis-pattern and routes the WHOLE pattern off the native-G#-pattern paths onto the existing boolean-lowering path, where the same leaf hook emits the predicate andnot/and/orcompose for free; a CAE designator binds by substitution at the only three positions C#/G# both allow one (top level, under top-levelnot, or underand— GS0390/CS8780). A position that is G#-pattern-ONLY (a switch-case label, a switch-expression arm, or a nested property subpattern that could not be boolean-lowered) has no sound CAE rewrite and now surfaces a loudCS2GS-GAPinstead of silently falling back to the non-discriminatingExpressionSyntaxsupertype. The statement-levelif let/guard-hoist machinery builds its own hoisted local ahead of the pattern-test translation ever running, so it needed its own fix, not just a caller of the shared leaf: for CAE the hoist's initializer becomesAsNullConditionalHop(receiver)directly (already nullable, so it drops into the existinglet t T? = …; if t != nilshape unchanged), and for a plain-access type the existingas-cast is kept but the guard gains an extraIsNullConditionalconjunct; where adapting the hoist wasn't safe to do inline it declines and falls back to the general lowering instead. Two real, in-repo sites insrc/Analyzers/InternalAnalyzers/RewriterClonePreservationAnalyzer.cs(a nested property-subpattern designator and a switch-case label, both testingMemberAccessExpressionSyntax) go from silently over-matching to the faithful discriminated rewrite — an intentional, expected change in the self-hosted migration's own translated output, not a regression.NullConditionalChain.HopSpan/ReceiverSpanalso now throwArgumentExceptionon a non-hop node instead of silently returning a plausible-but-meaningless span (precondition hardening; the sole existing emitter only ever calls them on an already-proven hop, so this is not a behavior change there). Still explicitly out of scope, unchanged from round 2 and pending a repo-owner decision: anas ConditionalAccessExpressionSyntax/as MemberAccessExpressionSyntax/as ElementAccessExpressionSyntaxcast still gets only the warn-and-passCS2GS-ANALYZER-SHAPEtreatment, not the discriminating rewrite. Adversarial re-review of round 3 found one more unguarded site of the exact same bug class:TranslateRecursivePatternTest's OWN designator binding (is T { }/is T { ... }over a shared-node type) calledBuildPatternNarrowingReplacementdirectly instead of routing through the round-3 leaf, which is unreachable from anif-statement condition (the guard-hoist machinery intercepts that shape first) but very much reachable from any other boolean-expression position — a ternary condition over a smart-castable local/parameter receiver failed to bind at all (GS0158/GS0130) before this fix, since the designator was bound to the bare, un-narrowed receiver instead ofNullConditionalChain.AsNullConditionalHop(receiver). Fixed with the sameIsConditionalAccessAnalyzerType/IsPlainAccessAnalyzerTypebranchTranslatePatternTest'sDeclarationPatternSyntaxcase already used. -
A contravariant delegate/interface argument now raises the inferred type argument, matching
csc(issue #4133). Insidefunc f[T DisposableBase](value T),VarianceOverloads.DelegateType(value, ObjectAction())— whereObjectAction()returnsAction<object>— printedDisposableBase;cscprintsObject, because the contravariantAction<T>/IComparer<T>argument's own type is an UPPER bound onTthat must be able to raise it past the lower bound the plainvalueargument contributes.MemberLookup.FixSymbolicMethodTypeArgumentscollected both bounds correctly but picked the fixed type from the lower-bound list whenever it was non-empty and consulted the upper-bound list only afterwards, to VALIDATE that pick, never to WIN it — so an upper bound could confirm a narrower answer but never replace it.FindSymbolicInferenceCandidatenow builds its candidate set from both lists together and keeps the unique survivor every other survivor converts into, the same rule that already let one upper bound narrow another (order-independence, unaffected by this fix). The*Winneroverload does not move — only the inferred type argument does — which is why this was a narrower, separate defect from #4086's. A related permissiveness surfaced by this same change was closed alongside it: bound satisfaction was checkingConversion.Classify, which also admits ADR-0148 structural projection (constructing a new, unrelated user object through its public surface) — a VALUE conversion, not a type relationship, that must never let one generic bound satisfy another. It now checksConversion.ClassifyNonStructural, so a homonym imported type still reports its ambiguity (GS0159/GS0547) rather than silently inferring the wrong one of the two. Consulting both bound categories together also makes a NEW pair reachable that a single category alone never produced: a lower and an upper bound that are mutually convertible underConversion.ClassifyNonStructural(identity modulo a reference-nullability annotation or a tuple element name — e.g.(Key string, Value string)against(Key string, Value string?)) but notTypeSignaturesEquivalent. C#'s own fixing never treats that as ambiguous — nullability is folded in after fixing picks a type, not used to eliminate a candidate — so this case now merges through the sameMergeRecoveredTypeArgumentthe equivalent-bounds branch already used, instead of falling to the true-ambiguity branch and reporting a spuriousGS0159. -
A bare method group argument's own parameter type no longer raises a generic method's inferred type argument (issue #4133 follow-up, found by the hot-core translation guard). #4133's fix (above) taught symbolic inference to consult an upper bound alongside a lower one, but a bare method-group argument's recovered natural type is not a genuine upper bound in the first place: C#'s own method-type inference (§12.6.3.7) makes only an OUTPUT inference from a method group's return type, never an INPUT inference from its parameters.
RefineSymbolicArgsForMethodGroupsrecovers a method group's naturalFunc/Action-shaped type from its real declared signature so its same-compilation return type is not lost to erasure, and that natural type then flowed into the same delegate-shape unification #4133 touched — so the group's own PARAMETER type, not just its return type, was treated as a real upper bound.Where[TSource]overIReadOnlyList[Item]with a bare predicatepred(x Item?) bool(the nullable annotation cs2gs's oblivious-nullability heuristic adds to nullable-oblivious C#) correctly lower-boundedTSourcetoItemfrom the receiver, but the predicate's ownItem?parameter then raised it toItem?— a false ambiguity two calls downstream, reported asGS0159(tools/cs2gs/Cs2Gs.Pipeline/DeclaredProjectItem.gs). Worse, a real subtype (not just a nullable annotation) could raiseTSourcepast what the receiver actually enumerates:INamespaceSymbol.GetMembers()(a shadowing declaration returningIEnumerable<INamespaceOrTypeSymbol>) correctly lower-boundedTSourceto the narrowerINamespaceOrTypeSymbol, but a bare method group whose own parameter was the widerISymbol?raised it back toISymbol— gsc emitted aSelect<ISymbol,…>MethodSpec at a call site whose delegate creation was still pinned toFunc<INamespaceOrTypeSymbol,string>by the extension method's own receiver, an inconsistency gsc's binder did not catch but ILVerify did (StackUnexpected,Cs2Gs.Translator.EmittedNameAllocator.GetScopeNames). A newSymbolicInferenceBoundKind.MethodGroupUppercategory keeps a method group's parameter-type contribution out of the ordinary bound lists — its legitimate return-type (output) contribution is unaffected — andSymbolicInferenceBounds.Adddiscards it outright, with NO fallback:cscreportsCS0411forWrap[T](predicate Func[T, bool])called asWrap(Check)with no other argument to fixTfrom, so a same-slot "fallback" promoting the discarded bound back in whenever no other bound exists (an earlier draft of this fix) would have made gsc silently accept that call — strictly MORE permissive thancsc, the opposite direction of #4133's own defect (caught in review). Reversing an already-demotedMethodGroupUpperbound through a SECOND contravariant level (nested inside another delegate type argument) now stays demoted rather than cycling back to a real bound, so the demotion holds at any nesting depth, not only the first.HasImplicitSymbolicConversionalso now treats a pure reference-nullable-annotation difference as mutually satisfying at its own elimination gate, not only inside the later merge branch that resolves it — closing the reverse direction of the #4133 nullable-merge case above (a nullable LOWER bound against a non-nullable UPPER bound), which the gate rejected outright before the merge branch ever ran. Threaded through every CLR call-resolution path that recovers a method group's natural type for its FINAL generic instantiation (extension, instance, static, inherited and constraint-dispatched calls) rather than only the one the two reported failures exercised; one narrower internal helper — recovering a target delegate's PARAMETER shape for a second, still-unresolved method-group/lambda argument in the same call — is left on the pre-fix behavior, since it only feeds a secondary lambda-shape heuristic and no known call site depends on it. Both failures bisect to #4133's own first commit, not pre-existing. -
A
func(...)->R-shaped structural cache key no longer aliases two differently-named, same-shaped tuples (issue #4160, found alongside #4133).FunctionTypeSymbol.AppendStructuralKey's tuple case built its key fragment from element types only, never folding inTupleTypeSymbol.ElementNamesthe wayTupleTypeSymbol.Get's own cache key does (ADR-0172) — so(Id Guid, Text string)and(Id Guid, Content string)produced an identical key wherever this shared builder is used (FunctionTypeSymbol's own cache,FunctionPointerTypeSymbol, synthesized state-machine/anonymous-type/ref-delegate caches). Two textually-separate calls in one compilation, each with an explicitly-typed tuple lambda parameter of one of these shapes, could have the SECOND one silently resolve to the FIRST one's (wrong) tuple type. Confirmed pre-existing and unrelated to #4133's own change via a control build of the pre-#4133 compiler reproducing the identical aliasing. The key now folds in element names exactly asTupleTypeSymbol.Getdoes. -
paramspacking, interpolated-string-handler forwarding and named arguments now bind, in SOURCE evaluation order (issue #4134). Ten call shapescsccompiles and runs were previously a mix of rejection and incorrect ordering. Eight reportedGS0159 Cannot find function— aparamsarray of interpolated-string handlers, a namedFormattableStringin an expanded call, and every spelling of anInterpolatedStringHandlerArgument-forwarded handler except the extension-method one, which reportedGS0246and was never executed. The remaining two — a reordered named argument into aparamsslot, and itsreflvalue variant — already bound, from the erased-type-parameter fix's named-argument mapping, but evaluated in PARAMETER order rather than source order:second|first|andref|first|wherecscprintsfirst|second|andfirst|ref|. They now all bind, and with C#'s ordering rule rather than the parameter order they are reordered into:EvaluationOrder(items: first(), value: second())evaluatesfirst|second|, a forwarded handler evaluatesfirst|prefix|handler|, and a synthesized extension receiver — unreachable before, since that call never bound — is evaluated exactly ONCE (receiver|first|prefix|handler|). Aref/outlvalue in a reordered expanded call is captured through a managed-pointer local so the write-back still lands. The forward-reference rule C# spells CS8950 — a handler-forwarded argument occurring at or after the handler expression — is now reported asGS0221naming the parameter that is not a preceding argument, where the parent said only "cannot find function". Measured against a compiled and run C# twin on every row, including side-effect order. Stacked on #4086's PR: the erased-type-parameter fix is what makes the named mapping reach these calls at all, and the two rows it pinned as known-wrong (binding in parameter order) are flipped green here. -
An inferred imported generic overload now keeps a type-parameter identity conversion ahead of a genuine
objectoverload (issue #4086). Insidefunc callIt[T DisposableBase](value T),Overloads.Take(value)overTake<T>(T) where T : IDisposableandTake(object)erased the caller'sTtoSystem.Object. Both candidates therefore appeared to take the argument by identity, and the later parameter-specificity tie-break preferred the non-genericobjectslot;cscinstead picks the inferredTake<T>(T). The resolver now carries source argument symbols beside their CLR erasures and checks the candidate's OPEN parameter: a method type-parameter slot keeps identity only when its recovered symbolic argument is the same source parameter and no genuineobjectbound also fixed that method parameter; otherwise the real reference/boxing conversion is ranked. The prerequisite thatT : DisposableBaseimpliesIDisposableis supplied by #4070/#4088's rule, which this change relies on rather than duplicates. This is not a generic-over-non-generic preference: an actual or mixed-inferenceobjectargument still selects the non-generic overload. Symbolic method arguments also survive constrained imported instance/static dispatch through async rewriting/spilling into conversion and MethodSpec emission — measured, that path also stops emitting IL ILVerify rejected with[StackUnexpected] … [found value 'T'][expected ref 'object']. A neighbouring divergence surfaced by this same review — a contravariantAction[object]/IComparer[object]argument not raising the inferred type argument the waycscdoes — was pinned rather than ridden along at the time and is fixed separately, above (issue #4133). Focused regressions compile against C# sibling libraries, IL-verify, run, and cover the exact inferred/explicit repro, mixedTplusobjectinference, constrained instance/static calls, and genuine-objectcontrols. -
A
typeofattribute argument now keeps the type arguments supplied by an enclosing generic (issue #4095).@Marker(typeof(Outer[Status].Inner))compiled, IL-verified and ran, but the custom-attribute blob wroteP.Outer`1+Innerwith no instantiation, so reflection handed back the OPENOuter<T>.Innerrather than the type the source names — a wrong answer that resolves, which is why nothing threw. A nested type records the encloser's vector onEnclosingTypeArgumentsand leavesTypeArgumentsholding only what its own level declares, so the constructed-generic arm added in #4073 never fired for it. The serializer now writes the flattened enclosing-then-own vector, matching the GenericParam rowsTypeDefEmittercopies down each nesting level (ADR-0087 §3 R1). A nested ENUM takes the same repair:EnumSymbol.ConstructNestedrecords the identical vector, andtypeof(Outer[Status].Kind)wrote the openP.Outer`1+Kind. Two further shapes were wrong for a second reason and are fixed with it: the blob computed a nested name's backtick suffix from the FLATTENED parameter count while the TypeDef declares only the parameters of that level (issue #2916), sotypeof(Outer[Status].GenericInner[string])namedP.Outer`1+GenericInner`2— a type the emitted metadata does not declare, which made the attribute undecodable rather than merely wrong; the suffix is now taken from the same expressionTypeDefEmitteruses, so the two cannot drift again. An operand whose identity still depends on an IN-SCOPE type parameter —typeof(Outer[T].Inner)insideclass C[T]— now reportsGS0202instead of silently serializing the erased close as a different closed type; an unbound generic DEFINITION such astypeof(Box[_])ortypeof(Outer[_].Inner)stays legal and reifies as the open type. Every other operand shape keeps the path it had. The nested class, the nested enum, a nested type with its own argument, a three-level nesting, and enclosing arguments that are themselves a source generic, an imported generic, an interface, a delegate, a nilable value type or a nilable reference are compiled, IL-verified, read back through raw metadata and aMetadataLoadContext, and materialized at run time. -
A same-compilation enum now satisfies a
structconstraint (issue #4139).GsStruct[Color]()andclass Source : ISource[Color], whereenum ColorandISource[T struct]are declared in the same compilation, reported a falseGS0152; so did theunmanagedspelling, whose check thestructone gates. An enum is a non-nullable value type (ECMA-335 II.14.3) andcscaccepts the equivalent.IsNonNullableValueTypeForConstrainthandled type parameters and structs explicitly and otherwise fell through to a reflectiveClrTypeprobe, which a same-compilation enum does not have while binding; the imported spelling of the same shape always worked, because that one does. Same root as #4124 (a same-compilation symbol has noClrTypewhile binding, so a reflective probe answers "no" without reading the symbol), but in a sibling predicate that #4124's consolidation does not reach — measured on both sides, not assumed. It is a true prerequisite of the type-clause enforcement above rather than a separate user-visible failure: the interface-implementation spellingclass Source : ISource[Color]only becomes reachable by this predicate once that enforcement makes a type clause ask it at all. -
A method group used as an ordinary value now reports
GS0582before emit (issue #4065). Member chaining such asitems.Add.ToString(), targetless inference, ordinary object-typed arguments/returns, and targetless conditional arms previously retained an unresolved method group until IL emission, where they surfaced as the internal-compiler-errorGS9998. The binder now records whether contextual conversion supplied a delegate target and rejects only groups that escape without one. The public diagnostic names the group and distinguishes user/imported instance, static, and extension groups; successful calls, natural method-group interpolation, and delegate-targeted assignments, arguments, returns, and conditional arms are unchanged. -
Imported generic member projection now keeps both symbolic receiver arguments and their nullable flags (issues #4059 and #4066). A CLR generic constructed over a same-compilation G# type no longer reads members typed by that parameter as the binding-time
objectsurrogate, and a receiver imported as (for example)Dictionary<string, string?>no longer drops the value argument's?. Property reads/writes, fields, method returns/parameters/out, indexers, nested generic member types,.Values, enumerators andforiteration now substitute from the receiver's symbolic vector, while declaration-site nullable metadata is merged back over concrete positions. Annotated and non-annotated receivers therefore remain distinct through binding and emitted metadata. -
A class constraint reached through a dependent type-parameter bound no longer reports a false
GS0152(issue #4091). Infunc f[U SchemeOptions, T U](), constructingGsHandler[T]whereGsHandler[TOptions SchemeOptions]requiresSchemeOptionswas rejected even thoughT : U : SchemeOptions. The shared class-constraint predicate followed onlyClassConstraint, while dependent bounds deliberately live in the separateTypeParameterBoundslot. It now walks both links with reference-identity cycle detection, so every generic construction path accepts transitive class bounds without imposing an arbitrary depth limit. -
A dependent constraint now propagates the bounding parameter's
classconstraint, sonilis legal at[TBase class, TDerived TBase](issue #4062).return nilat aTDerivedslot reportedGS0155: Cannot convert type 'nil' to 'TDerived'.wherecscaccepts it: C# propagates a bounding parameter's special constraints to the bounded one, andTDerived : TBase : classprovesTDerivedis a reference type in every instantiation.[T class]on its own was already accepted (#2354's rule); the reference-ness simply did not travel down aTypeParameterBoundchain. #4043 kept that slot separate fromClassConstrainton purpose — roughly forty consumers readClassConstraint != nullboth as "this is a reference type" AND as "this is the type it derives from", and merging them is #4027's defect shape — so the repair answers only the reference-ness half, in one depth-bounded chain walk read by exactly one consumer. The emitted IL is unchanged and needed no new path:nilat any bare type-parameter target has been lowered todefault(T)(ldloca; initobj; ldloc) since #4027, and the dependent-bound body is asserted opcode-for-opcode identical to the bare[T class]one. Every sink is covered — areturn, a local initialiser, an assignment, and an argument — as are a three-link chain, a root carrying a class-BASE bound rather than the bare flag, and a method parameter bounded by its enclosing type's parameter. This is a completeness fix, never a soundness one: a chain whose root is unconstrained,struct-constrained, or interface-constrained still reportsGS0155, because those admit a value type andnilat a value-type slot is exactly what #4027 fixed. -
An imported generic method constrained to an interface now binds when the type argument is a type parameter whose CLASS bound implements it (issue #4070).
Probes.NeedsDisposable[T]()insidefunc callIt[T DisposableBase]()— overNeedsDisposable<T> where T : System.IDisposableandDisposableBase : System.IDisposable— reportedGS0159: Cannot find function NeedsDisposable.whilecscaccepts the C# equivalent.T's own bound proves the interface in every instantiation, so the candidate was legal; it was FILTERED out of overload resolution rather than diagnosed, which is why the author saw "cannot find function" instead of a constraint message. The question "does this type parameter satisfy an interface constraint" is asked in exactly two places: the generic-TYPE construction path walked the parameter's class-constraint chain (since #4037), and the generic-METHOD path did not (since #750/ADR-0088), soclass Forwarded[T DisposableBase] : DisposableConstrained[T]bound while the method call did not. That walk is now EXTRACTED and called from both, so one rule answers one question; the shared leaf that reads only the declared interface bounds is left byte-identical, because its other callers need that narrower meaning. Every arm is covered: an imported class bound, an interface inherited from the bound's own base, a same-compilation class bound reaching the interface through its imported base, and a same-compilation class implementing the imported interface directly. The rule skips a bound that MENTIONS a type parameter, which is the same skip the generic-type path applies: an unguarded first version boundProbes.NeedsEquatable[T]()underwhere T : IEquatable<T>from a same-compilationclass GsEq : IEquatable[GsEq], and that program compiled and then threwSystem.Security.VerificationException— bothIEquatable<T>and the erasedIEquatable<GsEq>project toIEquatable<object>, so the walk proved an implication that does not hold, andcscreports CS0311 on the C# equivalent. The skip is narrow: a constructed bound over concrete arguments (where T : IEnumerable<string>from[T StringBag]) still forwards, and a self-referential bound carried by the parameter itself ([T IEquatable[T]]) never reached the new rule at all. The change is monotone — it can only turn a rejection into an acceptance — so nothing the CLR refuses is newly accepted, and an unconstrained parameter, a class bound implementing a different interface, and a class bound implementing none are all still refused. No overload winner moves, which was measured on both sides rather than reasoned about: the explicit spelling of a call with a constrained and an unconstrained sibling reportedGS0159before (an explicit type argument removes the non-generic sibling from the candidate set entirely) and now picks the constrained one, ascscdoes, while the inferred spelling picks the boxing overload both before and after. Three neighbouring gaps stay open and are filed rather than ridden along: a BASE-CLASS constraint at a generic method (#4083), which lives on the non-interface fallthrough every imported generic method call takes; a DEPENDENT bound (#4084), which neither path reads; and that inferred spelling's divergence fromcsc(#4086), which this change leaves exactly where it found it. -
A generic attribute's
System.Typeargument now names the type the source wrote, not its erased surrogate (issue #4073).@JsonConverter(typeof(JsonStringEnumConverter[ConstructStatus]))over a same-compilation enum serialised the constructed generic withSystem.Int32in the argument slot, and reflection materialisation threwGenericArguments[0], System.Int32, on JsonStringEnumConverter<TEnum> violates TEnum : struct, Enum— the assembly passed ILVerify both before and after, because the defect is in the custom-attribute blob rather than in any instruction. A same-compilation type has no CLR type while binding, so a constructed generic over one closes over a surrogate and keeps the real argument only on its symbolic vector; the attribute binder preferred the CLR type whenever it was non-null, and for a constructed generic it always is. The enum is not special: measured across the argument kinds, a same-compilation CLASS wroteSystem.Object, a nested generic wrote the surrogate at depth, a two-parameter generic wrote one of each, a slice argument wroteSystem.Int32[]and a nilable enum wroteSystem.Object. Sweeping the sibling shapes found two more kinds the issue did not name. A same-compilation GENERIC —typeof(MyBox[Status])— serialised as the bareP.MyBoxwith neither arity suffix nor argument list, because a constructed user generic carries its arguments onTypeArgumentsand leavesTypeParametersempty; that name does not resolve, so the attribute could not be decoded at all, with an imported argument (MyBox[int32]) just as much as an erased one. And an attribute whoseType[]argument contained a same-compilationtypeofwas dropped from the assembly ENTIRELY with no diagnostic, because #3684 widened the constant container toobject[]to hold the placeholder and the constructor matcher then refusedobject[]for aType[]parameter. All three are fixed together, and every position is covered: positional, named, and array element alike. A fully imported argument such astypeof(Box[int32])has a faithful CLR type and keeps the byte-identical path it always took, as does a scalartypeofof a same-compilation type. A nullable REFERENCE annotation (typeof(Box[string?])) is deliberately left on the CLR path: it is metadata over an entirely faithful runtime shape and ECMA-335 II.23.3 has nowhere to record it. One shape in this family is measured and left alone — a nested type inside a generic OUTER (typeof(Outer[Status].Inner)) still loses its enclosing argument, which needs a different vector and a decision about the nested TypeDef's declared arity; it is filed as issue #4095, and closed in this same release (above). A constructed generic DELEGATE —typeof(Pred[Status])— is fixed here too, and it failed one step further along than the others: a constructed delegate preserves its type parameters, so the arity suffix was already right and the name resolved, to the OPEN definition, silently dropping the argument; nested as an argument (typeof(Box[Pred[Status]])) it named an open generic in an instantiation slot, which reifies to a type with no full name at all. Two further kinds are measured and filed rather than fixed: a G# structural type (a map, a tuple, a function type, a channel) over a same-compilation component serialises its G# SPELLING, because the symbol has no CLR type and the fallback writes the display name — four kinds, each needing its own BCL projection decided, so repairing one would leave three undecodable (issue #4098, closed in this same release); and an attribute whose arguments match no constructor is still dropped from the assembly with no diagnostic, which is what kept theType[]half of this issue invisible (issue #4097, closed in this same release). -
A G# structural type over a same-compilation component now serialises its BCL projection into an attribute blob, not its G# spelling (issue #4098).
@Marker(typeof(Box[map[string, Status]]))over a same-compilation enum wrote the DISPLAY name of the structural type, qualified to the compilation's own assembly, so reading the attribute back threwCould not find type 'map' in assembly 'P.dll'; the tuple form failed asThe given assembly name was invalid, and the function and channel forms named(Status) -> boolandchan. Each structural symbol computes its own CLR type by closing an open BCL definition over its components, a same-compilation component has none while binding, so the symbol's CLR type came out null and the serialiser fell through to a baretype.Name. These operands are only reachable NESTED inside a generic argument: at top leveltypeof(map[string, Status])is rejected byGS0202, which gates on the top-level operand's CLR type, while an imported generic closed over the erased surrogate HAS one, so the component reached emit unchecked. Five kinds are repaired together rather than one at a time, each taking its projection from the symbol that already owns it: a map becomes aDictionary<K, V>, a sequence anIEnumerable<T>orIAsyncEnumerable<T>through the same shape probe the rest of the compiler uses, a channel theChannel<T>/ChannelReader<T>/ChannelWriter<T>its direction already selects, a tuple theValueTuplefamily with the tail nested intoTRestabove arity 7, and a function type anActionor aFuncby the same void rule its CLR builder applies. Every open definition is resolved through the COMPILATION's reference set rather than the emitting host's, so the assembly named is the one the emitted assembly actually references, and the components are closed over SYMBOLICALLY — never throughMakeGenericType, which is what has no answer here. The arms are deliberately not gated on a null CLR type, and that matters:map[string, List[Status]]has a component whose CLR type is non-null — aList<object>closed over the erasure surrogate — so the map's own CLR type resolved, and the old fallback wrote a name that RESOLVED, toDictionary<string, List<int>>. A row that reads back as a plausible wrong type is worse than one that throws, and a null-check guard would have left it in place. The existing wrapper arms for slices, arrays, rectangular arrays and nilables already recurse symbolically without such a guard, for the same reason. The same kinds over components that DO have a CLR type —map[string, int32],(int32, string),func(int32) bool,chan[int32]— are what identify the trigger, and they are asserted unchanged. A function type above the shippedFunc/Actionarities still has no spelling at all and is left as it was rather than given an invented name. -
An attribute whose arguments match no constructor now reports
GS0583instead of vanishing from the assembly (issue #4097).@Many([]object{1})— whereManyAttributedeclares onlyManyAttribute(Type[] values)— compiled with exit 0 and no diagnostic at all, IL-verified, ran, and the emitted assembly simply carried noManyAttributerow on the annotated class. The author asked for metadata and got silence. Constructor SELECTION is the emitter's own applicability rule, applied over the resolved constructor set including params-array expansion, and "no candidate matched" was being treated as "nothing to emit" rather than as an error. That silence is precisely what kept theType[]half of #4073 invisible for as long as it was: a widenedobject[]container failed aType[]parameter match, the whole attribute vanished, and the only symptom was a downstream framework not seeing an annotation that was plainly in the source. The new diagnostic names the attribute and the argument types it was given, the way C# spells the same rule as CS1729 and CS1503. It travels the channelGS0546already uses — an emit-time failure that carries its own diagnostic id and anchors at the annotation — because the emit pipeline has no diagnostic bag in scope; the consequence, and it is the same limitationGS0546has, is that the first offending attribute aborts emit, so a file with several reports one. Two neighbouring silent drops on the same path are closed with it. A user-defined attribute's PRIMARY constructor was accepted on arity alone, with no argument-type check at all, so@Note(1)atNoteAttribute(Text string)reached the blob writer with an int for a string slot and came out as the internal-compiler-errorGS9998; it now reportsGS0583like every other mismatch. And a user attribute whose constructor parameter is typed as a same-compilation enum — arguments correct, program legal — was dropped in silence because that parameter has no CLR type while the blob is built. It is now EMITTED: a G# enum's underlying type is alwaysint32, the bound value is already that constant rather than a symbol, and the constructor's token and signature come from the emitted method definition, so only the value's width was ever missing, and ECMA-335 II.23.3 writes an enum-typed fixed argument as its underlying primitive. The pinned Oahu corpus carries exactly this shape (@OahuCapability(CapabilityClass.Safe)), and an earlier draft that rejected it took four of that corpus's apps red. What remains of the second new diagnostic,GS0584, was then measured rather than assumed, over a probe comparinggscagainstcscon the same shapes: every surviving case — a same-compilation class, interface, delegate, or a structural type — is one C# rejects as CS0181, "not a valid attribute parameter type". There is no legal-but-unencodable residue, soGS0584says that rather than claiming an encoder limit. It was a backstop rather than the right place — the check belongs on the attribute's declaration — and G# validated no such thing at the time; #4143, in this same release, moves the check there asGS0585and retiresGS0584's reachable population for these shapes (below). The diagnostic then immediately earned its keep: it ran the pinned Oahu corpus red on@ToString(typeof(ToStringConverterPath)), againstToStringAttribute(Type type, string format = null)— a trailing OPTIONAL parameter, which no pass of the constructor resolver admitted. That program is legal C# and legal G#, so the diagnostic was the wrong answer for it and the attribute had in fact been dropped from every Oahu assembly that used it for as long as the corpus has been pinned, silently, which is exactly the failure this issue is about. Optional parameters are now a resolution pass of their own, placed before params-array expansion because normal form beats expanded form, and the omitted arguments' defaults are written into the blob. The two forms have to be told apart carefully: "fewer arguments than parameters" is true of an omitted optional AND of a params tail absorbing zero trailing elements, so filling defaults is gated on the omitted tail being genuinely optional — a params array is not, and conflating them wrotenilinto the array slot where the expanded form writes an empty array, which compiled and IL-verified while quietly leaving an xunit theory with no data rows — ECMA-335 II.23.3 gives the fixed-argument list one entry per constructor parameter, so a reader gets the defaulted values back rather than a malformed row. Attributes that DO match a constructor are untouched, including the widened[]Type{typeof(Status)}container #3684 introduced, a faithful[]stringat astring[]parameter, a scalartypeof, an explicitly supplied optional, and a matched user attribute. -
An attribute class's constructor parameter types are now validated at the DECLARATION, and an enum-array attribute argument is accepted (issues #4143 and #4144). Both were found while measuring #4097's
GS0584down to its final population: every survivingGS0584case is onecscrejects asCS0181("not a valid attribute parameter type"), which is a declaration-time defect that G# had no bind-time check for at all —class BoxedAttribute(Value Holder) : Attribute, whereHolderis an ordinary class, compiled clean and only reportedGS0584at each USE, or not at all if the attribute was never applied.GS0585now reports it once, on the attribute class's own declaration — primary constructor and every explicitinit(...)overload alike — the waycscreportsCS0181, and it shares its valid-parameter-type predicate withGS0202's argument check rather than each spelling out the list separately: a primitive,string,System.Type,object, an enum, or a 1-D array thereof.GS0584's reachable population for these shapes is retired as a consequence, surviving only in its own tests as a backstop that can no longer be reached. Separately,@E([]Status{Status.Active})againstEAttribute(Kinds []Status)reportedGS0202— "must be compile-time constants ... or 1-D array thereof" — rejecting a shape its own message says it permits, and measuring settled that this is not one bug but two, in different code paths: a same-compilation enum element's array type has noClrTypeuntil the enum is emitted (the same erasure shape #4097's scalar fix already had to look past, now extended to the 1-D array binder, which is checked symbolically instead of by gating onClrType), while an IMPORTED enum element's array (@Days([]DayOfWeek{...})) reached a real, non-nullClrTypeand still failed, because the constant-array coercion converted the boxed value to the enum's UNDERLYING primitive rather than to a genuine boxed enum instance, andArray.SetValuerefuses to store anintinto aDayOfWeek[]slot ("Object cannot be stored in an array of this type") — a plain coercion bug, unrelated to erasure, now fixed withEnum.ToObject. Both shapes are read back through aMetadataLoadContextas the enum array the source named, not merely accepted at compile time.object/object[]— part of C#'s real CS0181 list but absent from the issue's own summary of it — is included in the shared predicate; excluding it regressed #4097's[]object{...}-argument rows (anobject[]container assignable to aType[]/string[]parameter) toGS0202, which is how the omission was caught. -
A static compound assignment through a generic receiver over a same-compilation type argument emits verifiable IL (issue #4056).
Handler[MyOptions].Field += "z", where the importedHandler<TOptions>declareswhere TOptions : SchemeOptionsandMyOptionsis a class declared in the same compilation, compiled with no diagnostic, was rejected by ILVerify with[UnsatisfiedFieldParentInst] Field parent instantiation has unsatisfied class type parameter constraints, and threwTypeLoadException: GenericArguments[0], 'System.Object', on the open Handler definition violates the constraint of type parameter 'TOptions'— a constraintMyOptionssatisfies. A same-compilation class has no CLR type while binding, so the receiver closes over theSystem.Objectsurrogate and the real construction is recorded beside it; the read spelling and the simple-write spelling both carried that symbol onto the bound node, and the compound-assignment path dropped it, so the surrogate reached emit. Both halves of the compound needed it: repairing the write alone left the read'sldsfldstill parented atHandler<object>and ILVerify stayed red. The affected shapes are wider than the issue's static field — a static property, a non-+=operator such as*=, an unconstrained generic, a same-compilation type NESTED in the argument (Registry[List[Item]]), and an in-scope type-parameter receiver inside a generic function all emitted the surrogate the same way. A receiver over an IMPORTED type argument has a real CLR type, records no symbolic view, and is unchanged. This is wrong emission of a construction the language already accepts, so no diagnostic was added and none was suppressed. -
An object literal of an imported generic over a same-compilation type argument now binds (issue #4042).
Handler[MyOptions]{Tag: "z"}reportedGS0157: Cannot find type Handler. Are you missing an import?— not a message about the literal but the fall-through for a type that could not be named at all — while the constructor spellingHandler[MyOptions]()of the same instantiation compiled, IL-verified and ran. The literal branch was gated on the type argument having a FAITHFUL closed CLR type, and a same-compilation class has no CLR type at all while binding (its TypeDef is produced at emit), so the branch was skipped entirely. That predicate is right about what it says and was being used for the wrong thing: it decides the RESULT TYPE, not whether to enter the branch. The literal now closes over theobjectsurrogate and carries the symbolic vector beside it, exactly as the constructor path has since #671, so the emitter reifies the realHandler<MyOptions>— asserted as the runtime type, not merely as the printed value. An in-scope type parameter works the same way. The three lossy-but-real spellings that the predicate does admit —[]T(#4024),[N]T(#3962) and a named tuple (ADR-0172) — take the same path they always took. Widening the branch does not widen what is accepted:Handler[NotOptions]now reportsGS0152where it used to report the wrongGS0157. A member whose type IS the erased type parameter still reads as the surrogate — on the constructor spelling too, which is pre-existing and unchanged; parity with that spelling is what the issue asked for. -
A member on an element from a dictionary's
.Keys/.Valuesnow binds (issue #4033).for v in m.Valuesfollowed by a property read on the loop element —v.Length,lists.Count,b.Label— reportedGS0158, or the internal-compiler-errorGS9998when the element was a constructed generic, for both themapand theDictionaryspelling, while the same member on the same value reached throughm[k], throughfor k, v in m, or throughTryGetValuebound fine.Dictionary<,>.Valuesis declared[Nullable({1, 0, 0})], and the two zeroes sit at theTKey/TValuepositions: those are type parameters, so the byte is a placeholder for whatever is substituted, not an annotation. Read as concrete positions they made the elementV?, and because G# requires narrowing or?.before a property read on an explicitT?(but not before a method call), the element lost its whole CLR property and field surface. The receiver's own type arguments are now projected through the open property whenever it mentions a type parameter, which is what the openmap[K, V].Keysspelling already did. The issue names a constructed generic as the failing ingredient; measured, a plainstringvalue fails identically,.Keysis affected as much as.Values, a generic key is affected, and so isSortedDictionary[K, V]— while non-nested generic collections such asList,HashSet,Queue,StackandLinkedListwere never affected. -
A
map[K, V?]over a nullable value type emits verifiable IL (issue #4035).map[string, int32?]compiled clean and then threwInvalidProgramException, because its backingDictionarywas closed over the bareint32while the literal, the indexer'soutslot and the loop variable all carried aNullable<int32>— two ILVerifyStackUnexpectederrors in that repro, one or more in every affected shape. A nullable type symbol answers its underlying type's CLR type, since a nullable reference has no distinct runtime type, and the map's construction read that directly instead of theGetEffectiveClrTypecorrection itssequence[T]sibling already used. Every value-position operation was affected — the literal,m[k],m[k] = v,m[k] = nil,TryGetValue,ContainsValue,for k, v in mandfor v in m.Values— and so was the key position,map[int32?, V]andmap[DateTime?, V]alike. Over an imported underlying such asDateTimethe wrapper is now built inside that type's own reflection context, alongside the openDictionary, for the reason issue #4023 documents. TheDictionary[string, int32?]spelling was already correct, so the two spellings of one type disagreed and assigning one to the other failed to verify; they now agree. Nullable reference values such asmap[string, string?]were never affected and are unchanged. -
A DEPENDENT generic constraint is no longer answered after erasure has collapsed two distinct same-compilation classes (issue #4041). Against the imported
Coupled<T, U> where U : IList<T>, the programvar c Coupled[A, List[Bee]]— withAandBeeunrelated classes declared in the same compilation — compiled with no diagnostic and threw at run time:System.TypeLoadException: GenericArguments[1], 'System.Collections.Generic.List`1[P.Bee]',on 'HelperLib3.Coupled`2[T,U]' violates the constraint of type parameter 'U'.A same-compilation class has no CLR type while binding, so it projects to a
System.Objectplaceholder; bothAandBeetherefore arrived asobjectand the vector handed to the constraint check wasCoupled<object, List<object>>, whereList<object>genuinely does satisfyIList<object>. The check answered honestly on what it was given — the information that distinguishesAfromBeewas gone before it was asked — and emission then reified the real types. #4016/#4031'sNormaliseErasedTypeArgsForConstraintCheckis not the culprit and this was verified rather than assumed: it operates on the vector after the collapse, which happens earlier inProjectGenericArgument. The declared bound is now kept UNSUBSTITUTED, and when it mentions another type parameter whose argument arrived erased the question is put to the SYMBOLS instead — the expectedIList[A]is built from the symbolic vector and compared againstList[Bee]'s ownIList[Bee], recursing so a nested bound such asIList<List<T>>is answered at its leaf. The check is three-state on purpose: a definitive no reportsGS0152, a definitive yes accepts, and anything the symbols cannot settle — a concrete position, a missing symbol, a VARIANT parameter (whereIEnumerable[Derived]legitimately satisfiesIEnumerable[Base]), or a BARE dependent bound at the TOP of a constraint (where the relation is assignability rather than identity, soChain[ChBase, ChDerived]must keep binding) — falls back to the pre-existing CLR comparison rather than to a rejection, because a wrong “no” would be aGS0152on a legal program. That last exit is load-bearing and measured: with it disabled and nothing else changed, the legalChain[ChBase, ChDerived]fails with a falseGS0152. It left the imported spelling of a bare dependent bound over two UNRELATED erased classes still accepted, which needs symbolic assignability rather than identity — issue #4063, closed in this same release by asking that position the same predicate the G#-declared spelling of the bound already used. The matched spellingsCoupled[A, List[A]]andCoupled[string, List[string]], the covariantCovariant[CovBase, List[CovDerived]], an open forwarded instantiation, and #4031'sAddScheme[TOptions, THandler]shape all stay green. The asserting row #4032 left pinned for this gap has been moved intoConstraintViolations, not deleted. -
A
map[K, V]whose key or value is an imported type now has its wholeDictionarymember surface (issue #4023).m.Count,m.Clear(),m.Remove(k),m.ContainsKey(k),m.ContainsValue(v),m.Add(k, v),m.TryGetValue(k, out v),m.Keys,m.Valuesandfor k, v in mall reportedGS0158/GS0159/ the internal-compiler-errorGS9998on such a map, while theDictionary[K, V]spelling of the same type — which ADR-0104 says is the same type — kept every one of them. The backing dictionary was built by closing a hosttypeof(Dictionary<,>)over a key or value resolved through the compilation's reference context, which yields aTypeBuilderInstantiationthat answers no members; it is now built inside the key/value's own reflection context instead. The failing shapes were every imported type, not only classes: an imported struct, an imported generic such asList[int32], a same-compilation class extending an imported base, and the key position as well as the value position.m[k]andm[k] = vwere already fixed by #4015 and are unchanged, andlen(m)still reportsGS0566because ADR-0174 (D13) retiredlenas a built-in. -
await for v in snow iterates anasync sequence[T]whose element has no CLR backing (issue #4020). Anasync sequence[T]parameter with an open element reportedGS0134("cannot be iterated withawait for") while the closedasync sequence[int32]spelling iterated and the synchronousfor v in sover an equally opensequence[T]iterated too. The operand check recognised the shape only through its CLR type, which an in-scope type parameter or a same-compilation class element leaves null. A diagnostic naming an async sequence now readsasync sequence[T]rather thansequence[T], so it can no longer be mistaken for the synchronous sequence — whichawait forstill correctly refuses. -
A
nilat a reference-constrained[T class]slot emits verifiable IL (issue #4027). Anilliteral is legal at a[T class]slot — deliberately, since #2354, because such a slot is a reference in every instantiation — but nothing lowered the accepted literal, so emit produced a bareldnullagainst a signature slot typed!!T(or!T). ECMA-335 has no rule admitting a null reference at a generic-parameter slot, constraint or no constraint, so ILVerify rejected it:[StackUnexpected] [found Nullobjref 'NullReference'] [expected value 'T']. The program ran and printed the right answer, which is why it went unnoticed. It is now lowered todefault(T), which the emitter has materialised asldloca; initobj; ldlocsince #774 — the same three opcodescscemits forTakes<T>(null)underwhere T : class, measured. Six sinks were affected, not the one the issue names: a call argument, a local declaration, an assignment, areturn, and — on a generic CLASS, where the slot is!T— a field write and a memberreturn. Nothing about the VALUE changes:default(T)at a[T class]slot is the null reference in every instantiation, so thenilstill arrives and the callee'sx == nilstill observes it. An UNCONSTRAINEDTstill refusesnil(#4010). -
A
map[K, V]whose key or value is an ordinary CLR type no longer crashes the compiler (issue #4015).map[string, DateTime]{},map[string, TimeSpan]{},map[string, List[int32]]{}and every map over a type imported from another assembly reported the internal-compiler-errorGS9998(NotSupportedException: TypeBuilder generic instantiation does not support resolving members). The backing type is built astypeof(Dictionary<,>).MakeGenericType(key, value), and .NET returns aTypeBuilderInstantiation— which refuses to resolve members — whenever a type argument is not itself a host runtime type, as every type the compiler resolved through its reference context is. Only a handful of G# primitives (int32,float64,bool,string,object) escaped it, which is why the shape went unnoticed. The three emit sites that reflected on the closed dictionary — the literal's constructor andset_Item,m[k]'sTryGetValue, andm[k] = v'sset_Item— now fall through to the reified-TypeSpec MemberRefs already used for a map with an open element. The issue reported this as specific to an imported reference type in the value position; it was neither specific to imports nor to reference types nor to the value position. A same-compilation class, aDictionary[K, V]()spelled directly, and the sibling slice and tuple literals were never affected and are unchanged. A separate, pre-existing binding gap on the same types —m.Count,m.Remove(k),m.ContainsKey(k)andfor k, v in mover a map whose value is a class — is tracked as #4023. -
An inferred generic call over a
mapemits a MethodSpec over the real key and value (issue #4026, second face).Probes.CountAnyMap(entries)on amap[string, Derived]— no explicit type arguments, ordinary code — compiled, ran and did NOT IL-verify: the symbolic recovery that decides what the MethodSpec is closed over had no arm for amap, soKandVwere never recovered and the spec fell back to whatever the erasure presented (ImportedBasefor a same-compilation class with an imported base,objectfor one without) while the value pushed was the realDictionary<string, Derived>. Amapis now restated as the constructedDictionary[K, V]it is (ADR-0104) before unification, so it takes the same patterns theListand[]Tspellings always took — which is why those never had the bug — andIDictionary[K, V],IReadOnlyDictionary[K, V]andIEnumerable[KeyValuePair[K, V]]formals are covered with it. -
A same-compilation class that implements an imported interface now satisfies that interface's constraint (issue #4124). For
class D : IDisposable, bothtake[D]()overfunc take[T IDisposable]()andBox[D]()overopen class Box[TB IDisposable]reportedGS0152: Type argument 'D' for type parameter 'T' does not satisfy the 'System.IDisposable' constraint.— a false rejection of ordinary, idiomatic code, which is the worse of this rule's two failure modes.cscaccepts the C# equivalent and so does the CLR.Binder.SatisfiesConstraint's CLR-interface arm bottoms out in a reflective walk overtypeArgument.ClrType.GetInterfaces(), and a same-compilation class has no CLR type until emit, so the walk answered "no" without ever reading the interface list the symbol already holds. This is the FOURTH appearance of that one blindness — #4061 repaired it at the imported class-chain walk, #4068 at the dependent-bound path (whereSourceSymbolImplementsImportedInterfacewas written), and #4092's review round at the G#-declared forwarding rule, each at the site that happened to hit it. Rather than add a fourth caller-side patch, the fallback moves INTO the shared leaf: the reflective body is split out and the symbolic walk now wraps every exit of it, and the two copies sitting at #4068's and #4092's call sites are DELETED. All three symbolic askers now bottom out in one predicate, so a fifth site cannot appear. The widening is monotone in the satisfaction direction and only consulted for an argument with no CLR type, which is exactly what reflection cannot see, so nothing the reflective path already answers changes: a class implementing nothing, a class implementing a different imported interface, and a generic bound with the wrong symbolic argument are all still refused. The source base chain and a G#-declared interface that extends the imported one both count, and a self-referential generic bound now works too — flat ([T IComparable[T]]overclass Cmp : IComparable[Cmp]) and NESTED ([T IComparable[List[T]]]overclass C : IComparable[List[C]]), both red before, on both halves of the predicate, because the argument-for-parameter substitution the two halves share is now recursive rather than a whole-argument identity test. The blast radius named by the issue was measured rather than reasoned about:BoundScope.TryUnifyAndCheckConstraintsgates extension-method candidates through the same predicate, so a constrained generic-receiver extension over a source class went fromGS0159to binding. Exactly one overload winner moves, and it moves TOWARDcsc: with a same-namedDescribe[T IDisposable](T)andDescribe(object),D().Describe()picked the boxingobjectbefore (the constrained candidate was not applicable) and picks the constrained one now, which is whatcscpicks — verified by compiling and running the C# equivalent. It is this change's move rather than #4086's: re-measured against a parent carrying #4108's fix for #4086, the answer there is stillobject, because #4108 fixed inferred type-parameter identity during ranking while this fixes candidate applicability before ranking runs. The rankings that do not move were measured too: two same-name GENERIC extensions still tie (GS0266, both sides), the free-function pair still picks the generic one (both sides), and a receiver that fails the constraint still falls back to the unconstrained sibling. -
An imported generic method constrained to a BASE CLASS now binds when the type argument is a type parameter carrying that bound (issue #4083).
Probes.NeedsScheme[T]()insidefunc callIt[T SchemeOptions]()— overNeedsScheme<T> where T : SchemeOptions— reportedGS0159: Cannot find function NeedsScheme., and so did the same-compilation spellingclass MyScheme : SchemeOptionsand the dependent spellingfunc callIt[U SchemeOptions, T U]().cscaccepts all three. This is #4070's defect one bound-kind over, on the non-interface FALLTHROUGH every base-class constraint of every imported generic method takes: aTypeParameterSymbolargument matched none of the symbolic arms, so the erasedSystem.Objectplaceholder was asked whether it derives fromSchemeOptions, said no, and the candidate was silently FILTERED out of overload resolution — which is why the author saw "cannot find function" rather than a constraint message. The generic-TYPE construction path has walked the parameter's own class-constraint chain since #4037; that walk is now extracted and the generic-METHOD path calls the SAME one. It is deliberately not a call to its parent, whoseobject/ValueTypeearly-out would be a false ACCEPT here, since on this path aSystem.ValueTypebound is the metadata shadow ofwhere T : structand is settled by the special-constraint arms before the type-bound loop runs. The new arm carries #4070's guard — the unsubstituted bound must mention no type parameter — so a CRTP base bound cannot be "proved" on the erased vector, and it is monotone in the satisfaction direction: an unbound parameter and a class bound naming a different base are both still refused. -
A dependent bound now forwards the bounding parameter's interface and base-class constraints (issue #4084).
Probes.NeedsDisposable[TDerived]()insidefunc callIt[TBase DisposableBase, TDerived TBase]()reportedGS0159: Cannot find function NeedsDisposable., while the generic-type spellingclass Forwarded[TBase DisposableBase, TDerived TBase] : DisposableConstrained[TDerived]reportedGS0580— two different wrong answers to one question, on a programcscaccepts, sinceTDerived : TBase : DisposableBase : IDisposableholds in every instantiation. Both askers read onlyClassConstraintand never #4043's separateTypeParameterBoundslot. They now share #4067's existing symbol walk, which follows both slots and terminates on a visited set rather than a depth limit, so one chain answers the question on both paths. That walk's 32-element cap is removed with it: it was belt-and-braces on top of the visited set and it was lossy, so a 40-link dependent chain was truncated and its terminal bound reported unforwarded (GS0159at a method,GS0580at a type — measured). The deduplication already visits each distinct parameter at most once, so the walk terminates on any graph without it. The base-class half at an IMPORTED generic type was still open too and is closed here: #4102 taughtSatisfiesClassConstraintto follow the dependent bound, which covers a G#-DECLARED generic, butclass Fwd[TBase SchemeOptions, TDerived TBase] : SchemeConstrained[TDerived]over an importedSchemeConstrained<T>goes through the reflective path instead and still reportedGS0580. A dependent bound forwards the bounding parameter's constraints and nothing else, so a chain ending in a class that implements a different interface is still refused. -
A suppressed body-header identifier followed by a labeled body no longer mis-parses that body as a struct literal (issue #4189).
if flag { retry: break }, and the equivalentwhile/for-in/C-style-for-post shapes, reportedUnexpected token <BreakKeyword>, expected <IdentifierToken>.: the label's spelling (Identifier :) is byte-for-byte identical to a struct literal's first field, andStructLiteralAllowedInSuppressedHeadertreated any NON-empty brace content as unambiguously a struct literal — the same lookahead that #1575 already had to add for an EMPTYIdent{}colliding with an empty body, generalized here to non-empty content that happens to startIdentifier :or.... The predicate now scans (bounded) to the candidate brace's own matching close and admits the struct-literal reading only when what follows still lets it stand — a genuine second body brace (the existing two-brace shape,Numbers{X: 1} { … }), or a token under which the surrounding suppressed expression legitimately keeps going (a binary/postfix operator, a,betweenif letbindings, a ternary's:); otherwise this{ … }is the header's own body, since a suppressed header always requires one and none would otherwise be left to open it. A spread-led brace (Ident{...expr}) reaches the same check but can never actually collide with a body — no G# statement begins with a bare...— so it keeps resolving in the struct literal's favor exactly as before. Every non-empty struct literal that is genuinely followed by more expression content (if Pt{X: 1} == p { }) is unaffected. -
A native
let ref/var refalias local is now legal in an async function, an iterator, or an async iterator as long as it is never live across anawait/yieldsuspension point (issue #4222).{ var ref alias = values[0]; alias = 42 }before anawait(or ayield) used to reportGS0258unconditionally — the same blanket "no ref alias anywhere in this state machine" rule issue #2350 already replaced for by-ref-like (ref struct) locals with per-local liveness.RefStructAsyncLivenessAnalyzernow also tracks native alias locals (identified byLocalVariableSymbol.RefKind, since an alias keeps its pointee type onType) and treatsyieldas a suspension point alongsideawait, so it runs for iterator functions too; assigning THROUGH an alias (alias = 42) is correctly modeled as a use of the alias (it needs the address), not a redefinition, unlike overwriting a by-ref-like value outright. A local actually proven live across a suspension still reportsGS0258, now with a precise reason instead of a blanket one.AsyncCaptureWalkerand the iterator rewriter's hoist-local collector both exclude alias locals from state-machine fields (mirroring the existing by-ref-like exclusion), so a confined alias simply stays a plain state-machine-method local — the pre-existingRefKind-drivenldind/stindemission handles it exactly as it would in an ordinary function, with no managed-pointer field ever emitted. The existing low-level&/*pointer-reconstruction path (RefInitializationHoister) and by-ref-like-local liveness are unchanged. -
A top-level extension function can now name the CLR type its self-hosted
MethodDefshould be filed under, instead of always the package's synthesized<Program>(issue #4234). A receiver-clausefuncbinds only at top level (its receiver is not in scope inside ashared { }block), so cs2gs lowers a migrated C# static extension class's methods to package-level G# funcs — which the emitter has always hosted on<Program>, unlike the native C# class that declared them. That is invisible to ordinary G# code, but not to a self-hosted build of the SAME method: a reflection-based test (typeof(ChannelExtensions).GetMethods()) or an analyzer's owner-specific recognition (RendezvousBatchAnalyzer'sChannelBatchExtensionscheck) that key off the exact declaring type saw the migrated runtime's shape diverge from the native assembly's. The new compiler-intrinsic@ExtensionOwner(typeof(T))annotation, which cs2gs now synthesizes automatically wherever it lifts an extension method out of a static class, names a same-package non-generic class to host theMethodDefon instead, reusing the same static-owner routing an ordinary shared-block static method already gets; the owning class is emittedabstract sealedand[ExtensionAttribute]-stamped, exactly like<Program>already is, so C#/F# extension-method discovery is unaffected. A malformed use reports the newGS9306. This mechanism exists for cs2gs's own self-hosting; ordinary hand-authored G# extension functions are unaffected and keep landing on<Program>as before. -
A generic local function calling a sibling local function that captures outer state no longer crashes
gsc(issue #4221 follow-up to #4252, which removed the blanketGS0463rejection so a generic local function could capture ordinary outer state at all).let Second[U] = func(x U) int32 { return outer }/let First[T] = func(x T) int32 { return Second(0) }— whereFirstnever readsouteritself, only callsSecond— crashed emission withGS9998: InvalidOperationException: Variable 'outer' has no local slot or parameter index in the current method., regardless of whetherSecondwas declared beforeFirst(ordinary forward visibility) or after (forward reference), or the two called each other (a cycle) — and the same crash reached one level deeper, through a plain local function declared insideFirst's own body that calledSecondonFirst's behalf, since that nested literal's own captured-variable set was fixed at its bind time and the reconciliation pass originally only re-walked the group's top-level members. Root cause: a generic local function is called directly through itsFunctionSymbol, not through a delegate value, soLambdaBinder's existing transitive-capture folding — which already walks a PHYSICALLY NESTED function literal'sCapturedVariablesinto its enclosing literal's own set — had no equivalent for aBoundCallExpressionreferencing a SIBLING local function'sFunctionSymbol; nothing propagatedSecond's capture ofouterintoFirst's own environment, soMethodBodyEmitter.EmitGenericLocalClosureInstancehad no field to readouterfrom when rebuildingSecond's closure instance at theSecond(0)call site insideFirst's body.CapturedVariableCollectornow consults a per-binder map of every local function's already-known captures (keyed byFunctionSymbol) and folds a callee's captures into its caller exactly like the nested-literal case;LambdaBinder.ReconcileGenericLocalFunctionGroupCapturesre-converges a whole consecutive-declaration group to a fixed point afterward, covering both the forward-reference and the call-cycle shapes the in-order pass alone cannot see, and now also recomputes every function literal nested inside a group member's body (not just the member's own top-level literal), so a plain local function declared inside a member that calls a sibling on the member's behalf gets the same fixed-point treatment instead of keeping the stale captured-variable set it had at its own bind time. Multiple generic instantiations of either function continue to share one captured cell;this, a captured parameter, a mutable reference-type field, and a plain local function nested inside a generic local function's own body (reading the outer state directly) all compose correctly with this and were previously untested. -
A call to a native ref-returning function/method, or a read of a native ref-returning property/indexer, is now usable as storage — an alias source, a
return refoperand, and (when the getter is writable) an assignment target (issue #4224).var ref alias = At(values, 0)(func At(values []int32, i int32) ref int32) andvar ref alias = holder.Value(prop Value ref int32 { get { return ref slot } }) used to reportGS0256unconditionally —ExpressionBinder.IsLvalueandStatementBinder.IsLvalueForRefReturnaccepted only a variable, field, array element, or*pdereference, never a call/property result, even a ref-returning one. Both classifiers (andreturn ref At(...)'s equivalent, previouslyGS0253) now also accept a call to a same-compilationFunctionSymbol/instance method or a read of a same-compilation property/indexer whoseReturnRefKind != None, via a sharedRefCapabilities.IsNativeRefReturningCallclassifier so the two do not drift. An imported/CLR ref-returning member (e.g.Span[T]'s indexer) needed no change —ConversionClassifier.AutoDereferenceRefReturnalready wraps those in aBoundDereferenceExpression, which was already an lvalue. Forwarding is escape-checked, not merely permitted:RefCapabilities.TryGetRefReturnEscapeSourcesdecomposes a ref-returning call into its instance receiver and its non-scopedref/in/outargument storage, andHasFunctionLocalRefScopepropagates through them —return ref Fwd(ref x)from inside the SAME function that declaredxstill reportsGS0254, whilereturn ref Fwd(ref value)over a caller-suppliedrefparameter, andreturn ref At(values, 0)over a by-value slice parameter, both compile, matching C#'s ref-safe-scope rules for a ref-returning call. A writable (ref, notref readonly) getter with no setter now supports assignment without inventing one —holder.Value = 42andGetHolder().Value = Next()(receiver/RHS each evaluated exactly once) store through the getter's managed pointer, reusing the existing imported-indexer write-through shape (BoundIndirectAssignmentExpressionoverBoundAddressOfExpression); a native indexer's writable getter (prop this[i int32] ref int32 { get {...} }) gets the same treatment for bothbox[i] = vand an alias. A struct receiver that would need a defensive copy to call the getter (alet-bound or otherwise non-addressable struct value) is rejected for both the alias and the assignment form — alet ref readonlyview remains allowed, since a read-only view of a copy is still sound. cs2gs'sref exprtranslator, which previously refused any call operand outright (issue #1900's own follow-up), now translates it like any other lvalue and lets gsc's binder validate it. -
out/refof aT?pointee, whereTis an OPEN, unconstrained type parameter, no longer emits an invalid reference-slot store/load for a real value-type instantiation (issue #4236, found self-hostingCompiler.Tests's channel matrix).Gsharp.Concurrency.Chan[T].ReceiveOrPark'sout value T?parameter —Tcarrying nostruct/classconstraint — compiled and ILVerified cleanly, then threwInvalidProgramExceptionthe momentTwas JIT-instantiated as a genuine non-primitive struct; a CoreLib primitive element (int32, etc.) never exercised the faulty path, which is why the native, non-self-hosted test suite never caught it. Root cause:MethodBodyEmitter.EmitStoreIndirect/EmitLoadIndirectpickstobj/ldobjonly for a pointee that is PROVABLY a value type (NullableLifting.IsAnyValueTypeNullable) or a bare boundTypeParameterSymbol— aNullableTypeSymbolwrapping an OPEN, unconstrained type parameter satisfies neither check, so it fell through to the reference-slotstind.ref/ldind.refarm, which is invalid IL once the CLR resolvesTto a real struct (a gap ILVerify's abstract-over-open-Tchecking does not catch, but the runtime type loader does).ImportedMemberRefFactory.GetElementTypeTokenalready resolved this exact shape correctly (issue #814 / ADR-0084 §L5 —Nullable[T]whenTis struct-constrained, bareTotherwise);EmitStoreIndirect/EmitLoadIndirectsimply never routed into it for this case. Both now recognizeNullableTypeSymbolover an openTypeParameterSymboland route tostobj/ldobjwith that already-correct token. A minimal, non-self-hosted regression (Issue4236OpenNullableTypeParameterIndirectTests) reproduces the crash directly against nativegsc— a generic class method with anout value T?and aref value T?parameter, instantiated over a user struct — confirmed red before the fix and green after;ilverifyreports no new finding on the fixed self-hostedGsharp.Runtime.Channels.dll(the pre-existing, trackedReturnPtrToStackfalse positive onSelectRandom.Shuffleis the only remaining entry).
Known limitations
Translating the compiler from C# to G# lost some things, by decision
(owner decision D7). A !! assertion is a runtime check (see "!! density"
and the fail-fast breaking change above), so a violated non-null assumption now
throws where C#'s compile-time-only ! carried null onward. For the changes
that cs2gs makes to any C# it translates, see
What translation changes.
!!density. The translated source has many!!assertions where the G# compiler could not prove that a value is non-null. Reducing them is ongoing cleanup.- Conditional compilation. G# has no
#if. cs2gs reports#if/#elifas an error instead of silently choosing a branch, and the compiler source was free of them before translation (three test-only#if DEBUGsites were rewritten as runtime checks). - Performance of the G#-built compiler compared with 0.4.1270 (compile time, memory), against the agreed budget (owner decision D11: aim for parity, 1.5x acceptable). Measured on the cut-over candidate: about 1.04x time and 1.02x peak memory; to be re-measured on the released bits.
0.4
The fourth pre-1.0 line focuses on sound defaults, expressive control flow, and faithful CLR interop. Collection zero values are now usable without hidden nulls, pattern matching works directly in boolean and loop conditions, rectangular CLR arrays have native syntax, and explicit extension receivers preserve extension semantics for owned types and enums. Tooling adds synchronized-map support, clearer REPL display, and dotnet watch hot reload.
Highlights
- Sound collection zero values. Initializer-less maps, slices, fixed arrays, rectangular arrays, and sequences receive usable empty instances, including collection fields nested through structs. Channels remain explicit: globals and fields require
make(chan T), while locals use definite-assignment analysis and nullable channels use(chan T)?. - Richer conditions and blocks. Boolean
isaccepts the full pattern grammar, including constant, type, property, relational, list, and composed patterns.while letintroduces a narrowed, body-scoped binding that is re-evaluated before each iteration. General block expressions can contain declarations and statements before their trailing value. - Native rectangular arrays.
[,]T,[,,]T, and higher ranks preserve CLR type identity.[rows, columns]Tallocates storage, flat initializers use row-major order, anda[i, j]supports reads, writes, compound assignment, address-taking, null-conditional access, iteration, and interop. - Cleaner extension declarations.
func extension (receiver T) Name()explicitly declares an extension for an owned type or enum without turning it into an instance member. - Migration fidelity.
cs2gsnow emits native block expressions, rectangular arrays, multi-target storage assignment,if let/while letbindings, nativeispattern variables, explicit extensions, and value-position assignments instead of synthetic spill-heavy rewrites.
Added
- Full patterns in
value is patternandvalue !is pattern, with type-plus-property composition and narrowed right-handandpatterns. - Pattern variables in boolean
is(ADR-0166):value is string text && text.Length > 3,value is { Length: > 0 } text,box is { Value: Dog d },values is [1, ..rest], and the guard idiomif !(value is string text) { return }followed by uses oftext. Variables are scoped to the regions where the match is known to have happened;Type namedesignations are also accepted inswitcharms.cs2gspreserves C#ispattern syntax and names for these shapes instead of hoisting__spillNtemporaries. while let name = nullableExpression { ... }, including multiple bindings and normal labeledbreak/continuebehavior.- General value-producing block expressions such as
{ let x = compute(); x + 1 }. - Native rectangular array types, allocation, initialization, indexing, iteration, nullable forms, metadata, and expression-tree support.
- Explicit extension receiver clauses through
func extension. - Map iteration through
for key, value in maporfor entry in map. Gsharp.Extensions.Sync.SyncMap[K,V]for goroutine-safe shared maps with atomicUpdate.- Structural display of plain user values in the REPL while preserving their emitted CLR
ToStringbehavior. dotnet watchhot reload for SDK projects.
Changed
- The
asoperator now has nullable result typeT?; useif let, keep the nullable value, or apply!!when a prior check guarantees success. - The deprecated
name = valuenamed-argument spelling is removed. Usename: value;=in an argument is an ordinary assignment expression. - Multi-target assignment accepts all writable storage forms, including fields, properties, indexed elements, nested members, and pointer dereferences.
- Nil comparison is available for every reference-backed built-in type. Comparisons against bare non-null collection types are diagnosed as statically constant.
- Legacy
print,input, andrndbuilt-ins and thestring(T)conversion are removed; use CLR APIs and explicit formatting/conversion methods. - The website specification, guides, tutorials, feature matrix, diagnostics catalogue, bridges, and tooling pages now describe the complete 0.4 surface without implementation-tracker references outside the design-decision index.
- The Docusaurus site cuts a new
0.4snapshot from the live docs. The version dropdown lists0.4,0.3, andNext.
Fixed
- Generic delegate, enclosing-generic, function-pointer, method-group, map, imported override, XML documentation, channel capture, boxing, and tuple-return emission now preserve the source type and storage semantics across compilation boundaries.
- Null-conditional delegate invocation, nullable-flow analysis, rejected-call diagnostics, interpolation spans, and source-migration output are aligned with the emitted behavior.
cs2gsnow carries widened nullable array-element types through inferred local alias chains, so maybe-nil writes through an alias widen the owning allocation and reads through any alias receive the required!!assertion (issue #4507).
Known limitations
- A non-empty rectangular-array initializer requires constant non-negative dimensions and a flat row-major element list.
- Pattern variables introduced by a boolean
ispattern (value is string text, ADR-0166) are read-only and are in scope only where their match is known to have happened; C# forms that rely on full definite-assignment data flow (for example a variable read after awhilewhose exit depends on the pattern) reportGS0532.
0.3
The third pre-1.0 line is a breadth-and-interop release. The language gains a large batch of C#-parity expression and declaration constructs, an unsafe pointer surface, partial types, and anonymous-object literals. Tooling adds the cs2gs C#→G# migration tool and the gsgen Roslyn source-generator host for native G# projects. The language server gains incremental binding, an incremental semantic-model pipeline, and a cross-session cold-start cache. This release also cuts a fresh 0.3 docs snapshot from the live docs and retires the 0.2 snapshot.
Highlights
- Unsafe pointer surface. An
unsafecontext now supports unmanaged raw pointers*T,stackalloc [n]Tproducing either safeSpan[T]or unsafe*Tstorage, thefixedpinning statement, theunmanagedtype-parameter constraint,sizeof(T), and pointer compound-assignment and cast lowering. - New expression and statement forms. Throw expressions, value-producing increment and decrement expressions
++/--, from-end indexes^n, standaloneSystem.Rangevalues such as1..3, expression-bodied members via->, generalgoto/ labels, collection initializersList[T]{…}/HashSet[T]{…}/Dictionary[K,V]{…}, and inferred-type arrow lambdas with statement-block bodies. - New declaration forms.
partialclasses, structs, and interfaces; anonymous-object literalsobject { … }; nested type declarations; user-defined conversion operatorsoperator implicit/operator explicit; user indexer membersprop this[i int32] T { get; set }; theshared { init { … } }static-initializer block; static imports throughimport Ns.Type; and top-levelprivatemapping to ILassembly/ internal. cs2gsC#→G# migration. The new translator lowers C# source to canonical G#, with construct coverage, gap triage, and a build-time strategy that reproduces generated code rather than freezing it. See the new cs2gs tooling page.- Source generators for native G#. The
gsgenhost runs Roslyn analyzers and generators against native G# projects.gsc /analyzer:<asm>spawnsgsgenas a sibling before compiling. A shared resx codebehind generator emitsResources.Designer.gs. - Language-server performance. Incremental binding, instance-keyed semantic-model memoization, a cross-session cold-start cache, completion-as-you-type triggering, and unified member resolution reduce repeated work across binder and LSP flows.
- Diagnostics catalogue extended. v0.3 adds diagnostics through the
GS04xxrange. The Diagnostics reference is reconciled against the compiler source and lists the current per-ID cause and fix detail.
Added
- Null-coalescing operator respelled
??. The null-coalescing operator isa ?? b. The earlier?:spelling is retired in that role;cond ? a : bremains the ternary expression. - Runtime array allocation
[n]T. A length-bearing[n]Tallocates a zero-initialised array at runtime, complementing array literals. - Nullable array-element spelling.
[]T?is an array of nullable elements;[]?Tis a nullable array. - Nullable function-type spelling. Nullable function types are spelled and displayed with the appropriate parenthesisation.
- Expression-tree lambda conversions. A lambda converts to
Expression[TDelegate]where the target demands an expression tree. - Delegate return-type covariance. Delegate return types can be covariant, including lambda target-typing on CLR method calls.
- Predefined type aliases as static-member receivers. Friendly numeric aliases can be used as receivers for static member access.
- Assembly-attribute parity with C#. Assembly-level attributes are accepted with C#-equivalent behavior.
- Collection spreads. Array, slice, and CLR collection initializers accept
...source, preserving lexical evaluation order and applying ordinary element conversions. - Safe structural projections. Compatible public object shapes project into safely constructible concrete targets;
Target{ ...source, Member: override }provides explicit object-spread mapping. - Explicit interface qualifier clauses. Methods, properties, indexers, events, and static interface members can use
(IFace)to provide distinct implementations without source-visible mangled names. - User-defined compound-assignment operators. Classes and structs can declare in-place
operator +=and related one-operand, void-returning instance operators.
Changed
- C#-compatible numeric conversions. Numeric literal narrowing and widening, plus implicit numeric promotion at call sites, now align with C# behavior.
- Unannotated imported reference types are nullable by default. Imported reference types without nullable annotations bind as nullable.
charbitwise and shift operators promote toint32. Enum==/!=comparisons against the integer literal0are also permitted.- The website spec, feature matrix, diagnostics reference, CLR-interop reference, guides, tour, tutorials, and tooling docs were refreshed to match compiler ground truth for the 0.3 surface; a new
cs2gstooling page was added. - The Docusaurus site cuts a new
0.3snapshot from the live docs and retires the0.2snapshot. The version dropdown lists0.3andNext.
Fixed
- Extensive
cs2gstranslator hardening across nullability promotion, extension-call lowering, deconstruction and indexer targets, pattern binding, named-argument lowering, and source-generator-shaped constructs. - Numerous
gscbinder, emitter, and interpreter correctness fixes across imported generic interface methods, nullable value-tuple boxing,data classequality andwith, overload resolution, async lambda inference, and smart-cast narrowing.
Known limitations
gsc --helpadvertises/implicitimports[+|-]; the+/-suffix form is not currently accepted by the Release parser. Use/noimplicitimports.- Migration coverage in
cs2gsis still expanding. C# source generators are reproduced at build time rather than translated, and some constructs remain on the gap-triage backlog.
0.2
The second pre-1.0 line is a syntax-and-ergonomics release. The parser, binder, and emitter absorb substantial additions; several legacy Go-flavored spellings are retired in favour of canonical G# forms; and the native-interop and default-interface-method surfaces ship end-to-end. This release also formally introduces docs versioning: the 0.1 snapshot is removed and a fresh 0.2 snapshot is cut from the live docs.
Highlights
- New language surface.
while/do…whileloops with labeledbreak/continue;if let/guard let/while letsmart-cast bindings; null-coalescing compound assignment??=; null-conditional indexinga?[i]; arrow lambdasx => body; canonical(T1, T2) -> Rfunction-type clauses; lambda binding-type inference; Kotlin/Swift-style type-declaration grammar; if-as-expression completion; smart-cast extensions; discriminated-union enum payloads;default(T)and target-typed baredefault; variadic...Tparameters in function and anonymous function-type clauses;class/struct/init()constraint flag spellings; default-interface methods; reified generics;Gsharp.Extensions.OptionalandGsharp.Extensions.Sequences; friendly numeric type aliases; and the canonical map type clausemap[K,V]. - Removals and migrations. Legacy spellings now produce focused, span-accurate diagnostics with canonical replacements so IDE quick-fixes can patch most migrations in one edit.
typekeyword for type declarations (type Foo struct { … }→struct Foo { … }).recordkeyword, replaced bydata classordata struct.:=short variable declaration, replaced bylet/var, with diagnosticGS0305.name = valuenamed-argument separator, replaced byname: value, with diagnosticGS0315.func(T) Rlegacy function-type clause, replaced by(T) -> R, with diagnosticGS0303.- Go-flavored
map[K]Vtype clause, replaced bymap[K,V], with diagnosticGS0366. static funcon interface methods is removed. Static-virtual interface members now live inside the interfaceshared { … }block. A body-lessfuncinside that block is an abstract static-virtual slot; afuncwith a body is the default. Static private helpers also move into theshared { … }block asprivate func, while instance private helpers stay directly in the interface body. The oldstatic func …shape now produces a parser error, andGS0330fires when a non-funcmember appears inside an interfaceshared { … }block.
- Body-less
funcnow requires;. Afuncdeclaration without a{ … }block is terminated by the universal no-body marker;. This already held for P/Invoke (func getpid() int32;) and now also applies to abstract interface methods and abstract static-virtual slots inside an interfaceshared { … }block. A body-less interfacefuncmissing its;reportsGS0368; afunccarrying a body still takes no;. - Native interop end-to-end. P/Invoke via
@DllImport, source-generator-shaped@LibraryImport, struct and class marshalling,ref/out/inparameter marshalling, function-pointer marshalling, and@MarshalAsparameter overrides are supported. - Go-flavored concurrency moved behind an opt-in import.
go,chan,select, channel send and receive,make(chan T), and the built-inslen,cap,append,make, anddeletenow requireimport Gsharp.Extensions.Go. DiagnosticsGS0316/GS0317point at the missing import. - Tooling polish. LSP completion understands async-shaped types such as
async (T) -> Randasync sequence[T];textDocument/codeActionoffers nil-related quick fixes; andnullnow produces anil"did you mean" diagnosticGS0273. - Diagnostics catalogue extended. v0.2 introduces
GS0273andGS0288–GS0366. The Diagnostics reference has the per-ID cause and fix detail.
Added
- Friendly numeric type aliases.
int,uint,long,ulong,short,ushort,byte,sbyte,float, anddoubleare accepted everywhere a type name is accepted, as a strict superset on top of the canonical width-bearing names (int32,uint32, …). The alias resolves to the canonicalTypeSymbolat the binder, so diagnostics,typeof,nameof, hover, and emitted IL always print the canonical spelling. Canonical names remain preferred in documentation and public library APIs; aliases are appropriate inside function bodies and local code. Aliases are reserved type names, sotype int = stringand equivalents are rejected withGS0102. - Null-conditional indexing
a?[i].a?[i]evaluates receiveraexactly once. If it isnil, the whole expression yieldsnil; otherwise the result is the indexed value lifted to the nullable form of the indexer's return type. It works on arrays, slices, maps, and CLR indexers on both emit and interpreter paths. Chained forms (h?.Data?[i]?.c) short-circuit on the first nil. The new token?[is recognized only when[immediately follows?, preservingcond ? [arr] : [arr]ternary parses. DiagnosticsGS0300andGS0301cover non-nullable receivers and assignment left-hand sides. - Documentation comments. Markdown-authored
///documentation comments round-trip losslessly to CLR XML doc. Hover renders merged documentation for both G# declarations and imported CLR APIs. New warnings includeGS0227,GS0228,GS0229,GS0230, andGS0231. - Named delegate types.
delegate Name(...)declares a real CLRMulticastDelegate-derived type so C# consumers see a conventional handler type and G# events can carry first-class custom delegate types. DiagnosticsGS0233–GS0234cover invalid forms.; ref/out/inparameters. Declaration-site and call-site ref-kind modifiers are supported, including inlineout var/out let/out _declarations. DiagnosticsGS0235–GS0243cover the rules. Passing a value to aninparameter without writinginat the call site is warningGS0242rather than a silent spill.- Ref-aliasing locals.
let ref m = arr[i]andvar ref v = c.Fieldproduce locals whose IL slots areT&and alias another lvalue. DiagnosticsGS0256–GS0258cover invalid aliases. - Ref returns.
func f(...) ref T { return ref <expr> }is supported. DiagnosticsGS0248–GS0255cover escape rules, async and iterator bans, and override matching. - Conditional ref-arguments. The narrow
ref cond ? a : bform is supported inside ref-kind argument payloads. DiagnosticsGS0260–GS0262cover invalid forms. - Generalized ternary expression.
cond ? a : bis now a normal expression.GS0259is retired in value contexts;GS0263covers "no common type" failures. - Method overloading and optional parameters. User G# functions can carry overload sets that differ by parameter types or ref-kinds, and optional parameters can use compile-time-constant defaults. Diagnostics
GS0264–GS0267cover invalid overloads and defaults. - Named arguments at call sites.
Foo(timeout: 30, retries: 3)works for free functions, user methods, user constructors, extension functions, inherited CLR methods, and delegateInvoke. DiagnosticsGS0244–GS0247cover invalid usage. The legacyname = valueform is deprecated this release with diagnosticGS0315; migrate.copy(...)and attribute argument lists alongside ordinary call sites. scopedparameter modifier.scopedconstrains aref structor managed-pointer parameter from escaping, enforced byGS9004/GS9006.data structsynthesis completed. Everydata structsynthesizesEquals(object),Equals(T),GetHashCode(),ToString(),op_Equality,op_Inequality, andDeconstruct(...). Hand-written versions are rejected withGS0232.- Editor features. Hover for CLR XML docs, live pull-based diagnostics, CodeLens reference counts on members of structs, interfaces, and enums, implicit
thisfor properties, methods, and events, hover forthis, bare static-member access from instance methods, chained-member hover, and six VS Code color themes inspired by the G# logo: Ember, Magma, and Synthwave in dark and light variants. :=short variable declaration removed. Every binding site now requiresletfor immutable bindings orvarfor mutable bindings. The lexer still recognizes:=so the parser can emitGS0305with context-sensitive migration suggestions such asx := 1→let x = 1,for i := 0 ... 10→for i in 0 ... 10, andcase v := <-ch→case let v = <-ch.
Changed
- The website spec, feature matrix, FAQ, bridges page, guide pages, and design-decisions index were refreshed to match compiler ground truth. Outdated statements such as "Parameters do not have default-value syntax" and "Named arguments — Partial" were rewritten.
- The repo
docs/lexical.mdblock-comment paragraph is corrected, and a documentation-comments subsection was added. - The VS Code TextMate grammar adds contextual keywords (
data,inline,record,delegate,event,prop,init,shared,scoped, accessor namesget/set/add/remove/raise, and ref-kindsref/out), operators (:=,?.,??,?/:,!!,...,=>), an@Annotationscope, and a///documentation-comment scope with@taghighlighting. The VS Code snippet set was rewritten to match current grammar. - The Docusaurus site cuts a new
0.2snapshot from the live docs and retires the0.1snapshot. The version dropdown lists0.2andNext; the0.1URL space is no longer served.
Fixed
- Numerous IL-emit, determinism, language-server, and editor hardening rounds improve CLR verification, byte-for-byte reproducibility, property access, CodeLens accuracy, and stale-tree handling.
Known limitations
- Full ref-safe-to-escape analysis is partial;
GS0257is reserved for a future pass. - Unsupported async state-machine emit shapes continue to report
GS0190.
0.1
The 0.1 version base identifies the first pre-1.0 line. This is not a dated stable release announcement; it summarizes the major capabilities implemented in the repository at that point.
Language and libraries
- Packages, imports, import aliases, top-level declarations, and multi-file or multi-package compilation.
- Width-bearing primitive names such as
int32,uint64,float32, andfloat64, plusbool,char,string,object,decimal,nint,nuint, andvoid. - Nullable
T?types withnil,?.,??, and!!. - Structs, classes, interfaces, enums,
data struct,recordas adata structalias, andinline structvalue wrappers. - Generic functions and types with square-bracket type parameters and arguments, constraints, method inference, and CLR variance support where applicable.
- Fixed arrays, slices, maps, tuples, function values, delegates,
sequence[T],async sequence[T], and iteratoryieldsupport. - Control flow including
if,for,for inorrangeforms, switches, switch expressions,try,catch,finally,throw,using, anddefer. - Go-shaped concurrency with
go,scope, channels, channel send and receive,make(chan T),close, andselect. async func,await, async lambdas, awaitable-shape support, andawait forover async sequences.- CLR interop for imported constructors, methods, overload resolution, fields, properties, indexers, events, delegates, extension methods, optional CLR arguments, operators, conversions, attributes, and generic types.
Tooling
gsccompiler driver with immediate execution when no/out:is supplied and saved managed executables or libraries with/out:.- Managed PE and metadata emission without Roslyn, optional reference assemblies, target-framework-aware reference resolution, runtime configuration output, and Portable PDB support.
- MSBuild SDK support through
Gsharp.NET.Sdk,.gsprojprojects,dotnet build,dotnet run, templates, and SDK-side response-file invocation. - VS Code extension and language server support for diagnostics, hover, definitions, references, symbols, formatting, completions, signature help, rename, code actions, CodeLens, semantic tokens, inlay hints, and debugging integration.
- Stable diagnostic IDs in the
GS####form, with warning suppression and warning-as-error controls.
Pre-1.0 notes
- The language is still evolving; source compatibility may change before a stable release.
- Some surfaces are intentionally documented as current implementation behavior rather than final specification guarantees.
- The Playground page exists, but browser-hosted execution is deferred.
Future release-note format
Use reverse chronological order. Each version entry should identify the version and, when a real release process exists, its date. Do not invent dates or version numbers; derive versions from the repository's release process. Write for end users: describe what changed, what it means, and any migration steps they should take.
## X.Y.Z
Short summary of the release.
### Added
- New language, tooling, documentation, or interop capabilities users can try.
### Changed
- Behavior changes, breaking changes, renamed features, or migration notes.
### Fixed
- Short grouped quality notes for user-visible correctness, diagnostics, emit, interpreter, or tooling improvements.
### Known limitations
- Important limitations users should know before upgrading.