Skip to main content
Version: Next

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). gsgen reuses 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. GSharpWorkerMemoryBudgetMiB bounds workspace concurrency estimates, not the total process heap. Direct compiler users can select --workers:N and --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_PHASES to a JSON path for nested phase timings, process counters, and input/cache metrics without changing diagnostics.

  • Suspending for … in loops dispose their enumerators (issue #5037). Loop cleanup now survives yield and await: exhaustion, early consumer disposal, break, return, and exceptions release acquired disposable enumerators. Nested iterator cleanup runs inside-out even when an inner enumerator's Dispose throws.

  • Bounded compiler reuse is opt-in (issue #5038). GSharpUseSharedCompilation reuses 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). gsgen discovers 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 try wrappers 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 NullReferenceException and 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 declared func LoadAsync() Task[Token?];, typed await store.LoadAsync() as non-null and reported a spurious GS0536 on its !!. The same loss affected abstract shared interface methods, a delegate's Invoke, auto-property getters and setters, the add / remove accessors of field-like events, interface properties, events and indexers, and a data class's Deconstruct. 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 take T? 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 let now has the same header as the if let expression (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 referenced text outside its scope. The statement and expression forms now share one header. A top-level && after the last binding starts an optional bool guard. Bindings are evaluated left to right and stop at the first nil, 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 an else whose arms both return no 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 an if branch sees its condition: && o != nil narrows o, and && o is string t puts t in 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 as if let v = a && b ? x : y used to bind the whole conditional. It now reports GS0624; write if let v = (a && b ? x : y) to keep the old meaning. Nullable assignment or conditional conversion can also hide the boolean: with cached bool?, if let v = cached = a && b now 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 an if let binding 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 let and while let don't accept a guard yet, #4968; they reported GS0157 before) and GS0626 (a top-level || after the bindings).

  • An if expression accepts the same if init; cond initializer as the if statement (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 trailing if var v = g(); v > 3 { … } else { … } at the end of an arrow-lambda block body, even though the same if compiled 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 every else if link, and not after the if. gsfmt also keeps an if init; cond header on one line in both forms; it used to put a line break after the ;.

  • Hot reload calls MetadataUpdateHandler callbacks after each applied update (issue #4960). Under dotnet 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 from UpdateApplication therefore 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# under dotnet watch. Every ClearCache runs before any UpdateApplication, 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 static void (Type[]?) shape, is reported as GSHR1005, 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, where store is declared IStore? and assigned a different implementation on each branch, hovering s.LoadAsync() could show one implementation's method (for example func (KeychainTokenStore) LoadAsync()) when the interface came from a referenced assembly and that implementation's LoadAsync was called elsewhere in the program. With the interface declared in source, hover and go-to-definition on the call showed nothing, and hovering s showed 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, so s.LoadAsync() is IStore.LoadAsync. The compiler itself was already correct, in 0.4.1270 too: s reads as IStore, and calling a member that only one implementation declares reports GS0159. Hovering the binding still shows its storage type, IStore?.

  • Blocks, arrow bodies and switch follow one rule for their last item (issues #4963, #4965, #4966, ADR-0202). A value block can now end in throw, return, break or continue: let a = if c { 1 } else { return 0 } and let 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 a switch with : 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, so return statements and a body ending in for work. A void arrow body accepts an if without else and 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 in if c { list.Remove(x) } else { list.Clear() } compile. A void switch, like a switch statement, needs no default. let a = throw e reports GS0124 instead of crashing the compiler. Compatibility: a lambda body whose last item is a nested { … } or an if let … else … now returns that block's value, as a lambda ending in a plain if … else … already did; this can change an inferred lambda type (an Action/Func overload choice) when that value is not void. End the lambda with return to 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 ??= z and the right-associative chain a ??= b ??= c used 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#, so int32? ??= 42 is an int32 and string? ??= "x" is a string; a right side that can be nil, including an unannotated (T!) value, keeps T?. 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 the if expression (issue #4964). s != nil ? s.Length : 0, s == nil ? 0 : s.Length, (s != nil && t != nil) ? s.Length + t.Length : 0 and (o is string) ? o.Length : 0 reported GS0158 (Cannot find member Length), while the same code written as if s != nil { s.Length } else { 0 } compiled. A ?: branch now sees the same nil-guard and type-test narrowing as the matching if-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 the if expression already had: with n int32?, n != nil ? n : 0 is int32 rather than int32?, which can change an inferred let/var type 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 as s != nil ? s!!.Length : 0, now reports the GS0536 redundant-assertion warning, as it does in an if branch; remove the !!. Nullable<T> members keep working on the guarded value, in every narrowing form: n != nil ? n.Value : 0, n.HasValue and n.GetValueOrDefault() bind against the Nullable<T> storage when the narrowed type has no member of that name, and now also compile after an if n != nil statement or early return, where they used to report GS0158. A member the narrowed type has itself (a struct's own Value) still binds to it. Value and HasValue also bind on a nullable same-compilation struct or enum (issue #4986). In an expression-tree lambda, (v int32?) -> v != nil ? v + 1 : 0 used to compile and then throw when the tree was built; it now reports GS0473, as the if form 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 early return, if let fn = … { fn() }, guard let), calling a (unmanaged[CC] () -> R)? local or parameter emitted calli on its Nullable<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, as Fp(), this.Fp() or h.Fp(); this.Fp() and h.Fp() reported GS0159 before. GS0536's advice to drop the redundant !! from fn!!() 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, and as / is against a function-pointer type work again (issue #4938). cast[unmanaged[Cdecl] () -> int32](address) reported GS0125 / GS0130: the look-ahead that decides whether name[...] is a generic call did not recognise the unmanaged[CC] (T) -> R clause, so cast[...] 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 called cast[T](x) no longer reports GS0521, which still applies to every other generic argument list. []unmanaged[Cdecl] () -> int32 also parses as an array of function pointers (it read unmanaged as the element's type name). Separately, address as (unmanaged[CC] () -> R)? compiled but always returned nil, x is unmanaged[…] was always false and case 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 name System.IntPtr again, the behaviour of 0.4.1150, so as from a nint yields 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.props and in the isolated src/vs-gsharp/test/VsGsharp.UnitTests project. Before the bump, NuGetAudit failed every restore of a project that pulled in 2.5.301 with NU1902 (warnings are errors). The pin stays on the 2.5 line that StreamJsonRpc 2.22.x is built against.

  • gsc embeds its own NullableAttribute / NullableContextAttribute for netstandard2.0 and .NET Framework targets, and no longer references the build host's System.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 to System.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 carry internal copies of both attributes (plus Microsoft.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.Sdk is now compiled by the gsc built from the same commit rather than the pinned bootstrap SDK, so a release ships a clean tools/task/Gsharp.NET.Sdk.dll without waiting for a second release; build/check-netstandard-host-refs.py fails the build job if the packed task assembly references any System.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's System.Private.CoreLib type, so s.Split(',') in the netstandard2.0 SDK task bound to Split(char, StringSplitOptions), which .NET Framework does not have, and Visual Studio's MSBuild failed every G# build with MissingMethodException (released in 0.5.6). Member lookup on a receiver now goes through the target's declaration (ReferenceResolver.TryGetMemberLookupType, also used for static string.X access), so the call binds Split(params char[]); a receiver whose type the downlevel target does not declare at all (a host type a dependency's signature pulled in, such as System.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 as MemoryExtensions.Contains or CollectionExtensions.TryAdd is no longer bound (and referenced from System.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.0 String.Split result is []!string! (an oblivious array) like any other unannotated netstandard2.0 member. build/check-netstandard-member-refs.py now fails the build job if any TypeRef or MemberRef of the packed task assembly does not resolve on its netstandard2.0 reference surface, and a new desktop-msbuild-sdk-smoke job builds InternalAnalyzers.gsproj with Visual Studio's MSBuild against the SDK package from the same build; publishing requires it.

  • Design decision: data class / data struct will synthesize the C# record ABI (ADR-0199, issue #3501). Planned breaking change: ToString adopts the C# record format (Point { X = 3, Y = 4 }) in place of Point(X=3, Y=4), every data type implements IEquatable<T>, declared data classes and structs get PrintMembers, and cs2gs stops emitting the @__Cs2GsRecordProvenance_4828 marker. 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 including translate, compile, ilverify or test-parity (the default). --stop-after compile still performs the redundant-!! polish and writes the migrated tree, so a cut-over rehearsal no longer pays for test parity. A run that stops before test-parity records its passing apps as unverified (not green) and prints a partial-run line. --translate-only selects the same stages as --stop-after translate but keeps its own reporting: it does not mark apps unverified, because the nightly pairs it with cs2gs validate shards.

  • The @__Cs2GsRecordProvenance_4828 marker 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 own Deconstruct (fields, then auto properties), like a hand-written data class. The Core public-API snapshot allows exactly those ten record Deconstruct additions through gsharp-core-public-api-allowed-additions.txt; any other addition still fails.

  • Breaking: data class / data struct print and compare like a C# record (ADR-0199, issue #3501). ToString now renders the C# record format, Point { X = 3, Y = 4 } (Empty { } when there are no members), instead of Point(X=3, Y=4). Values use the current culture, as in C#, so a float64 prints 1,5 under de-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 record PrintMembers(StringBuilder) slot (private on a sealed type that starts it, otherwise protected virtual), and a hand-written PrintMembers of the compatible shape replaces it (GS0623 rejects any other shape). Every data type, including the backing type of a data object or object literal, now implements IEquatable<Self>. Anonymous literals print as { Name = David, Count = 2 } with no type name and get no PrintMembers, copy constructor or clone. cs2gs no longer generates a PrintMembers method 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 ToString format, current-culture formatting, implicit IEquatable<T>, PrintMembers and 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 abstract PrintMembers), #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 .cs file referenced as data or foreign input (a None/Content/EmbeddedResource item, or a Compile item of an already-G# project such as samples/ForeignCompile/ThisAssembly.cs and 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-level out/ build-output root is no longer inventoried, so tracked scratch tools there are not discovered as apps (out/scratch/ildump is 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 to DecodeSignature(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 for yield 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 ( (which yield at statement position does not parse as a plain expression, #4705), sized array creations (printed [3]T, which yield [ 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 named item (item_2, ... when taken) instead of __yielded{N}. The __yielded family 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 TypeDefinitionHandle to EntityHandle, 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 final flags 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 prop and abstract override prop preserve 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.Exception result observed by Assert.Null remains 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's T! 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 with and .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 DataStructErgonomics and WebsiteSwift; 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.
  • 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 with and .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 separate CS2GS-GENERIC-STORE-BRIDGE report. 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/ValueTask results, 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). with preserves 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 open policy. 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 named User, 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 named Kind. 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 Invoke signatures, 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 preserve ref and ref readonly returns 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, Where and FirstOrDefault type-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). open methods, 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 $name in declarations, references, and named arguments instead of the lossy name_ rename, regardless of member visibility. Named-argument binding compares semantic names, so $scope: binds the CLR parameter scope; 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 AssemblyVersion of GSharp.Core and the other compiler assemblies to 0.5.0.0. A G# analyzer built against 0.4's GSharp.Core still loads, but reports GS9303 (an analyzer was built against a different GSharp.Core version 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 throws NullReferenceException at 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 struct print and compare like a C# record (ADR-0199). ToString renders 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 struct print 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-specific Equals(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 reports GS0158 (issue #4354). xs.Capacity += 1 on a List[int32]? and e.InnerException.Source += "x" bound with no diagnostic and failed at run time with an unattributed NullReferenceException, while the plain write xs.Capacity = 1 and 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.
  • 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 use Name = value; the existing Name: value member 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 ^x is now a first-class System.Index expression, and one's-complement is spelled ~x (ADR-0187, issue #4350). This matches C#: let last = ^1 is a reusable System.Index, consume(^2) passes one, and a standalone range may start from the end (let r = ^4..^1), so the former GS0410 restriction is retired. Binary x ^ y is still bitwise XOR, and bracket indexing (xs[^1], xs[1..^1]) is unchanged. Remedy: rewrite prefix one's-complement from ^x to ~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.Range locals, fields, parameters and expressions directly instead of reporting CS2GS-GAP, and it translates C# ~ to ~.

  • Calling an imported .NET method on a receiver declared T? now reports GS0159 (issue #4287). Examples: s.ToUpper() on a string? field, local or parameter; l.Add(1) on a List[int32]?; Path.GetDirectoryName(p).ToUpper(). These used to compile with no diagnostic and throw a bare NullReferenceException when 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 is string?), or narrow it (if s != nil { … } or if let t = s { … }).
    • Not affected: a value-type T? such as int32?.GetValueOrDefault(), whose members are safe on nil; an extension function declared over the nilable type; and an oblivious T! 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 a let memo Dictionary[K, V]? field now compiles, as the this.memo spelling already did. Before, the bare name kept its T? 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:" + o for a C# object? now translates to "p:" + o?.ToString(). A nil operand concatenates as the empty string, as in C#. The previous o.ToString() threw on nil and now reports GS0159.
  • An if join no longer lifts a non-nil local across a possible jump into either branch (issue #4290). A goto that lands after a branch's assignment could previously bypass the proof while the compiler still treated the local as non-nil after the if, producing a runtime NullReferenceException. Functions containing a user label or goto now 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 goto no 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 reports GS0158 instead of compiling and throwing NullReferenceException. 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) or Unsafe.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 with InvalidProgramException. Remedy: take &Call(...) where the address itself is needed.

  • Nullability-oblivious imported reference positions are now read as platform types T! rather than as T? (ADR-0186 step 3). --nullability=platform-types is the new default; --nullability=enabled restores 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 as T?, 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 type T!, usable as both T and T? 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-null T, a [Nullable(2)] member stays T?, and System.Object.ToString() still returns string?; a direct use of those positions binds exactly as before, but a member chain through a T? link now asks for the same proof a source-declared T? does (see the member-chain bullet below).

    • Code that previously errored now compiles. Environment.Version.ToString(), s.ToUpper().Trim(), values.Min() and xs.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-declared T?, an annotated-nullable imported member, a tuple element or an as result, and can no longer fire for an oblivious CLR receiver. GS0129, GS0296, GS0298, GS0300, GS0503, GS0523 and GS0536 each gain T! on the "admits nil" side of their test, and string! + "x" concatenates exactly as string? + "x" does — as do string! + 'c' and s += 'c' on a string! local (issue #4324).
    • Existing null-handling constructs keep compiling, unchanged. x!! on a T! is legal, is not reported as redundant, and emits the same check the compiler would have inserted anyway; if let, ?., ?? and x != nil all accept a T!, and a nil guard narrows it to T so 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, an out/ref target, an argument (including an extension method's receiver), a return, a throw operand, a lock subject, a for … in source, a deconstruction source, a delegate invocation target, or an instance-member receiver — the compiler inserts a check that throws NullReferenceException with a message naming the expression, the oblivious origin and the file:line. The exception type is deliberately unchanged, so existing catch clauses behave as they do today; what improves is the message. --platform-nil-checks=off suppresses 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 to System.Linq.Expressions as data and is never lowered to IL by the compiler, so there is nowhere for the check's dup; brtrue; …; throw to 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.Expressions emits the dereferencing form for instance member access regardless of virtuality, including for a non-virtual getter on a sealed class that never reads this — but it fails with the runtime's own unattributed NullReferenceException rather than the named message above, and a user-written x!! 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 throw NullReferenceException instead of ArgumentNullException. 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. A catch ArgumentNullException around such a call no longer matches.
    • xs.Reverse() on an oblivious List[int32] now binds a different method. Member lookup on a T! receiver runs against T, 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 than Enumerable.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, and string?.Trim() similarly stops retyping to ReadOnlySpan<char> via MemoryExtensions.
    • One overload-resolution shape can select a different method. Given a G#-declared pair func G(s string) / func G(s string?), the call G(obliviousCall()) used to pick G(string?) and now picks G(string): a T! argument is applicable to both and the more specific T parameter 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] and C[T?] are three distinct constructed types, and the only implicit conversion between them is C[T!] → C[T?]. So let 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 admits C[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 destination List[string?], or copy element-wise through the T! → T boundary so each element is checked. Reading an element through the container is unaffected and needs no conversion at all — obliviousList()[0] is string! 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-declared T? 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 reaches IEnumerable[string] silently and reports GS0154, matching a G# callee. A destination such as IEnumerable[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) and List[string]{"a", nil} now report GS0155; nullable and oblivious slots continue to accept nil.
    • 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 disable assembly is a platform array of plain string ([]!string), not of string!, and converts to []?string or []string just as the annotated BCL's does; the same holds for Enumerable.Empty[T](), Array.FindAll(xs, …) with T inferred from xs, and FsCheck's Arb.From(gen). Only the concrete container position stays T!. One consequence is a tightening: a nil tuple element passed through such a method's params T[] against a non-null sibling (AnyEqual((nil, false), ("x", false))) is now rejected, exactly as the same call to a G#-declared func AnyEqual[T](values ...T) always has been; give the sibling a nilable type ((string?, bool)) to widen T. 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 as T! — never 1, which would launder an unknown into a guarantee. Under --nullability=enabled the same byte reads as ADR-0136's T?, 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.InnerException is declared Exception? by the BCL), the write e.InnerException.Source = "src", or b.Value.Count on a Box[List[int32]?] whose plain generic T member takes its ? from the type argument. It did so with no narrowing and no nil check, so a nil link threw an unattributed NullReferenceException. That carve-out existed for oblivious members, which are now T! 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 reports GS0158 on the member after the T? link, exactly as a source-declared T? receiver always has. Remedy: assert the link (e.InnerException!!.Message, same result type), read it null-conditionally (e.InnerException?.Message, whose result is string?), or narrow it (if let inner = e.InnerException { … }). The member's own type does not change: an annotated member is still T?, and a direct read of it, or a read that flows into a T? destination, is unaffected. Under --nullability=enabled an oblivious member is ADR-0136's T? again, so an oblivious chain such as Environment.Version.Major now reports too and needs !! in that mode; the default mode reads it as T! and is unaffected. Code produced by cs2gs already carries the needed !! or ?. (#4365).
  • return this BY VALUE from a ref struct instance method is now legal, with or without @UnscopedRef (ADR-0184 D1, correcting ADR-0058). ADR-0058 rejected it with GS0219 and claimed that matched C#'s default behaviour; it does not. C# gives this a 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 one IsScoped bit. HasFunctionLocalEscapeScope now excludes the receiver parameter from its IsScoped test, and the ref-scope half moved to a dedicated IsUnscopedRefReceiver flag read by HasFunctionLocalRefScope. A genuinely scoped source is unaffected: returning a scoped parameter, or a local seeded from one, is still GS0219, inside a struct member exactly as at top level. Two existing RefSafeEscapeTests cases changed their expected outcome; one of them (RefStructMethod_WithUnscopedRef_ReturnsThis_IsLegal) had also been silently emitting GS0198 for its whole life, because it wrote @UnscopedRef with no import and only asserted DoesNotContain(GS0219).

  • A struct member that returns a reference to its own instance state without @UnscopedRef now reports the new GS0589 (ADR-0184, the CS8170 analogue), instead of GS0253's inaccurate "must be an lvalue". A reference rooted at a LOCAL still reports GS0254, and a genuinely read-only (let) field still reports GS0253 whether the member is annotated or not — @UnscopedRef lifts the escape-scope restriction only.

  • @UnscopedRef on a declaration where it means nothing is rejected with GS0590 (ADR-0184), rather than silently ignored: a class member, a shared (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 @UnscopedRef only 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 letting IBase[A] satisfy IBase[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 implicit value parameter before accessor symbols exist, and imported ScopedRefAttribute metadata 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 out parameter by reference now reports GS0254, and an out argument no longer constrains a ref-returning call. ref and in parameters use the return-only ref scope, while a ref parameter of ref-struct type has a separate function-local value scope and reports GS0219 if returned by value. @UnscopedRef widens the applicable defaults, including the ref-struct value scope; it is rejected with GS0590 on by-value and explicitly scoped parameters. Constructed generic declarations preserve this contract through substitution. Imported modules opt into the implicit defaults with RefSafetyRulesAttribute(11); markerless modules retain legacy caller-scoped by-reference parameters, and explicit ScopedRef/UnscopedRef metadata remains authoritative. G# does not yet emit RefSafetyRulesAttribute, 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 @UnscopedRef work 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() } — where Slot is a legal @UnscopedRef member returning a reference into Acc'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. Real csc rejects the C# analogue with CS8156. 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 — an in parameter, a ref readonly alias, or a ref readonly call/property result — and the called member is not itself a readonly member, which (G# has no readonly func) is every native G# member. Three sites encode that rule: MethodBodyEmitter.EmitInstanceReceiver, MethodBodyEmitter.EmitConstrainedTypeParameterReceiver (which has no isReadOnlyCall opt-out at all, since a constrained.-prefixed receiver is addressed through a temp regardless), and ReflectionMetadataEmitter.NeedsRvalueReceiverSpill. StatementBinder.HasFunctionLocalRefScope modelled 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 making return 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 new IsDefensivelyCopiedReceiverForwarding. GS0591 is reported rather than the generic GS0254 for the same reason GS0589 got its own identity: the defensive copy has no spelling in the source, so GS0254's "function-local storage" would point the author at storage they never wrote, whereas GS0591 names the copy and the remedy (take the receiver by ref rather than in, 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 the in-parameter spelling of the same forward was still accepted and dangled identically — an independently pre-existing bug, not a regression from this ADR. The Span[T] shape is deliberately NOT affected: func first(in s Span[int32]) ref readonly int32 { return ref s[0] } stays legal, because a Span[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 from y, the caller's own storage. This is NOT the CS8156 case — real csc ACCEPTS the C# analogue (verified directly, in the same file where it rejects the Slot() shape above), because C# gives a non-[UnscopedRef] struct method's this a scoped ref ref-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, @UnscopedRef or not — pre-existing behaviour, not introduced here: the by-VALUE receiver spelling of the same shape already reported GS0254 before this change. The defensive copy makes an in receiver function-local too, so the in spelling 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: a ref (rather than in) 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/in PARAMETER of its enclosing function is now rejected with GS9010 instead of silently compiling a lossy by-value snapshot (issue #4259, found auditing #4221 for DoD coverage). LambdaBinder's capture-legality checks already rejected a ref struct capture (GS0219), a managed-pointer capture (GS9004), and a fixed-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 of x's value, not a live reference to the caller's argument), since neither prior check inspects ParameterSymbol.RefKind and a plain parameter's own Type carries no by-ref marker. The check is scoped to ParameterSymbol instances with RefKind.Ref/Out/In specifically, so it does not affect a ref/var ref LOCAL alias capture — a distinct, plain-LocalVariableSymbol capture kind with its own (separately handled, and per #4222 sometimes legal) rules — nor a struct instance method's implicit this (constructed with RefKind.None, verified with a struct-method closure repro).

  • A closure that captures a ref/var ref LOCAL alias of its enclosing function is now rejected with GS9011 instead of silently compiling a lossy by-value snapshot (issue #4271, the sibling gap #4259/GS9010 deliberately left open — a ref-alias local is a plain LocalVariableSymbol, not a ParameterSymbol). 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 printed 1 instead of 2 — the alias's slot holds a managed pointer, and CaptureBoxingRewriter.IsBoxable refuses 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, not LambdaBinder's capture-legality checks). The whole .gs corpus was swept: 0 files use var ref/let ref at 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], where interface 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 with TypeLoadException: GenericArguments[0], 'T', ... violates the constraint of type parameter 'TI'. It now reports GS0580, exactly as the class spelling has since #4067, and class Holder[T SchemeOptions] : IGsBox[T] keeps binding. Separately, class Bad : GsHandler[Unrelated] and var f GsHandler[Unrelated] over open class GsHandler[TOptions SchemeOptions] compiled and emitted IL that does not verify ([UnsatisfiedMethodParentInst] ... Method parent instantiation has unsatisfied class type parameter constraints); the constructor spelling GsHandler[Unrelated]() has always reported GS0152, 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 spells GS0152, and CS0314, which it already spells GS0580.

  • 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 whenever GsLate was declared BELOW it and reported GS0580 when 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 .gs corpus was swept with the fixed build: 197 files, 170 emitting an assembly, 0 occurrences of GS0152 and 0 of GS0580.

  • 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], where open 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 same GS0580, 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 by Type.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 .gs corpus 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, and var c Chain[ChA, ChB] over two unrelated same-compilation classes compiled and threw TypeLoadException naming TDerived the 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 reports GS0152 naming the bound the author wrote — TDerived does not satisfy the TBase constraint — rather than the System.Object the erasure substitutes into it. Chain[ChBase, ChDerived], Chain[IFoo, ChImpl], Chain[object, string] and Chain[object, int32] all keep binding; an indeterminate answer still falls through to the pre-existing comparison rather than to a rejection. The whole .gs corpus 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 imported Handler<TOptions> declares where TOptions : SchemeOptions, compiled with no diagnostic and emitted an extends row that names an instantiation which is not valid for every T; 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 new GS0580, 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 from GS0152 (#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 against csc, not CS0311 as the issue reported, because CS0311 is the concrete-argument case G# already spells GS0152. The same measurement set the scope: csc reports 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 .gs corpus was swept: 0 files affected.

  • G# formatting is now canonical and option-free (ADR-0179). gsfmt and 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 .gs files by default.

  • chan T is respelled chan[T] (ADR-0174 D2). The element type moves inside brackets like sequence[T] and map[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 and chan[T?] a channel of nullable — the (chan T)? grouping carve-out is gone. The legacy spelling is rejected with GS0567, which names the exact replacement.

  • make(chan T[, n]) is retired in favour of chan[T]() (rendezvous — capacity 0, Go's unbuffered channel), chan[T](n), and Chan.Unbounded[T]() (the wave-1 behavior of make(chan T), now named). GS0566 names the replacement per site; note the semantic change for the no-capacity form.

  • close(ch) is retired in favour of the member ch.Close() (GS0566). Closing twice throws (Go's panic); Dispose() is the idempotent close, so using let works. len(ch) / cap(ch) become ch.Length() / ch.Capacity on 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.Go is gone (ADR-0174 D13): the concurrency syntax is the language, the namespace and its marker type are deleted, and the gate diagnostics GS0316 / GS0317 are retired. The concurrency library lives in the implicitly imported Gsharp.Concurrency namespace (/noimplicitimports disables it).

  • len, cap, append, and delete are retired (ADR-0174 D13). A call reports GS0566 naming the member replacement for that site: xs.Length, m.Count, m.Remove(k), ch.Length() / ch.Capacity; append has no member spelling — a slice is a fixed CLR array, so keep a growable List[T] and .Add; cap on 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 a chan[T] parameter, a ([]T)? to a []T one, or a T? to a reference-constrained [T class] one used to compile with no diagnostic at all, while every closed spelling of the same call reported GS0154. It was not only a missing message: a nil reached the non-nullable slot and failed at run time. All of these now report GS0154, 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 != nil check 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 UNCONSTRAINED T? is deliberately unaffected — it erases to Nullable<T> and keeps the value-type rules.

  • A nullable map, sequence or async sequence at 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; a nil reached the non-nullable slot and threw NullReferenceException at run time. Second, and contrary to the issue's own diagnosis, the CLOSED sequence spellings were not being rejected either: (sequence[int32])? at a sequence[int32] parameter, and a plain imported IEnumerable[int32]? at an IEnumerable[int32] parameter, were both classified as an IDENTITY conversion by the sequence[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 report GS0154 (or GS0155 at an initializer), naming the parameter and both types; the remedies are the usual ones — !!, a nullable-typed parameter, or narrowing with != nil or an assignment from a non-nullable value. Note the migration this implies for unannotated imported libraries: an IEnumerable<T> returned by a C# assembly with no nullable annotations arrives as IEnumerable[T]? (see Unannotated imported reference types are nullable by default, 0.3), so it now needs !! or a narrowing check at a non-nullable sequence[T] or IEnumerable[T] parameter — exactly as that same library's string return already does at a string parameter. The alias itself is untouched in the directions that drop nothing: sequence[T] and IEnumerable[T] still convert to one another as identity, and both still reach an IEnumerable[T]? slot, so LINQ flowing into a sequence[T] parameter is unaffected. Measured blast radius across all 192 .gs files in the repository and the SDK's own G# sources: zero.

  • A bare nil literal 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 rejects nil — 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: a nil LITERAL has no annotation to inspect, so the nil argument in takesChan[T](nil), takesSlice[T](nil), takesMap[K, V](nil) or takesSeq[T](nil) was still passed through unchecked whenever the substituted parameter type mentioned a type parameter — while every closed spelling of the same call reported GS0154. It was unsound, not merely silent: forwarding takesMap[K, V](nil) out of a generic and instantiating it at [string, int32] compiled, IL-verified, and threw NullReferenceException when the callee read m.Count. All of these now report GS0154. Three shapes the issue did not name change with them, each of which its closed sibling already rejected: an UNCONSTRAINED T parameter, a fixed array [N]T, and a function type (T) -> T. nil still reaches every null-tolerant open shape — a reference-constrained [T class], a T?, a chan[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 .gs files under samples/, src/Sdk/, bench/, e2etests/ and test-assets/: zero.

  • Two constructions of one generic type no longer structurally project to each other (issue #4014). let zs List[string] = xs for an xs List[int32] used to compile, IL-verify, run, and print 0 — the ADR-0148 projection planner answered yes because List<int> and List<string> share a parameterless constructor and a settable Capacity, so the "conversion" built a NEW, EMPTY List<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 (GS0490 at an assignment, GS0154 at 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 user class Box[T] still projects Box[int32] to Box[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 imported Box[T] whose settable Value is T — 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 same GS0490 naming the member. One pair that was already rejected changes diagnostic id: this pair at an IMPORTED CLR parameter moves from GS0155 to GS0159, because the candidate is now never applicable rather than applicable-then-unconvertible. Measured blast radius across all 180 .gs files under samples/, src/Sdk/, bench/, e2etests/ and test-assets/: zero.

  • A generic instantiated over a fixed-length array is a distinct type per length (issue #3962). List[[3]int32] and List[[4]int32] used to be the SAME type and converted to one another silently: [N]T is backed by the plain CLR T[], so both spellings resolved to one List<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 report GS0155/GS0154 naming both lengths. This holds through a variant interface (IEnumerable[[3]int32] does not convert to IEnumerable[[4]int32] despite IEnumerable's covariance, and IComparer likewise contravariantly) and for a static generic receiver (EqualityComparer[[3]int32].Default is not an EqualityComparer[[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]int32 converted implicitly to [4]int32, []int32 converted implicitly to [3]int32, and var lit [3]int32 = [2]int32{1, 2} was accepted with lit.Length reporting 2. [N]T is backed by the plain CLR T[] — the same type as []T and 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]T converts implicitly to []T (dropping a known length for an unknown one is a representation no-op, so a fixed array still reaches every []T slot), and nothing converts implicitly INTO a [N]T except a [N]T of the same length. Rejected pairs report GS0156 (or GS0154 at a call argument) and keep their explicit cast[…], exactly as slice covariance does; the cast reinterprets rather than converts, so a [3]int32 cast to [4]int32 still reports .Length 3. An array recovered from METADATA is exempt in both directions — a G# [3]int32 still reaches a C# int[] parameter and a C# int[] return still fills a [3]int32 slot. The same change also closes two gaps it found. [2]Foo to []Foo for a same-compilation Foo used 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 report GS0155 — no conversion at all, so cast[[]Base](d) did not compile either — and now reports GS0156 with a working cast, matching what []Derived -> []Base has had since #2516; element invariance for implicit conversions is unchanged. Zero of the 175 .gs files under samples/, src/Sdk/, bench/, e2etests/ and test-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 threw ArgumentException at run time: List<T> implements the non-generic System.Collections.IList, whose Add(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 generic Add(T), resolution fell through to the explicit implementation and silently widened the call site to object. 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 reports GS0159, 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 new GS0577, 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, since Dictionary<K, V> implements Contains only explicitly. Measured blast radius over every .gs file under samples/ and e2etests/: 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 a Dictionary[K, V] through the explicit ICollection<KeyValuePair<K, V>>.Add; and a call whose argument carries a same-compilation type, where IList.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 as List<Action<object>> and the lambda as Action<int>). Both turn on something the call site has rather than on the member, so the reported hole — a genuine string at a genuine int32 — 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, with xs[0].Length reporting 4: List[[3]int32] keeps [3]int32 in 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]int32 has a perfectly real int32[] CLR type, which is exactly the problem, since that type is shared with []int32 and with every other length. Add's parameter therefore stayed the int32[] reflection reports off the erased List<int32[]>. It is now the [3]int32 the receiver carries, so a differing length reports GS0156 (and []Foo at a [3]Foo slot does too), with cast[…] still available exactly as at the bare level. Stack[[3]int32]'s Push isolates the rule — List[T] also exposes IList.Add(object), which accepts anything (#4013). Second, List[[3]Foo]().Add([3]Foo{…}) for a same-compilation Foo reported GS0159 "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 []T and [N,M]T had that ride-through while [N]T did not — so resolution never ran at all. It now presents as object[], exactly as []Foo already did; what a genuine object[] parameter accepts is unchanged. The third sub-case of the issue — List[[]int32] converting to List[[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 to List[[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]T type argument symbolically, so List[[3]int32] carries [3]int32, but a slice argument was still erased to T[]. That left List[[]int32] carrying an EMPTY symbolic argument vector — which is exactly what a List<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 report GS0155/GS0154 naming 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]int32 still widens implicitly to []int32: a generic's type argument is matched by identity, not convertibility, so List being invariant in T makes the two instantiations as unrelated as List[Derived] and List[Base]. The sharpest case is a COVARIANT interface — a List[[3]int32] at an IEnumerable[[]int32] parameter is now refused, even though IEnumerable is covariant and read-only and the bare element conversion is implicit — which is the same line #3962 drew when it refused IEnumerable[[3]int32] to IEnumerable[[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 []T and the CLR T[] describe the same shape and need no symbolic rescue; member parameters, indexers and iteration over a List[[]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. So Stack[[]int32]().Pop() now surfaces []int32 rather than the erased int32[], and assigning it to a [3]int32 goes 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 explicit cast[[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 emitted NullableAttribute byte arrays for List[[]string], List[[]string?] and List[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 .gs files under samples/, src/Sdk/, bench/, e2etests/ and test-assets/ change a single diagnostic.

  • A map[K, V] reaches the interfaces its Dictionary[K, V] backing implements, not only the class (issue #4011). var d IDictionary[K, V] = m reported GS0155 whenever the key or value was still open, while the closed spelling of the very same conversion (map[string, int32] at an IDictionary[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 a map[K, V] IS a Dictionary[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. A map[K, V] now converts implicitly to IDictionary[K, V], IReadOnlyDictionary[K, V], and ICollection / IReadOnlyCollection / IEnumerable of KeyValuePair[K, V] (and to sequence[KeyValuePair[K, V]], the alias spelling of the last) whenever the elements match, in that direction only — an IDictionary[K, V] is not necessarily a Dictionary[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-nullable IDictionary[K, V] now reports GS0154 instead 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 reported GS0154, so the two now agree. At an imported parameter nothing changes and nothing disagreed: both spellings are accepted, before and after, which is BindClrParameterConversions' 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 imported Handler<TOptions> declares where TOptions : SchemeOptions, compiled with NO diagnostic and threw TypeLoadException: GenericArguments[0], 'System.String', on 'Handler`1[TOptions]' violates the constraint of type parameter 'TOptions' the first time the program touched the type. Type.MakeGenericType validates constraints for a live runtime definition and does not for one loaded by a MetadataLoadContext, which is where every /reference assembly 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 reports GS0152 naming 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 call Handler[string](), a static member through a closed receiver Handler[string].Describe(), and an object literal Handler[string]{…}. Only CLOSED instantiations are asked: a type argument that still mentions a type parameter is erased to an object placeholder, 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 its object surrogate — so class MyOptions : SchemeOptions still satisfies where TOptions : SchemeOptions. The GS0149 half of #4032 as reported (a SATISFYING class MyHandler : Handler[MyOptions] rejected as "not generic") did not reproduce on 565b9a04 across 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 with UnsatisfiedMethodParentInst (issue #4037, the forwarded spelling class H[T SchemeOptions] : Handler[T] verifies); a DEPENDENT constraint over two distinct same-compilation classes (Coupled[A, List[Bee]] against where U : IList<T>) was still accepted because both erase to the same object placeholder (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, reporting GS0157 (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 identical GS0152 three times for System.Nullable[string].Value and twice for Handler[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, and System.Nullable is of course generic — it is the argument that is wrong. That fallback is now suppressed when the failure has already explained itself, so System.Nullable[string].Value reports GS0152 alone. A genuinely non-generic type named with type arguments still reports GS0149. 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 two GS0125s, and Handler[string]() adds a GS0130 — tracked as issue #4051.- after(d) and tick(d) round a delay or period UP to a whole millisecond (ADR-0174 D8/D9 erratum 46, issue #4008). System.Threading.Timer truncates a TimeSpan to 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 the Timer.Change contract disables periodic signalling. A tick(d) with any 0 < d < 1ms therefore 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-millisecond tick now 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 a TimeSpan, so these are tick(TimeSpan.FromMilliseconds(1.5)) and so on.) after moves 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, and tick still rejects a period of zero or less with ArgumentOutOfRangeException — the same shape Go's ticker panics on. Rejecting a sub-millisecond period was the alternative and was declined: it would make tick partial on inputs Go's time.Ticker accepts, 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, printed any:0 — and ILVerify rejected the assembly (StackUnexpected: a List<Derived> found where a List<ImportedBase> is expected). The type argument is genuine, so the MethodSpec really is closed over ImportedBase and the parameter really is the INVARIANT List<ImportedBase>; only the argument's erased List<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 reads List<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 reports GS0159 — the same diagnostic List[Derived] at a genuine List[ImportedBase] parameter already reported. The map spelling 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 an IEnumerable<T> parameter, and Probes.CountArray[ImportedBase]([]Derived{…}) at a T[] 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 — so List[int32]{"x"} and Dictionary[string, int32]{"a": "x"} went on compiling and throwing ArgumentException from inside IList.Add(object) / IDictionary.Add(object, object), as did List[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 []Celsius spread into a List[float64], whose user-defined conversion the erasure hid). Everything else reports GS0159, which is what the author-written List[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, so Dictionary[K, V](){ ...pairs } goes on filling through ICollection<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 threw ArgumentException from inside the BCL, while the spread List[float64](){ ...someCelsiusSlice } printed the value — the same pair of types, two answers. A same-compilation struct has no CLR type while binding, so Celsius presents to overload resolution as object; against a List<double> that is an IDENTITY match for System.Collections.IList.Add(object) and no match at all for Add(double), so the widening member won and the Celsius was 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 binding Add, 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 real double beats a boxed one. Six neighbouring forms close with it, two of them previously silent rather than loud: the collection literal List[float64]{ celsius }, Dictionary[string, float64]().Add("a", celsius) and list.Insert(0, celsius) threw; list.Contains(celsius) answered False and list.IndexOf(celsius) answered -1; and list.Remove(celsius) reported GS0124 because IList.Remove is void while ICollection<T>.Remove returns bool. 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, so lookup.Add(value: someCelsius, key: "a") asked whether a Celsius fits key: string and reported GS0159 for 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 reordered Dictionary[string, int32]().Add(value: someCelsius, key: "a")) reports GS0159.

  • A same-compilation user-defined implicit conversion now participates in ordinary imported CLR method overload resolution (issue #4054). Math.Abs(someCelsius) reported GS0159 even though the same Celsius -> float64 operator 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 its object erasure and rejected Abs(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 and params methods and both value- and reference-returning conversion operators; unrelated conversion targets remain ambiguous and a target with no conversion still reports GS0159.

  • 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 GS0152 stands alone. At the unqualified ones it did not: the resolver returned "not a type" after it had already REPORTED, so the binder re-read Name[Arg] as an ordinary index expression and looked Name and Arg up as values — telling the author GS0125: Variable 'Nullable' doesn't exist. and GS0125: 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 report GS0152 exactly once and nothing else: the eight in the issue plus a static-field read, write and compound assignment (GS0125 x2 each) and the generic-name receiver shape Handler[string?] (GS0124 + GS0159). The constructor spelling Handler[string]() is one resolver over — it added GS0130 Function 'Handler' doesn't exist about a type that plainly exists — and now claims the call as a refused CLR construction. Nothing individually correct is suppressed: GS0152 is 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 call System.Nullable[int32]() reports GS0157 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") emits Handler<object> and throws TypeLoadException even 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 extension contextual keyword, GS0103, and GS0314 are all retired (ADR-0182, issue #4240). ADR-0024 and ADR-0079 gave func (r T) M() { ... } an ownership-dependent meaning: an instance method (with a soft GS0314 nudge toward the in-body form) when the current package owns T, an extension otherwise, and an outright GS0103 rejection 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 contextual func 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, the delegate type-alias spelling) — and collided with ordinary use: the website's own Trail sample uses extension as 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 any T — 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. GS0103 and GS0314 are retired outright — the ownership-dependent binding rule they policed no longer exists — and the reserved IDs are pinned in DiagnosticIdUniquenessTests so they cannot be reused, the same way ADR-0174 retired GS0316/GS0317. The func extension (...) marker is itself a retired spelling: the parser recognizes the shape and reports the new GS0587 rather than silently accepting it, then recovers by parsing the declaration exactly as if extension had not been written — one clean diagnostic, no cascade. extension is an ordinary identifier again everywhere else: func extension(x int32) int32 { ... } (a plain function named extension) and func (s string) extension() string { ... } (an extension method named extension) 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-0182 GS0314-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, override loss, 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-tree GS0314 trigger 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-clause func, 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's Current, an awaiter's GetResult(), and imported CLR operators. The list and the remedy (narrow, assert with !!, or make the destination T?) are in the Fixed entry "Several imported members now keep the nullability they declare" below.

  • A derived class can no longer reach a protected instance member through a receiver typed as the base class (issue #4453). Inside class Derived : Source, s.f, s.M(), s.P and s.E += h with s Source (or with s of 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 static protected members 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# #if and #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 #if and #elif, including those nested in inactive arms, now reports the CS2GS-CONDITIONAL-COMPILATION translation 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 DEBUG that guards behavior only valid for optimized code can test DebuggableAttribute.IsJITOptimizerDisabled on the assembly. This repository's allocation tests do that, but it reflects the optimizer setting, not the DEBUG symbol.
    • cs2gs migrate --allow-conditional-compilation downgrades each site to a non-fatal warning in stderr and translate.log and 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 #if in an analyzer test snippet, an orphan source, or a library's .Tests project translated for parity is still an error.

Added​

  • abstract class and shared class modifiers (ADR-0195, issue #4674). abstract class Walker { … } is inheritable and cannot be instantiated even when it declares no abstract member (Walker() and Walker{} are GS0386). shared class Helpers { … } is the spelling of a C# static class: it is emitted CLR abstract sealed, cannot be instantiated, and its body is the shared member list, so every field, const, property, event and func declared in it is shared, init { } is its static initializer, and a shared { } block inside it is rejected (GS0617, which also covers init(…), primary constructors, deinit and open/override members; GS0618 on open/sealed/abstract/data; GS0619 on a base type; every part of a partial shared class carries shared, GS0479). Both are contextual and valid only on a class head. cs2gs now emits abstract class for a C# abstract class (previously dropped, leaving an instantiable open class) and shared class with flat members for a C# static class (previously an instantiable class with a shared { } block), so a migrated library keeps the CLR shape of those public types; non-static classes keep their trailing shared { } block.

  • cs2gs capture-test-oracle and 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> and cs2gs 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 validate now 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 exact Gsharp.NET.Sdk version, such as a published release, instead of the newest locally built package, and can write that pin under msbuild-sdks in the migrated repository's root and nested global.json files instead of into every generated project file. All nested scopes use the same version while retaining their other settings. cs2gs validate follows the pin recorded in the migrated tree, rejects missing or conflicting nested pins, and accepts --sdk-version to 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 .gsproj files use the same pin; template payloads, including project/item templates and their global.json files, 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 delegate Invoke(x), dict[k] = v, a lambda returning into Func<..., TResult>, an array element (new[] { a, b }, arr[i] = v), a collection initializer or expression element, or an iterator yield, 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-BRIDGE line in the app's translate.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, expanded params elements, 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 to params parameter variables and direct carriers passed to params indexers 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-preserving as, ??, ?., 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 as unknown-target, so the list over-approximates rather than missing a site.
  • go2gs analyze now provides the M0 typed Go inventory (ADR-0191, issue #4333). The separately versioned Go helper pins golang.org/x/tools/go/packages and 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, while migrationReady remains 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 executing go version or go env. The captured executable must identify as native cmd/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 GiB RLIMIT_DATA ceiling 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 uses go/packages only for metadata with CGO_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 selected cmd/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 nonempty goExperiment/goDebug settings or helper build-time GOEXPERIMENT/DefaultGODEBUG overrides 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), preventing go/packages query 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 UNC file: 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, including localhost text outside the actual file authority. Operational local-replacement rewriting makes only the mirrored go.mod owner-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 publish go/types' effective per-file language version. Profile and goFlags tags share one canonical selection set, architecture feature levels expand to cmd/go-equivalent cumulative tool tags, and complete artifacts require a typed *ast.File root 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 between compiled role and owning-package compiledFileIds, 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-aot is 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 a go2gs translator implementation.

  • @GeneratedRegex works in native G# projects (ADR-0192 follow-on 2, ADR-0145 amendment). Declare the method as a partial method in a shared block, shared { @GeneratedRegex("\\d+") private partial func Digits() Regex; }, and the SDK runs the real Regex source generator through gsgen. The generated implementation becomes a G# implementing part that pairs with your declaring part, so calls run through the generated Regex subclass. The same applies to other generators that implement partial methods, such as @LoggerMessage.

  • A partial data class or partial data struct can hold partial methods, and @GeneratedRegex works in one (ADR-0192 amendment). This is the G# spelling of a C# partial record with a [GeneratedRegex] method, e.g. public sealed partial data class GitHubUrl(Owner string, Name string) { shared { @GeneratedRegex("^https://...$") private partial func Pattern() Regex; } }. gsgen now shows generators a data type as a C# record when 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 gets data so it merges with yours. A positional parameter list a generator adds to a data type that already has one is reported as GS9209. Before, the build failed with GS0479. GS0607 and GS0608 now say that a data class or data struct qualifies.

    • The implementing part is spelled with your declaring part's header, so System.Text.RegularExpressions.Regex or count int pair as well as Regex or count int32. A parameter-name mismatch between your declaring part and the generated implementation is reported as GS9208 instead of being paired.
    • The generator's helper types keep their own package, System.Text.RegularExpressions.Generated, so a type of yours named Utilities no 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").
  • cs2gs migrate translates [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's shared block (an instance method stays an instance member), and the migrated project's build runs the real Regex generator through gsgen, exactly as a native G# project does. The old translation constructed a cached Regex in a private __generatedRegex_Digits field instead.

    • The pattern, options, match timeout and culture name are re-spelled from their constant values, positionally, so a const pattern or a named matchTimeoutMilliseconds: argument migrates as a literal.
    • Culture-sensitive IgnoreCase (RegexOptions.IgnoreCase with a culture name, or an inline (?i) without CultureInvariant) 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 record or partial record struct keeps the declaring part in the translated G# partial data class or partial data struct.
    • A top-level-statements program's partial Program class that declares a [GeneratedRegex] method stays a class instead of being hoisted to top-level funcs, and its private members become internal so 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.
  • A fixed statement can pin a fixed-size buffer field directly (issue #4378, ADR-0125 amendment). fixed p *int8 = h.Bone.Name { … } — or a bare fixed p *int8 = Name { … } inside the declaring struct — binds p to the buffer's first element and keeps the buffer's containing storage pinned for the whole block, the G# spelling of C#'s fixed (sbyte* p = Name) { … } and the same int8& pinned IL csc emits. This unblocks the Raylib-cs BoneInfo/ModelAnimation interop pattern: cs2gs now translates such fixed statements 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, a ref parameter, 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 new GS0507 (C#'s CS0213) — use it directly as a *T instead.

  • A fixed-size buffer field can be used by its bare name inside the declaring struct (issue #4377). Name[i], Name[i] = v and return Name now decay the buffer to its *T first-element pointer exactly as this.Name does, instead of failing with GS0116 ("is not indexable").

  • G# source can declare a scope nullability-oblivious: --nullability=oblivious, @Oblivious and @NullabilityEnabled (ADR-0186 §9, step 5). They exist so cs2gs can 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 than T: the declared type of a field, property, event, parameter, return, local, for variable, inline out declaration 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. So var xs List[string] there has type List[string]!: the slot's top level is platform, and the element is still string. Likewise []string is []!string. A nil stored into an unadorned element is therefore an error, as outside the scope; write List[string?] for a container that carries nils. T? still means T?, and value types and open type parameters are unchanged. (The owner reversed the first cut, which also made nested positions T!, 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 still T!.)
    • What does not. Base-type / interface lists, generic constraints, explicit-interface qualifiers and @DllImport / @LibraryImport signatures are exempt; the top level of a catch, pattern or if let binding stays non-null.
    • Where it applies. --nullability=oblivious makes a whole compilation oblivious; @Oblivious on a type, function, property, field, event, parameter, local or block does it for one declaration, and @NullabilityEnabled opts 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 (so List[string] is {0, 1}). It re-imports as List[string]! from another assembly. A csc consumer reads the top level as it reads a #nullable disable member, and the element as non-null.
  • 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, through this., with a compound operator, or with ++ — inside an init(...) 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 public init setter the C# assembly never had (found comparing the migrated Gsharp.Runtime.Values ABI, where Slice<T>.Length/Capacity had 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 @UnscopedRef since ADR-0058, but what shipped only ever cleared IsScoped on a ref struct receiver — which governs the by-VALUE GS0219 check, not the ref-safe-context that return ref this.<field> actually depends on. HasFunctionLocalRefScope's parameter case read p.IsScoped || p.RefKind == RefKind.None, and a receiver's RefKind is always None, 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 with declaration: null and the old recogniser read raw declaration syntax. @UnscopedRef is now recognised by CLR TYPE IDENTITY (System.Diagnostics.CodeAnalysis.UnscopedRefAttribute, ADR-0084 §L5) off the bound attribute list, works on a func, a prop, and an indexer of any struct or ref struct, and is emitted as a real CustomAttribute row — on the MethodDef for the func spelling and the PropertyDef for the prop/indexer spelling — so a C# consumer of the assembly sees the same contract gsc enforced. The emitted body is ldarg.0; ldflda; ret — the same sequence csc produces for [UnscopedRef] public ref int Slot() => ref this.total;; ldarga.0 there 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 requires import System.Diagnostics.CodeAnalysis; without it the binder reports GS0198 and 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.cs is restored as a real .cs file 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 Roslyn CSharpFixture helper 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.
  • A struct member's this is writable storage, in every instance member (ADR-0184 D2). ParameterSymbol derives IsReadOnly from its RefKind, and a receiver's RefKind is None, so RefCapabilities.IsReadOnlyStorage classified every this as READ-ONLY — which is backwards, since the CLR passes a struct's receiver as ref S. return ref this.Total therefore failed the lvalue/readonly gate and reported GS0253 ("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.

  • await is legal in a plain func, and colours it (ADR-0174 D4's await g() row, issue #3954). Awaiting makes the awaiting function suspending, so colourless Go-style code can await an ordinary Task/ValueTask without becoming async func. Where inference may not change a signature — an entry point, an open/override/interface member, an accessor, a constructor, an operator, an iterator, a function literal, an unsafe or fixed body — GS0574 asks for async func (an observable Task[R]) or suspend func (implicitly awaited, R). GS0132 keeps 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, channel for..in — never because a library method returns a task. ReceiveBatch/SendBatch are ordinary ValueTask[int32] calls: write await 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-capable Channel<T> subclass with Go-exact close semantics, a two-value receive, batch transfer, and the transactional select waiter protocol; gsc copies 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 a chan[T] is accepted wherever a C# API asks for a Channel<T>, a ChannelWriter<T>, or a ChannelReader<T> (issue #3976). The lattice reads the types rather than the spelling, so a plain BCL Channel<T> now converts implicitly to a writer or reader target — the chan[T] -> out chan[T] row written in CLR names, and the same conversion C# has always had through Channel<TWrite, TRead>'s implicit operators. This holds when the element type is still open: inside func f[T](ch chan[T]), the channel reaches an imported constructor or method taking Channel<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 that List had 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 reports GS0267 (naming applicability) rather than GS0214 claiming 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 []string or (int32, object) passed to a Base[T]'s IEnumerable<T> / (int32, T) parameter.

  • A C#-authored generic extension on ChannelReader<T> or ChannelWriter<T> is now found on the chan[T] spelling callers actually write (issue #3877), so a C# library can publish an extension on ChannelReader<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 an out chan[T] still finds no reader surface, an in chan[T] no writer surface, and the element still has to match. The batch surface (D10) drops the four Channel[T] overloads that existed only to work around this.

  • A C#-authored generic method or CONSTRUCTOR over a ChannelReader<T> or ChannelWriter<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 open chan[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 carries Pair rather than object. The lattice is unchanged: an out chan[T] still finds no reader surface, an in chan[T] no writer surface, and the element still has to match. A genuine ChannelReader<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 report GS0155 where the closed chan[int32]? — and a string? at a non-nullable string parameter — 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's ChannelReader<T>/ChannelWriter<T> parameter. This is a CLR-boundary rule only: passing a chan[int32]? to a G#-declared chan[int32] parameter still reports GS0154, 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 a chan[int32]? behaved differently from a chan[int32] is gone: Plain.W(c) used to report GS0159 for the nullable form and bind for the non-nullable one. Applicability had already been ignoring the annotation wherever the CLR shapes were related (a chan[int32]? reaches a Channel[int32] parameter exactly as a string? reaches a string one); 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 reports GS0154.

  • A nullable channel reaches a non-nullable imported Channel<T> parameter whatever its element type (issue #3985). chan[T]? there used to report GS0155 where the closed chan[int32]? — and a string? at a non-nullable string parameter — 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's ChannelReader<T>/ChannelWriter<T> parameter. A nullable channel still does not reach a non-generic ChannelReader<T>/ChannelWriter<T> parameter (issue #3992). This is a CLR-boundary rule only: passing a chan[int32]? to a G#-declared chan[int32] parameter still reports GS0154, and direction and element are still enforced.

  • A map[K, V] reaches a Dictionary<K, V> parameter, and a tuple a System.ValueTuple<…> one, when an element is still open (issue #3987). map[K, V] IS Dictionary<K, V> (ADR-0104) and a G# tuple IS a ValueTuple<…> (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, so func makeMap[K, V](entries map[K, V]) MapHolder[K, V] { return MapHolder[K, V](entries) } reported GS0155 against 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 object slot (issue #3989). A same-compilation type argument erases to object while binding — that erasure is what lets []Pair{…}.Count() reach LINQ — but at the erased level it was indistinguishable from a real object, so List[Pair] bound to a parameter that genuinely is List<object> and emitted IL ILVerify rejects; the covariant sibling (IEnumerable<object>) went further and threw EntryPointNotFoundException at run time, because CLR variance does not reach a value-type element. Applicability now asks whether this slot's object COULD 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 is object, 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 an object overload 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 genuine List<ImportedBase>. Both spellings of the confusion emitted IL ILVerify rejects, because List<T> and Dictionary<K, V> are INVARIANT, and each reached the parameter by a different route: map[string, Derived] matched a genuine Dictionary<string, ImportedBase> by IDENTITY, which #3989's gate declined to question because it only looked for a nested object; 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" (two Lists share a parameterless constructor and a Capacity) 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 for object, and applicability asks it on the structural-projection arm too. Everything legitimate is untouched: covariance still binds (IEnumerable<ImportedBase>, and []Derived at an ImportedBase[] parameter, since CLR arrays really are covariant), a genuine List[ImportedBase] or Dictionary[string, ImportedBase] still binds, an inferred generic slot (M[T](List[T]), and an imported delegate's erased Invoke) 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 a List<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 call Probes.FirstOf[T](pair) compiled, ran, and printed the right answer while emitting IL ILVerify rejects: the argument was converted against the ERASED parameter shape ValueTuple<int32, object> — boxing the element and rebuilding the tuple — and handed to a call closed over the real T, which wants ValueTuple<int32, T>. The erased-slot recovery that already handles a bare T, a T?, 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 the map[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) reported GS0159 while the List spelling 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 : ImportedBase arrives as ImportedBase), but an explicit type argument closed the candidate over a flat System.Object placeholder — so a Dictionary<string, ImportedBase> argument was ranked against a Dictionary<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 already object), and the failure was never map-specific — []Derived and [N]Derived at a T[] 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> or Dictionary<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 = ch where a receive was meant).

  • go { … } (ADR-0174 D14): the block form of go spawns the block as a goroutine, capturing the enclosing locals (per iteration for a for … in variable) — go func() { … }() without the ceremony.

  • scope is completed (ADR-0174 D5/D6): every go inside a scope reports to the scope's frame (no Task per goroutine); the block binds an implicit ctx (Gsharp.Concurrency.Context) that the first goroutine failure cancels promptly; exit follows the documented precedence table (ScopeException with the cause first, a failing body rethrown unwrapped) and, inside a suspending function, is awaited rather than blocked. A nested scope's ctx is linked to the enclosing one (ctx.Parent), and return from inside a scope still counts as returning on every path. A free go outside any scope is fail-fast on an unhandled exception (GoroutineRuntime.UnhandledGoroutineException to observe).

  • A small concurrency library (ADR-0174 D9): after(d) and tick(d) are selectable timers — case <-after(d) is the timeout arm, and a select receive arm now accepts anything selectable, not only a channel — and merge(a, b, …) fans several channels into one receive-only channel that closes when the last input does. They live in the implicitly imported Gsharp.Concurrency namespace and are called by bare name; a program that declares its own after keeps its own.

  • Batch channel operations and chunks (ADR-0174 D10). A channel handle gains TryReceiveBatch/TrySendBatch (a Span[T], never waits) and ReceiveBatch/SendBatch (a Memory[T], can park, and returns a ValueTask[int32] you await like 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 where batch is a ReadOnlyMemory[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. GS0562 warns when the channel is a rendezvous, where batching is correct but pointless.

  • async let (ADR-0174 D15): async let name = expr starts expr as a child of the enclosing scope and binds name to its eventual result, read as await name. Both children of two async let bindings run concurrently; the binding is a value of type R, 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's ScopeException rather than being dropped; a failing child does not cancel its siblings. New diagnostics: GS0551 (no enclosing scope), GS0559 (never awaited), GS0569 (read without await).

  • Three more select arm shapes (ADR-0174 D8). Every arm may carry a when guard, 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 to nil to disable this case". case await task and case let v = await task let a Task or Task[T] race the channels on the same waiter. case cancelled replaces Go's case <-ctx.Done(): the ambient context's cancellation becomes an arm instead of an unwind. New diagnostics: GS0556 (a guard that is not a bool), GS0557 (a cancelled arm with no context to observe, so it would be unreachable), and GS0564 (one channel both sent to and received from by the same select). Cancellation also now reaches a select written in a callee with no scope of its own, through the caller's context.

  • select is 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 on Task.WhenAny; and winning is the transfer, so a value can no longer be stolen between choosing an arm and running it. A select inside a cancelled scope unwinds.

  • Cancellation unwinds parked channel operations (ADR-0174 D7): a receive, send, or channel loop inside a scope parks on that block's ctx, 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 through Gsharp.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. Declaring ctx Context in a signature uses that parameter instead, and a C# consumer calls the signature as written or passes a Context explicitly.

  • defer cleanup 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.DeferGraceExpired reports an abandoned one. Cleanup outside any scope costs nothing.

  • Suspension is inferred (ADR-0174 D4): a plain func that performs a channel operation, or calls a function that suspends, is compiled as a suspending function — a ValueTask[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 at async 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 with GS0558. Every emitted G# method that suspends now returns ValueTask/ValueTask[R] instead of R — a binary change for C# callers of such functions.

  • suspend func (ADR-0174 D4, the declared form): a suspending function compiles to a ValueTask[R]-returning state machine on the pooling builder, carries [Suspending], and is awaited implicitly by every G# call site — callers write let v = take(ch) and see R; an explicit await take(ch) is accepted as the same thing. From a function that is neither suspending nor async the call blocks the thread and GS0558 warns; the entry point is the silent root. Inference of suspension for plain func follows in a later step of Phase 3.

  • Debugging through suspension (ADR-0174 P3-8): every async or suspending kickoff carries [AsyncStateMachine] and [DebuggerStepThrough], so Environment.StackTrace and 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 — every async func body, 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 func suspend instead of blocking (ADR-0174 D4, first step): a receive, two-value receive, send, for v in ch, or while let v = <-ch in an async 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 a lock body keeps the blocking lowering (the monitor is thread-affine).

  • Observable completion (ADR-0174 D3): the two-value receive let (v, ok) = <-ch (or v, ok = <-ch) distinguishes a delivered zero value from a closed channel; while let v = <-ch { … } and for v in ch { … } loop until the channel is closed. A chan[T?] element that is nil is 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) spelling let first = func(x int32) int32 { return second(x) } / let second = func(x int32) int32 { return first(x) } still reported GS0130 ("undefined variable") on the forward reference, because StatementBinder'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-generic let 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 ordinary let/var closure 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 from Function.IsGeneric to Function.LocalDeclaration != null, so a capturing non-generic group member dispatches through its closure's Invoke method exactly like a generic one, and shared/mutated captured state (via the pre-existing CaptureBoxingRewriter) 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 reports GS0126 ("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.IsMethodGroupCandidateUsable now also accepts a non-generic, non-capturing direct-call local). A run mixing generic and non-generic members remains out of scope (unchanged GS0130 on 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 is ref, reusing the named-function ref-return machinery in full (FunctionSymbol.ReturnRefKind, the existing return ref statement binder, #4224's alias/storage machinery for a call result, and the existing escape/lifetime checker — var ref alias = at(xs, 0); alias = 42 mutates the original storage, let v = at(xs, 0) reads a value, and func() ref int32 { var x = 1; return ref x } still reports GS0254 for escaping function-local storage). A ref-returning literal always routes through the direct-call local-function path — standing alone or grouped with another ref-returning sibling — because a CLR delegate's Invoke cannot be made to return by ref without a dedicated synthesized delegate shape; only a non-generic let name = func (...) ref T { ... } declaration is accepted. Deliberately deferred, and reported as the new GS0588 rather than silently accepted or crashing: converting a ref-returning function (literal-backed local OR named) to a delegate/function-type VALUE — DelegateTypeSymbol/FunctionTypeSymbol carry 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 : TBase has a G# spelling — [TBase, TDerived TBase] — that the parser already accepted and the binder then rejected with GS0153: 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 a GenericParamConstraint metadata row naming VAR(n) or MVAR(n) — the encoding csc emits 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 reports GS0152 naming 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 make nil legal at TDerived, because the CLR constraint row carries no such implication and the looser reading is the #4027 unverifiable-ldnull defect one release later. A circular chain ([T T], [A B, B A]) reports the new GS0581, mirroring C#'s CS0454. This was the single compile error the self-migration gate reported for test/Core.Tests.

  • The G# analyzer API gains an operation-block registration and the operation surface the ADR-0193 funnel rules use, and cs2gs maps the Roslyn equivalents (issue #4436, ADR-0169). AnalysisContext.RegisterBoundBodyAction runs a rule once per function body with a BoundBodyAnalysisContext (the analogue of RegisterOperationBlockAction). Bound nodes gain ChildNodes, Descendants() and DescendantsAndSelf(). Two analyzer-facing bases, BoundMethodReferenceOperationExpression and BoundPropertyReferenceOperationExpression, span the user and CLR forms of a method group or property access. Symbols gain GetAttributes(), and FunctionSymbol gains AssociatedSymbol and MethodKind. cs2gs now translates the matching Roslyn members: local references, simple assignments, variable declarators, foreach locals, inline out var, is tests, type/declaration/recursive patterns and typeof. Previously an analyzer that used them stopped at CS2GS-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). RegisterBoundNodeAction and translated Roslyn RegisterOperationAction rules enter lambda and local-function bodies, visit a local assignment's target before its value, and still treat a plain x is T as 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, when OutputFile is a filesystem root and Path.GetDirectoryName returns null. The task now skips creating a directory, as the C# did, and fails at the write.
    • ClrTypeUtilities.SafeGetEventIncludingInterfaces, SafeGetPropertyIncludingInterfaces and SafeGetMethodsIncludingInterfaces, 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 the nameof in nameof(executor). That crashed the translate stage for Oahu's src/Oahu.Cli.App, every project depending on it then failed to compile, and the cs2gs-apps-nightly oahu leg went red. The third was in the HTML report, where a compile error that carries a line but no file crashed report.html generation 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 a shared field is now emitted (issue #4903). gsc bound the annotation but never wrote the CustomAttribute row for a field declared in a shared block or shared class, so a @ThreadStatic field silently became one process-wide slot. The migrated Gsharp.Runtime.Channels pooled its per-thread SelectWaiter in such a field, so concurrent selects raced and threw ArgumentOutOfRangeException from CollectGates. The runtime now keeps that per-thread state in ThreadLocal<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-tree Gsharp.Runtime.Channels / Gsharp.Runtime.Values instead of the SDK package's copy.

  • Per-thread compiler and runtime state is back in @ThreadStatic fields (issue #4909). With the bootstrap pin on a release that carries the #4911 emitter fix, DeclarationBinder's conformanceMatchingDepth and the Gsharp.Runtime.Channels SelectWaiter pool, SelectRandom generator and InlineBudget counters drop their ThreadLocal<T> workarounds and return to @ThreadStatic shared fields, which avoids a ThreadLocal lookup 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 of Gsharp.Runtime.Channels replaces 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 virtual PrintMembers, 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 seven Type helpers 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/Action delegates retain their names in type positions, including typeof, 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 taking Token and returning Box previously called the operator on the default token and could return a non-null box for nil. Explicit Box(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 actual Nullable<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 this for getter-only auto-property stores (issue #4734). Assigning this.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 remain initonly; 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 IsAbstract for classes with unimplemented declared or inherited abstract members, not only an explicit abstract modifier. 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) * z and string literals that can become spliced concatenations now use a typed, collision-safe local immediately before yield, 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/Func identity 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), with text initialized to null, now declares sequence[(Text string?, Code int32)] instead of promising a non-null Text. 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-only in arguments 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-open class may declare a protected override, and cs2gs keeps the inheritance shape of public C# classes (issue #4674). protected on a member of a non-open class is still GS0380, except on an override method, property or event, whose accessibility the base member dictates; a sealed class Lowerer : BoundTreeRewriter can 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 emits open for every C# class that is not sealed and is reachable from another assembly, and keeps a C# sealed class (including a sealed record, whose synthesized protected members never needed open) non-open. Migrated output therefore gains open on 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 always public, 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 now public only 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 a public delegate nested in an internal class to a top-level public delegate; it now emits internal unless the delegate and every type enclosing it are public. A protected or protected internal container also bounds the lifted delegate to internal, 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 capturing let At = func (i int32) ref int32 { return ref data[i] } as the Invoke method 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. Invoke now carries the literal's ref / ref readonly return, for captured locals, parameters, this fields, nested literals and literals inside struct members.

    • A ref-returning literal inside a struct member that returns a reference into this now reports GS0254. The closure holds a copy of this, so the reference would alias the copy, not the caller's struct. Before, an @UnscopedRef member let return ref this.n compile, along with forwarding forms such as return 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.
  • cs2gs no longer adds redundant empty-array allocations (issue #4684). An empty collection expression targeting a rank-one array (T[] a = []) translates to System.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 migrated ManagedLocationKey, restoring 40 bytes per key instead of 88 without changing constructor assignments or explicit array allocations.

  • Synthesized names and orderings in /deterministic emit 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's MethodDef rows 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 using or namespace rather 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 the package declaration. 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, #nullable and other preprocessor lines are still dropped.
  • 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 imported interface IS { static abstract int Z(); } failed to load with a TypeLoadException, a shared member overriding a static virtual's default was ignored, and class DZ : BaseS, IS2, where the imported BaseS already implements IS, silently ran BaseS.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 as func (IB) Z() no longer counts as an implementation of another interface's Z, for G# interfaces as well. A type that declares only func (IB) Z() while also listing IA with a static Z now reports GS0331; before, the explicit member was silently bound to IA.Z too.

  • 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 imported Base already implements IDerivedI's base IBaseI, a Value declared on Derived was silently ignored when called through IBaseI: the program ran Base.Value. The same happened for Collection[int32], IList[int32] with an ICollection[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 additional InterfaceImpl metadata rows.

  • The self-migrated cs2gs translator no longer throws while building an anonymous function's control-flow graph (issue #4611). CreateControlFlowGraph appended 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 16 Cs2Gs.Tests in the self-migration. The translator now yields the branch value only when it is present.

  • Computed property accessors are emitted as SpecialName methods (issue #4589). gsc set the CLR SpecialName flag only on auto-property, abstract, interface and indexer accessors, so the get_/set_ methods of computed properties, explicit-interface properties and shared properties 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 carries SpecialName, as C# emits it, so tools that recognize accessors through MethodInfo.IsSpecialName treat them as accessors. G# code that references a G#-compiled implementation assembly no longer sees those get_/set_ methods as callable methods, which matches C# libraries and gsc's reference assemblies, and @UnscopedRef on 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's Task implements ITask, which ICancelableTask extends), class Bag : Collection[int32], IList[int32] and class S : MemoryStream, IDisposable used to report GS0187 for every interface member the base supplies, such as ITask.BuildEngine or IDisposable.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 host ValueTuple over a type loaded from the reference assemblies, which produced a type nothing could reflect on. let e IEnumerable[(int32, Type)] = xs was an internal compiler error (GS9998), and Assert.Single(calls) over [](OpCode, Method MethodBase?) silently picked xunit's non-generic object overload, so the following call.Item1 reported GS0158. The tuple is now built in its elements' own load context, including the Nullable<T> of a nullable value-type element such as (int32, DateTime?). This was a compiler bug, not a cs2gs one; it blocked the self-migrated test/Compiler.Tests.

  • cs2gs preserves typed null casts through parentheses (issue #4721). (string)(null) now emits the same default(string) as (string)null, instead of an unbindable string((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 imported System.Reflection, so the file's bare compiler Binder became ambiguous with System.Reflection.Binder (GS0547). Analysis-only mappings now leave the import set alone; a printed reference still adds its import.

  • cs2gs asserts a foreach binding 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 renders sequence[T?], so G# infers for x in Iterator(...) as T?. cs2gs still treated the loop binding as non-null and emitted x.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 only IReadOnlyCollection[string] failed with "Cannot infer type argument 'T'" instead of inferring T = string through IEnumerable[string]. Inference now walks interface inheritance transitively, including through base classes. Conflicting interface projections are still rejected.

  • @UnscopedRef is 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 the System.Runtime reference assembly, ignored a genuine @UnscopedRef and reported GS0589 on 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 @ScopedRef and RefSafetyRules, 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 explicit scoped) 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 with GS0159, 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 readonly ref returns now remain native let 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-migration liftedLocalCeiling is ratcheted from 3 to 0.

  • cs2gs no longer emits __deconN identifiers 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 TryGet wrappers now preserve their non-null success contract (issue #4560). The migrated server now narrows document content after a successful lookup instead of retaining DocumentContent? and rejecting content.SyntaxTree with GS0158; 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 __anonN names. The target Invoke signature still determines arity, types, and ref kinds, while captured outer locals — including names such as obj, __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 declares int32, and hierarchy-projection inference keeps IBox[int32] and IBox[int32?] ambiguous instead of collapsing them. The distinction also holds inside arrays, tuples, constructed types, user structs, enums, and struct-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 emit T? when Roslyn marks the inferred local maybe-null, so passing that storage to ref T? no longer produces GS0612. Value-type substitutions, explicit local types, non-null initializers, discards, and default(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 infers TSource = string!, and a G# generic taking sequence[T] infers the same element instead of erasing it to string; [Nullable(2)] elements likewise remain string?. 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 nullable object? tag value, and an oblivious concrete string[] 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 as File.ReadAllLines and TaskFactory[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 []Type nested in an []object argument and containing typeof of a same-compilation type now reaches metadata and source generators as Type[], rather than being exposed as the compiler's internal object[] 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 func pair, 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 Program class (issue #4472). When Program must remain a class because it contains a nested aggregate, exports a member, is preserved for a referencing project, or declares a GeneratedRegex method, private members reachable from C# top-level statements are emitted internal, and nested type references are qualified through Program. 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 denied when 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 checks P'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 established GS0472/GS0379 diagnostics for private/protected accessors; declaring and derived contexts remain allowed, subject to the existing protected-receiver rule.

  • @LibraryImport preserves native-transition attributes (issue #4410). Method-level @SuppressGCTransition and @DefaultDllImportSearchPaths(...) now move from the managed outer stub to the hidden inner P/Invoke where the runtime observes them. cs2gs translates 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.N where w is Wrap[string], or through the same generic struct chain beneath a captured class field, failed with GS9998 because 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 emits ldflda rather 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 reports GS0185. 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] over List[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 as PropertySymbol in the migrated GSharp.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, or init accessor now emits the non-fatal CS2GS-ACCESSOR-ATTRIBUTE-DROPPED warning to stderr and the run's translate.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 suspend method 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 trailing Context parameter is absent from Invoke; the generated delegate now passes IL verification and carries the ambient context from its conversion site. A declared or inferred suspending method with a ref, out, or in parameter now reports GS0422 instead of emitting a state machine that copies an address into a value field.

  • A suspend method 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 trailing Context parameter and physical ValueTask return as part of the G# signature. The emitted override keeps that physical ABI and an explicit MethodImpl, so virtual dispatch and base.M() work across the assembly boundary.

  • String indexing, string length, and rectangular-array length now match csc's callvirt opcode choice (issue #4313). The three specialized emitter paths used non-virtual call for String.get_Chars, String.get_Length, and Array.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 __argN identifiers, 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 … when filter inside a finally block (issue #4489). The repo-pinned dotnet-ilverify 10.0.8 reports [StackUnderflow] at the first instruction of an exception filter whose start lies inside a finally or fault handler (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 on test/Core.Tests. The cs2gs ilverify stage and the Compiler.Tests IlVerifier now 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 is finally or fault. StackUnderflow is 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 managed locations 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 out or ref parameter (issue #4482). When cs2gs widens an oblivious out/ref parameter to T?, gsc now requires the storage passed to it to match (GS0612). An array element passed by out (new string[n] then out a[i]) now makes the array [n]string?, and a local passed by ref widens the same way out locals already did.

  • cs2gs emits managed-reference array types with nullable elements (issue #4525). C# arrays of ManagedRef<T> and ReadOnlyManagedRef<T> now use managed[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, foreach bindings 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's GS0604 rejection 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 to T? 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-null string was passed through with no check, because both sides are the same CLR type. Such an argument is now checked, and a nil throws NullReferenceException at 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 as Ob.WrapList[string](x), now binds T to the platform type string!: the call returns List[string!] and takes a nil unchecked, as the C# does (Ob.IsNil[string](nil) returns true). A nil element read through the list is checked where it is used. Before, T was string, so a nil entered a non-null List[string] and failed later with an unattributed NullReferenceException.
  • 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!] to List[string] was already rejected, but the same container passed as an IEnumerable[string] or an IReadOnlyList[string], or an oblivious string[] passed as one, was accepted. The receiver then read a nil element as a non-null string, with no check anywhere. These conversions now report GS0154 or GS0155 at 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 a List[string] element by element.

  • An object initializer on an imported type now finds a friend assembly's internal property or field (issue #4468). Options{Hidden: 1} reported GS0158 Cannot find member for an internal member of an assembly that named this compilation in an InternalsVisibleTo, although options.Hidden = 1 bound. Only assembly accessibility is admitted, as on the assignment path; private, protected and private protected members stay hidden, and a non-friend assembly still sees none of them. This kept the migrated tools/cs2gs/Cs2Gs.Tests from compiling in the nightly self-migration.

  • An async function's physical Task or ValueTask return 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 read Task[Rep] as Task[Rep]? or lose a nested Rep?. The first fix made every reference Task envelope non-null, which still laundered an @Oblivious async declaration's platform top level. Signature and metadata emission now share one physical return symbol: an enabled Task[string?] re-imports as Task[string?], an oblivious one as Task[string?]!, and suspending or explicitly ValueTask functions 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 @Oblivious G# class used to throw an unattributed NullReferenceException from inside the invocation when the delegate was nil. Invoking a local (f()) or a parenthesised callee ((f)()) already named the source.
    • An await for source. A nil oblivious IAsyncEnumerable now fails with the same message as a for … in source.
    • Named deconstruction. let { X = x } = obliviousCall() reported GS0164 where the same code on a plain T compiled. 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, a switch scrutinee, and as.

  • A by-reference argument may now differ from its parameter only in reference nullability, as in C#, with the new GS0612 warning (issue #4422). &base.runstack at a ref []int32 parameter, where the reference pack declares RegexRunner.runstack as int[]?, was rejected with GS0154. That blocked the [GeneratedRegex] runner for any regex that backtracks inside a loop. []?int32 and []int32, or List[string?] and List[string], are one runtime type, so the storage is now accepted. GS0612 warns in the direction a nil can flow through the shared storage: any difference for ref, nullable storage at a non-null in, a nullable out parameter over non-null storage, and any nested difference. A platform (T!) position never warns. The same-compilation shapes (&s with s string?) used to compile with no diagnostic at all; they now get the warning too. Breaking: a value-type int32? is Nullable<int32>, not an annotation, and passing its storage to ref int32 (or []int32? to ref []int32) is now GS0154. It used to compile, and the callee's store overwrote the Nullable's hasValue field. The same holds for imported callees: Interlocked.Increment(&x) or Int32.TryParse(s, &x) with x int32? compiled to unverifiable IL and is now GS0154. Overload resolution follows the rule too: with P(ref int32) and P(ref int32?), int32? storage selects ref int32?, and string? storage prefers an exact ref string? over ref string. @SuppressDiagnostic (ADR-0175) now also suppresses compiler warnings such as GS0612 (never errors) in gsc and the language server, and cs2gs emits @SuppressDiagnostic("GS0612"), scoped to the one statement, where it drops a C# ! from ref x!, so the [GeneratedRegex] runner builds with no warning. GS0612 reads 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 generated add_Got/remove_Got cast the combined delegate to EventHandler<object>. They failed IL verification, and the first subscription threw InvalidCastException. They now cast to EventHandler<T>, like C#'s field-like event accessors. Subscribing through a constructed receiver (g.Got += h on a GB[string]), or by bare name from a class deriving GB[string], now takes an EventHandler[string] handler rather than rejecting it as not an EventHandler[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 BCL Func with the matching structural signature, and a function literal passed directly to EventHandler[T] on a constructed G# receiver used the natural Action<object, object> shape. Both produced ILVerify StackUnexpected; delegate combination could also throw at run time. Conversions now retain the target assembly and substitute the constructed receiver before materializing the delegate.

  • base.M converted directly to a delegate now calls the base implementation (issue #4393). In a generic class, let f (T) -> string = base.Name in 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's ldftn does.

  • Subscribing to an event you cannot access is now an error (issue #4394). s.Changed += h on a private or protected event compiled from an unrelated class. It now reports GS0472 or GS0379, as base.E += h already did, on every subscription path: obj.E and bare E, += 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 its protected events 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 in Wrap(x) or Enumerable.Repeat(x, 2) where x is string!, inferred List[string!], which does not convert to a declared List[string] (GS0155).

    • The rule. A T! argument now contributes T, and a T! argument alongside a T one also gives T.
    • The check. The argument is checked at the call, so a nil throws NullReferenceException there, naming the expression.
    • What keeps T!. Only the argument's top level is affected: the element of an oblivious List<string> still infers string!. A ref argument, and an argument to a parameter an oblivious library method declares, keep their exact type.
    • Inference over such a list (First(xs) against First[T](xs List[T]) T) now works; it used to infer string and then reject the argument. An annotated list from enabled metadata reads the same way: First over a List<string?> now infers string?, as C# does, where it used to infer a non-null string with no diagnostic.
  • 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 @Oblivious scope a parameter typed with a G# class is Node!. List[Node].Add(n), a List[Node]{n} initializer, Enumerable.Repeat(n, 2), and a method group of such a function passed to Select all reported GS0159 "Cannot find function", although the same calls over string! bound. They now bind the same method as for Node, 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?) bool now satisfies Func[Dog, bool], matching the existing non-nullable conversion and the imported-method-group path. The reverse null-unsafe direction remains rejected: a method taking Animal does not satisfy a delegate that may pass Dog?.

  • 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 properties T? for reasons only the declaring project's source shows: [AllowNull] (#3694), or the project's own == null tests on the member (#1072). Another project in the same migration sees that member through the declaring project's reference assembly. It therefore read compilation.DebugInformation.Format or x.Definition.Name bare, and gsc rejected the chain with GS0158 once 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 turned src/Compiler, its five dependants and test/Core.Tests red in the nightly self-migration.

  • A nullable argument to an imported generic T? parameter now infers T as its underlying type (issue #4425). Calling a referenced assembly's static T Required<T>(T? value) where T : class with a Type? argument inferred T := Type?, so the call returned Type? and assigning it to a Type failed with GS0155. The same method declared in G# source already inferred T := Type, as C# does. Inference now reads the slot's [Nullable(2)] through ADR-0193's NullabilityImportRule, as the inverse of the rule that widens that slot. A value-type argument is unaffected: an unconstrained T? given an int32? still infers T := 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 a string? 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's Cs2Gs.Tests test-parity failure in the migrated CSharpToGSharpTranslator.LibraryImport.

  • cs2gs maps a Deconstruct out-parameter to its member case-insensitively (issue #4382). A positional subpattern whose Deconstruct out-parameter differed in case from the member it reads (out Inner? p for property P) was reported as unsupported and translated to a bare type test, so o is Outer({ X: 0 }) became o is Outer. cs2gs now follows the C# convention out T x to X: 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, in is patterns and switch arms. An ambiguous match, a member of another type, no matching member, a member that a derived type hides from an inherited Deconstruct, a member or getter that isn't accessible, or an extension Deconstruct is still reported loudly, and the diagnostic now says why. A name: 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 type T! 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 throws NullReferenceException at 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 as T! (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 as string[] or List<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). Setting CS2GS_NULL_FORGIVENESS_TELEMETRY to a file path now makes cs2gs write its per-rule !! counts there when it exits.

  • base.M() on a suspend base method now calls the base implementation (issue #4392). A suspend override that called base.M() on a suspend base 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 hidden Context argument, and it is forwarded non-virtually from a function literal, a go operand or an async let initializer through the same <>n__N forwarders async bodies use. go base.M() and async let x = base.M() are now accepted instead of GS0137. A non-virtual base method that inference makes suspending now makes its base.M() caller suspending too. A suspending method of a base class from another assembly, called through base.M() or unqualified, is now awaited instead of staying typed ValueTask[R]. A call to such an imported suspending instance method, whether through base or on an ordinary receiver, now also receives the caller's context, so cancelling the caller's scope reaches it. Previously it ran under Context.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].Item1 from a netstandard2.0 library used to read as string!. It now reads as string, 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?].Default is now EqualityComparer[string?], not EqualityComparer[string].
    • [Nullable(2)] T marks only the argument's own position as nullable. FirstOrDefault() over an IEnumerable[List[string]] now returns List[string]?. Some reading paths used to return List[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. Inside func F[T](xs List[T]), xs.Find(p) now has type T, not T?. This matches values.Min(). T? on an unconstrained T would mean Nullable<T> for a value-type substitution. This follows ADR-0193's ruling on the case, which #4385 revisits.
    • A ref T return through a generic receiver is now ref T rather than a platform-wrapped reference.
    • The platform type T! is no longer constructed over a type that has no reference nullability. This covers T?! and value types. As a result, an oblivious System.Tuple result now reads as a tuple rather than (T1, T2)!.
    • Three known reader disagreements remain, each tracked separately:
      • the return type of a generic call whose type arguments are inferred symbolically (#4363, closed by ADR-0193 Phase 4);
      • #4401;
      • #4403.
  • An argument written without in at an in parameter 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 an in parameter used to push the value where the callee expected an address, which failed IL verification (StackUnexpected) and threw InvalidProgramException at run time. The free-function, delegate and constructor forms rejected the call with GS0242, and the imported in indexer with GS0155. 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. GS0242 is retired, and cs2gs no longer turns an rvalue such as Scale(x + 1) into the non-lvalue &(x + 1). A plain value at a ref/out parameter of a method call now reports GS0235 instead 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 declared string? arrived as string and 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 Current of a duck-typed enumerator, in for x in … and await for;
    • the result of an awaiter's GetResult();
    • the Invoke signature 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 type T! 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.

  • 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 both csc and 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 as Action[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-this rule (ADR-0187 / issue #4350, resolving an item ADR-0184 recorded as future work). A member of a concrete struct that is not @UnscopedRef cannot 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 does func k(in a Acc, ref y int32) ref readonly int32 { return ref a.Pick(ref y) }, which csc also accepts. A writable ref handed back by such a member stays writable through a let/in receiver, so var ref alias = h.Value and h.Value = 55 are accepted and write the storage the reference names. @UnscopedRef members, 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]?) bool now opens Box's T exactly like func (left Box[T]) operator ==, instead of reporting GS0113 for T.

  • Construction syntax is no longer shadowed by a same-named extension function (issue #4350). Slice[T](owner, 0, n, n) constructs Slice[T] even when an extension func (s Slice[T]) Slice[T](lo int32, hi int32, max int32) exists, matching C#, which never considers extension methods for new. 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] inside struct ReadOnlySlice[T] used to be attached to the source type Slice, and when that type's own shared block 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] and Memory[T] -> ReadOnlyMemory[T] used to call the erased <object> operator and fail IL verification. An inline out var passed to an inferred generic CLR method (MemoryMarshal.TryGetArray(memory, out var segment)) now also keeps the symbolic ArraySegment[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 override members and every indexer argument (issue #4350). A struct's override func Equals(obj object?) used to lose override, which silently hid the override so boxed equality fell back to ValueType.Equals. slice[index, fromEnd] used to drop every argument after the first, and a long constant operand (2L * capacity) was narrowed to int32.

  • gsfmt no longer splits a C-style for init; cond; post { … } loop header across three lines, one per clause. SyntaxFacts.IsBreakRequiredBetween treats every ; as a statement terminator that forces a following line break — true for an ordinary statement, but not for the two separators inside ForClauseStatementSyntax, which are list separators like , rather than terminators. GSharpFormatter's layout builder now recognizes a ; whose direct parent is ForClauseStatementSyntax and treats it the way it already treats a comma: no forced break, a Doc.Line wrap point that collapses to one space when the header fits and indents by 4 when it does not, and (via IsContinuationBoundary) 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 the for keyword (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, and src/Sdk/Gsharp.Extensions/Sequences/Sequences.gs carried the three-line form and are reformatted in the same change so gsfmt --check . stays clean. A second, unrelated gap found investigating the same class of defect: ADR-0179's own phase-7b measurement table lists &&/||/??/or/and chains together as a family that goes fully to zero reducible long lines, but only &&, ||, and the pattern-disjunction or/and had 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] } reported GS0254 ("cannot return a managed pointer to function-local storage") regardless of where s'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.HasFunctionLocalRefScope had no case at all for BoundClrIndexExpression — the node a CLR/imported indexer read such as Span[T]'s s[0] binds to (wrapped in a BoundDereferenceExpression by ConversionClassifier.AutoDereferenceRefReturn) — so it fell straight through to the fully-conservative default and reported unconditionally, never even inspecting s. 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 reports GS0254, 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 the Span in the first place. The new BoundClrIndexExpression case delegates to a new HasFunctionLocalReferentScope helper — mirroring the file's existing pointer-parameter-dereference precedent exactly — only when the indexed target's type TypeSymbol.IsByRefLike; a reference-type or ordinary-struct target keeps the previous storage-scope walk unchanged. RefCapabilities.SelectByRefArguments is split into two separate argument lists (byRefArguments, checked as before, and a new byValueByRefLikeArguments, 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 taking s Span[int32]) is escape-checked with the same precision as #4224's existing ref/in/out forwarding, without a ref/in/out argument 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 as Fwd(ref a) from a by-value a Acc still correctly reports GS0254). A scoped by-value Span[T] parameter forwarded this way still reports GS0254; a non-scoped one compiles and correctly aliases the original storage. ReadOnlySpan[T] works the same way for a ref readonly return; a plain (mutable) ref return from a ReadOnlySpan[T] indexer is unaffected by this change and still reports GS0253 (its indexer only ever yields ref readonly). Soundness hardening (found in review before this fix merged): the new BoundClrIndexExpression case originally trusted ANY byref-like CLR indexer target as encapsulating a caller-supplied referent, the way Span[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 (no unsafe, no IL authoring) meaning the indexer returns a ref into the RECEIVER'S OWN storage rather than an encapsulated referent — exactly the Acc.Total shape 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 forwarding buf[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.IsUnscopedRefIndexerGetter now inspects the resolved indexer for [UnscopedRef] via CustomAttributeData (so it works correctly through gsc's MetadataLoadContext-based reference loading, not just live reflection), checking BOTH the property and its get accessor — 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 than get_Item — and the BoundClrIndexExpression case falls back to the strict HasFunctionLocalRefScope treatment whenever either carries it, matching C#'s behavior exactly (including still accepting the same member forwarded through a genuine ref parameter, where the referent legitimately is the caller's own storage).

  • cs2gs no 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 throw NullReferenceException at runtime on a legitimate nil (issue #4262, found migrating DavidObando/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 accepts nil via a transitive if (convertAction != null) guard several calls downstream — got convertAction!! at the call site instead of a bare convertAction, because ObliviousNullabilityAnalyzer.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, a T? local legitimately initialized to null in 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. IsTaintedCore now 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, on ObliviousNullabilityAnalyzer.IsParamsElementTainted (issue #3888's cross-project params T[] element taint) — reachable the same way, from TargetWillRemainNonNullableReference'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 inner IsTaintedCore-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 download call 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 — unlike ShouldPromoteToNullableReference'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 way IsTainted'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 target params method with a literal null in its own source could suppress the !! bridge for a genuinely non-nullable enabled target, or (via PromoteParamsElementIfUsedAsNullable's declaration-side promotion) repaint that target's OWN emitted params element nullable. A third, structurally identical gap existed in PromoteAwaitedReturnIfTainted (an async 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: IsParamsElementTainted requires the queried parameter's own element annotation to be None before consulting any candidate's cache, and PromoteAwaitedReturnIfTainted requires the awaited type's own annotation to be None before consulting IsTainted.

  • 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 sibling let Keep = func(value U) U { return value } (arrow syntax included) both reported GS0468 even with zero outer-value captures, because the emitted hoisted method carried only its own declared type-parameter list and the enclosing U had 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, with BuildMethodSpecForGenericCall supplying 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]] = ..., U owned by the enclosing method) — previously GS0113 "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 a shared member of the enclosing GENERIC type by its unqualified name (return Public() resolving to Holder[Outer].Public()) therefore had its Outer reference missed entirely, silently falling back to an invalid VAR slot on the promoted (non-generic-owner) host — System.BadImageFormatException at 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. TryPromoteNonCapturingGenericLambda now 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 new GetPromotedExtras lookup 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.RegisterStateMachineEnclosingGenerics built 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 way RegisterGeneratedGenericRemaps already does for the analogous ITERATOR state machine (issue #810 + #2118 combined) — so a body reference to the original enclosing parameter fell through to a dangling MVar slot inside MoveNext (a non-generic member of the now-generic state machine), System.BadImageFormatException at 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 report GS0468. 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 through RegisterStateMachineEnclosingGenerics'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, runs Outer("hi") successfully but throws NullReferenceException for Outer(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 on main predating 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.CheckAsyncNonGenericLocalFunctionEnclosingTypeParameterInParameter retains 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 CapturedVariables as known then — but LambdaBinder.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)) } — first is declared before second (a forward reference), so second's capture of outer isn't folded into first until after both members bind — compiled with no diagnostic and crashed the CLR with an invalid-program error the moment first executed. 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 with GS0468 if 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]) int32 followed by let d D = (a int32, xs ...List[int32]) -> a reported GS0155: 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 carrier List[int32] itself via VariadicCarriers.ResolveDeclaredParameterType, but LambdaBinder's two lambda-parameter binding sites (function-literal func(...) and arrow (...) ->) still had the pre-ADR-0173 unconditional ...T -> []T wrap left over from #812, so the lambda's own xs bound as []List[int32] — a slice of the carrier, not the carrier — and the two sides disagreed. Both sites now route through the same VariadicCarriers.ResolveDeclaredParameterType helper 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 five IEnumerable-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 — where class C (declaring M) is declared BEFORE class B (declaring shared { var Value int32 = 42 }) — reported GS0158: Cannot find member Value. even though B.Value plainly exists; reversing the declaration order instead reported the semantically-correct GS0265: Optional parameter 'x' is invalid: ... must be a compile-time constant. Root cause: ConversionClassifier.BindAndAttachParameterDefaultValue ran eagerly, inline, during Binder.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 new pendingParameterDefaultValueBindings queue drained by DeclarationBinder.BindPendingParameterDefaultValues() once every struct's static-member surface exists compilation-wide. The drain runs BEFORE BindPendingBaseInitializers, per the ordering constraint the investigation identified: a : base(...) initializer reads ParameterSymbol.HasExplicitDefaultValue while 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 identical GS0265 — 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 equivalent csc behavior 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 after DetectClassInheritanceCycles, 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, compiled let x string = rows[0].Value with no diagnostic — the same-compilation (symbolic) equivalent already correctly reported GS0155. Root cause, in NullabilityAnnotatedTypeSymbol.TransferTupleNames: a tuple nested one level inside another generic argument (IReadOnlyList[...] inside Task[...]) is represented LAZILY, and TransferTupleNames only matched a tuple sitting directly under one Nullable wrapper or a top-level ImportedTypeSymbol, 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 symbolic GetTypeArgumentSymbol fast path) recovered the result via a naive TypeSymbol.FromClrType instead of the nullability-aware NullabilityAnnotatedTypeSymbol accessor — 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 (the GS0159 symptom 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 be Nullable-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; and GetTypeArgumentSymbolForClrType (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?) string passed as items.Select(Name) over IEnumerable[Derived] — or directly, Func[Derived, string] f = Name — reported GS0155: Cannot convert type '(Base?) -> string' to 'System.Func[Derived, string]'; csc accepts this as an ordinary contravariant method-group conversion. Conversion.IsFunctionToDelegateConvertible classified each parameter/return slot by comparing raw CLR Types (ClrTypeUtilities.IsAssignableByName), which can only ever fire when the method group's own parameter type carries a ClrType — never true for a same-compilation class/interface mid-binding, whose TypeBuilder does not exist yet. (A CLR-imported base/derived pair, e.g. Roslyn's ISymbol/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 to Conversion.IsImplicitReferenceVariantSlot — the same reference-only, hierarchy-aware classification Conversion.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 to object in the CLR-reflected signature mid-binding, so the real argument is recovered from the delegate's own TypeSymbol.ConstructedTypeArguments (symbolic, not erased) by position before classification. Removing the parameter loop's unconditional rejection also surfaced a dormant sibling defect the pre-existing Issue1518NullableDelegateInferenceEmitTests suite caught as a regression during this fix's own validation: NullableTypeSymbol.ClrType erases 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 returns bool? apart from one returning bool once the parameter gate that used to reject every same-compilation-parameter candidate outright stopped masking it — Select's TResult output-type inference locked onto the non-nullable candidate first and the emitter built Func[S, bool] where callers still expected Func[S, bool?] (an ilverify StackUnexpected). The return-type check now uses NullableLifting.GetEffectiveClrType (identity for every other shape, including reference types) instead of the raw, erasing ClrType, matching what FunctionTypeSymbol.BuildClrType already 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 a T? parameter satisfying a delegate declared over the bare T, which csc rejects with CS0123) 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-nullable T, and vice versa (issue #4184, the parameter-side sibling of #4165's return-type fix above). func HasValue(x int32?) bool -> x != nil assigned to Func[int32, bool] compiled with no diagnostic and ran, silently dropping the nullable requirement HasValue declared; csc rejects the equivalent C# with CS0123. Conversion.IsFunctionToDelegateConvertible's parameter fast path compared the method's declared parameter type against the delegate's using the raw, erasing fnParamType?.ClrType — NullableTypeSymbol.ClrType is the bare underlying value type, so int32? was indistinguishable from int32. The fast path now routes through NullableLifting.GetEffectiveClrType (the same helper #4165's return-type fix uses), which re-wraps a value-type underlying in Nullable[T]. That alone was insufficient for the REVERSE direction (a method declaring bare T wrongly satisfying a delegate's T? slot — also CS0123 under csc, and measured as a live gap while building this fix's own T/T? matrix): Type.IsAssignableFrom has its own CLR-level special case where Nullable<T>.IsAssignableFrom(T) is true (supporting the implicit T -> T? lift used in ordinary expression contexts), which ClrTypeUtilities.IsAssignableByName would 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 narrowing IsAssignableByName itself, 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 matches csc exactly. The identical reflection quirk also affects the RETURN side's T -> 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's bool-returning body IS accepted by csc when target-typed against Func[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 crashes gsc (issue #4171, found verifying a Copilot review finding on #4156). NestedVariance.Pick(strings, NestedVariance.Handler) — where Pick[T](items IEnumerable[T], handler Action[Action[T]]) correctly infers T = string from the receiver, and Handler's own Action[object] parameter does NOT satisfy the resulting Action[Action[string]] slot (csc rejects the equivalent call with CS1503) — crashed with an internal GS9998: NotSupportedException: Cannot encode signature for type '?' yet. during PE emission, rather than reporting a clean diagnostic. An unresolved bare method group is deliberately typed TypeSymbol.Error as a "defer me" sentinel until overload resolution picks a candidate (ClrOverloadResolution.IsUnresolvedMethodGroupArgument), and ExpressionBinder.Calls.Invocation's "an argument that already failed to bind cannot participate in overload resolution" guard treated ANY TypeSymbol.Error-typed argument as already-diagnosed and silently returned an undiagnosed BoundErrorExpression when no candidate accepted the call — but a deferred method group is never a prior failure. The top-level declaration's inferred type became the bare TypeSymbol.Error sentinel 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 existing GS0159 ("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, matching csc.

  • An annotation naming a type caught in an unresolved base-class cycle no longer hangs gsc and grows memory without bound (issue #4162). StructSymbol.GetHierarchy() walked the symbol-level BaseClass chain with no cycle guard; the post-bind cycle detector (#973) normally rejects a genuine class B : C / class C : B cycle before anything else walks that chain, but GetHierarchy() is also reachable earlier, during declaration binding, via DerivesFromSystemAttribute() → IsAttributeType(), whenever an annotation names a type that is itself part of an unresolved cycle. Without a guard the walk never terminated, so @B on an unrelated class over a cyclic B/C pair grew RSS from 3.6 GB to 18 GB in ten seconds rather than reporting a diagnostic. GetHierarchy() now folds a visited check into its own walk — a repeat both detects the cycle and stops it — so the source now cleanly reports GS0381 for the cycle and GS0200 for 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 hangs gsc when its enclosing class is part of a base-class cycle (issue #4164). A different, independently reachable unguarded BaseClass walk than #4162's: BoundScope.TryLookupNestedTypeAliasIncludingInherited() looped for (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 live dotnet-stack sample 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 same visited-set guard #4162 used, so open class B : C { init(value Widget) { } } / open class C : B { let W int32 } now cleanly reports GS0381 instead 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's ConditionalAccessExpressionSyntax/WhenNotNull chain 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 (through AccessorExpressionSyntax.RightPart, exactly like Roslyn's own WhenNotNull nesting, confirmed independently by AssignmentTargetSyntaxFacts.TryLiftTrailingMemberAccess's existing recursion), while [/?[/( continuations still nest the ordinary iterative way (through IndexExpressionSyntax.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-exact HopSpan/ReceiverSpan/WhenNotNullSpan location math), and cs2gs now rewrites: expr is ConditionalAccessExpressionSyntax [x] to NullConditionalChain.AsNullConditionalHop(expr) is ExpressionSyntax [x] (covering a?.b AND a?[i] uniformly, closing a genuine a?[i] under-fire the first landing left in place); RegisterSyntaxNodeAction(handler, SyntaxKind.ConditionalAccessExpression) to a guarded wrapper registered for BOTH SyntaxKind.AccessorExpression and SyntaxKind.IndexExpression; <x>.WhenNotNull is <Type> [designator] and <x>.Expression is <Type> [designator] to the matching NullConditionalChain predicate, with the designator narrowed to the real G# type the predicate proves; MemberBindingExpressionSyntax.Name/.OperatorToken and ElementBindingExpressionSyntax.ArgumentList to RightPart/DotToken/Indices; a bare cast or variable-typed ConditionalAccessExpressionSyntax read to G#'s non-discriminating ExpressionSyntax supertype (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 explicit NullConditionalChain.IsNullConditionalHop filter. Every rewrite handles BOTH C# shapes a type test can take — a designated is Type x/pattern-combinator form (IsPatternExpressionSyntax) and a BARE expr is Type with no designator, which parses as the older BinaryExpressionSyntax/SyntaxKind.IsExpression node and never reaches pattern translation at all; missing the bare form would have made the new non-discriminating ExpressionSyntax map row silently vacuous for the commonest real-world spelling of a type probe. A sibling pre-existing bug is fixed alongside: MemberAccessExpressionSyntax → AccessorExpressionSyntax and SyntaxKind.SimpleMemberAccessExpression → SyntaxKind.AccessorExpression were BOTH unconditional, but Roslyn's ordinary MemberAccessExpressionSyntax EXCLUDES a ?. hop's tail (Roslyn gives that its own receiverless MemberBindingExpressionSyntax kind) — so node is MemberAccessExpressionSyntax and an unguarded SimpleMemberAccessExpression registration both silently over-fired on every ?. hop's tail too; both now carry the same IsNullConditional-discriminating fix, inverted. .WhenNotNull read as a bare value (not the scrutinee of an is test) still has no G# counterpart and stays a loud CS2GS-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/designated DeclarationPatternSyntax/TypePatternSyntax/ConstantPatternSyntax, via one dedicated entry point) — a not, 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 designated is not ConditionalAccessExpressionSyntax cae registered on SyntaxKind.IdentifierName and 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-level if let/guard-hoist family) now routes through two shared builders instead of calling the general type mapper directly. For MemberAccessExpressionSyntax/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/IndexExpressionSyntax plus an IsNullConditional: false suffix), which composes for free under not/and/or/parens/switch/nested subpatterns through the existing, already-tested pattern walkers — no new shape enumeration anywhere. For ConditionalAccessExpressionSyntax, whose faithful G# form is the NullConditionalChain.AsNullConditionalHop(x) != nil PREDICATE (not a pattern), a shape-agnostic DescendantNodesAndSelf scan — 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-mentioning is-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 and not/and/or compose for free; a CAE designator binds by substitution at the only three positions C#/G# both allow one (top level, under top-level not, or under and — 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 loud CS2GS-GAP instead of silently falling back to the non-discriminating ExpressionSyntax supertype. The statement-level if 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 becomes AsNullConditionalHop(receiver) directly (already nullable, so it drops into the existing let t T? = …; if t != nil shape unchanged), and for a plain-access type the existing as-cast is kept but the guard gains an extra IsNullConditional conjunct; 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 in src/Analyzers/InternalAnalyzers/RewriterClonePreservationAnalyzer.cs (a nested property-subpattern designator and a switch-case label, both testing MemberAccessExpressionSyntax) 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/ReceiverSpan also now throw ArgumentException on 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: an as ConditionalAccessExpressionSyntax/as MemberAccessExpressionSyntax/as ElementAccessExpressionSyntax cast still gets only the warn-and-pass CS2GS-ANALYZER-SHAPE treatment, 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) called BuildPatternNarrowingReplacement directly instead of routing through the round-3 leaf, which is unreachable from an if-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 of NullConditionalChain.AsNullConditionalHop(receiver). Fixed with the same IsConditionalAccessAnalyzerType/IsPlainAccessAnalyzerType branch TranslatePatternTest's DeclarationPatternSyntax case already used.

  • A contravariant delegate/interface argument now raises the inferred type argument, matching csc (issue #4133). Inside func f[T DisposableBase](value T), VarianceOverloads.DelegateType(value, ObjectAction()) — where ObjectAction() returns Action<object> — printed DisposableBase; csc prints Object, because the contravariant Action<T>/IComparer<T> argument's own type is an UPPER bound on T that must be able to raise it past the lower bound the plain value argument contributes. MemberLookup.FixSymbolicMethodTypeArguments collected 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. FindSymbolicInferenceCandidate now 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 *Winner overload 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 checking Conversion.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 checks Conversion.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 under Conversion.ClassifyNonStructural (identity modulo a reference-nullability annotation or a tuple element name — e.g. (Key string, Value string) against (Key string, Value string?)) but not TypeSignaturesEquivalent. 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 same MergeRecoveredTypeArgument the equivalent-bounds branch already used, instead of falling to the true-ambiguity branch and reporting a spurious GS0159.

  • 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. RefineSymbolicArgsForMethodGroups recovers a method group's natural Func/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] over IReadOnlyList[Item] with a bare predicate pred(x Item?) bool (the nullable annotation cs2gs's oblivious-nullability heuristic adds to nullable-oblivious C#) correctly lower-bounded TSource to Item from the receiver, but the predicate's own Item? parameter then raised it to Item? — a false ambiguity two calls downstream, reported as GS0159 (tools/cs2gs/Cs2Gs.Pipeline/DeclaredProjectItem.gs). Worse, a real subtype (not just a nullable annotation) could raise TSource past what the receiver actually enumerates: INamespaceSymbol.GetMembers() (a shadowing declaration returning IEnumerable<INamespaceOrTypeSymbol>) correctly lower-bounded TSource to the narrower INamespaceOrTypeSymbol, but a bare method group whose own parameter was the wider ISymbol? raised it back to ISymbol — gsc emitted a Select<ISymbol,…> MethodSpec at a call site whose delegate creation was still pinned to Func<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 new SymbolicInferenceBoundKind.MethodGroupUpper category keeps a method group's parameter-type contribution out of the ordinary bound lists — its legitimate return-type (output) contribution is unaffected — and SymbolicInferenceBounds.Add discards it outright, with NO fallback: csc reports CS0411 for Wrap[T](predicate Func[T, bool]) called as Wrap(Check) with no other argument to fix T from, 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 than csc, the opposite direction of #4133's own defect (caught in review). Reversing an already-demoted MethodGroupUpper bound 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. HasImplicitSymbolicConversion also 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 in TupleTypeSymbol.ElementNames the way TupleTypeSymbol.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 as TupleTypeSymbol.Get does.

  • params packing, interpolated-string-handler forwarding and named arguments now bind, in SOURCE evaluation order (issue #4134). Ten call shapes csc compiles and runs were previously a mix of rejection and incorrect ordering. Eight reported GS0159 Cannot find function — a params array of interpolated-string handlers, a named FormattableString in an expanded call, and every spelling of an InterpolatedStringHandlerArgument-forwarded handler except the extension-method one, which reported GS0246 and was never executed. The remaining two — a reordered named argument into a params slot, and its ref lvalue variant — already bound, from the erased-type-parameter fix's named-argument mapping, but evaluated in PARAMETER order rather than source order: second|first| and ref|first| where csc prints first|second| and first|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()) evaluates first|second|, a forwarded handler evaluates first|prefix|handler|, and a synthesized extension receiver — unreachable before, since that call never bound — is evaluated exactly ONCE (receiver|first|prefix|handler|). A ref/out lvalue 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 as GS0221 naming 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 object overload (issue #4086). Inside func callIt[T DisposableBase](value T), Overloads.Take(value) over Take<T>(T) where T : IDisposable and Take(object) erased the caller's T to System.Object. Both candidates therefore appeared to take the argument by identity, and the later parameter-specificity tie-break preferred the non-generic object slot; csc instead picks the inferred Take<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 genuine object bound also fixed that method parameter; otherwise the real reference/boxing conversion is ranked. The prerequisite that T : DisposableBase implies IDisposable is 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-inference object argument 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 contravariant Action[object]/IComparer[object] argument not raising the inferred type argument the way csc does — 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, mixed T plus object inference, constrained instance/static calls, and genuine-object controls.

  • A typeof attribute 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 wrote P.Outer`1+Inner with no instantiation, so reflection handed back the OPEN Outer<T>.Inner rather than the type the source names — a wrong answer that resolves, which is why nothing threw. A nested type records the encloser's vector on EnclosingTypeArguments and leaves TypeArguments holding 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 rows TypeDefEmitter copies down each nesting level (ADR-0087 §3 R1). A nested ENUM takes the same repair: EnumSymbol.ConstructNested records the identical vector, and typeof(Outer[Status].Kind) wrote the open P.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), so typeof(Outer[Status].GenericInner[string]) named P.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 expression TypeDefEmitter uses, so the two cannot drift again. An operand whose identity still depends on an IN-SCOPE type parameter — typeof(Outer[T].Inner) inside class C[T] — now reports GS0202 instead of silently serializing the erased close as a different closed type; an unbound generic DEFINITION such as typeof(Box[_]) or typeof(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 a MetadataLoadContext, and materialized at run time.

  • A same-compilation enum now satisfies a struct constraint (issue #4139). GsStruct[Color]() and class Source : ISource[Color], where enum Color and ISource[T struct] are declared in the same compilation, reported a false GS0152; so did the unmanaged spelling, whose check the struct one gates. An enum is a non-nullable value type (ECMA-335 II.14.3) and csc accepts the equivalent. IsNonNullableValueTypeForConstraint handled type parameters and structs explicitly and otherwise fell through to a reflective ClrType probe, 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 no ClrType while 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 spelling class 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 GS0582 before emit (issue #4065). Member chaining such as items.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-error GS9998. 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 object surrogate, 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 and for iteration 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). In func f[U SchemeOptions, T U](), constructing GsHandler[T] where GsHandler[TOptions SchemeOptions] requires SchemeOptions was rejected even though T : U : SchemeOptions. The shared class-constraint predicate followed only ClassConstraint, while dependent bounds deliberately live in the separate TypeParameterBound slot. 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 class constraint, so nil is legal at [TBase class, TDerived TBase] (issue #4062). return nil at a TDerived slot reported GS0155: Cannot convert type 'nil' to 'TDerived'. where csc accepts it: C# propagates a bounding parameter's special constraints to the bounded one, and TDerived : TBase : class proves TDerived is 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 a TypeParameterBound chain. #4043 kept that slot separate from ClassConstraint on purpose — roughly forty consumers read ClassConstraint != null both 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: nil at any bare type-parameter target has been lowered to default(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 — a return, 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 reports GS0155, because those admit a value type and nil at 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]() inside func callIt[T DisposableBase]() — over NeedsDisposable<T> where T : System.IDisposable and DisposableBase : System.IDisposable — reported GS0159: Cannot find function NeedsDisposable. while csc accepts 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), so class 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 bound Probes.NeedsEquatable[T]() under where T : IEquatable<T> from a same-compilation class GsEq : IEquatable[GsEq], and that program compiled and then threw System.Security.VerificationException — both IEquatable<T> and the erased IEquatable<GsEq> project to IEquatable<object>, so the walk proved an implication that does not hold, and csc reports 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 reported GS0159 before (an explicit type argument removes the non-generic sibling from the candidate set entirely) and now picks the constrained one, as csc does, 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 from csc (#4086), which this change leaves exactly where it found it.

  • A generic attribute's System.Type argument 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 with System.Int32 in the argument slot, and reflection materialisation threw GenericArguments[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 wrote System.Object, a nested generic wrote the surrogate at depth, a two-parameter generic wrote one of each, a slice argument wrote System.Int32[] and a nilable enum wrote System.Object. Sweeping the sibling shapes found two more kinds the issue did not name. A same-compilation GENERIC — typeof(MyBox[Status]) — serialised as the bare P.MyBox with neither arity suffix nor argument list, because a constructed user generic carries its arguments on TypeArguments and leaves TypeParameters empty; 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 whose Type[] argument contained a same-compilation typeof was dropped from the assembly ENTIRELY with no diagnostic, because #3684 widened the constant container to object[] to hold the placeholder and the constructor matcher then refused object[] for a Type[] parameter. All three are fixed together, and every position is covered: positional, named, and array element alike. A fully imported argument such as typeof(Box[int32]) has a faithful CLR type and keeps the byte-identical path it always took, as does a scalar typeof of 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 the Type[] 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 threw Could not find type 'map' in assembly 'P.dll'; the tuple form failed as The given assembly name was invalid, and the function and channel forms named (Status) -> bool and chan. 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 bare type.Name. These operands are only reachable NESTED inside a generic argument: at top level typeof(map[string, Status]) is rejected by GS0202, 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 a Dictionary<K, V>, a sequence an IEnumerable<T> or IAsyncEnumerable<T> through the same shape probe the rest of the compiler uses, a channel the Channel<T>/ChannelReader<T>/ChannelWriter<T> its direction already selects, a tuple the ValueTuple family with the tail nested into TRest above arity 7, and a function type an Action or a Func by 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 through MakeGenericType, 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 — a List<object> closed over the erasure surrogate — so the map's own CLR type resolved, and the old fallback wrote a name that RESOLVED, to Dictionary<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 shipped Func/Action arities 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 GS0583 instead of vanishing from the assembly (issue #4097). @Many([]object{1}) — where ManyAttribute declares only ManyAttribute(Type[] values) — compiled with exit 0 and no diagnostic at all, IL-verified, ran, and the emitted assembly simply carried no ManyAttribute row 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 the Type[] half of #4073 invisible for as long as it was: a widened object[] container failed a Type[] 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 channel GS0546 already 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 limitation GS0546 has, 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) at NoteAttribute(Text string) reached the blob writer with an int for a string slot and came out as the internal-compiler-error GS9998; it now reports GS0583 like 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 always int32, 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 comparing gsc against csc on 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, so GS0584 says 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 as GS0585 and retires GS0584'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)), against ToStringAttribute(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 wrote nil into 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 []string at a string[] parameter, a scalar typeof, 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 GS0584 down to its final population: every surviving GS0584 case is one csc rejects as CS0181 ("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, where Holder is an ordinary class, compiled clean and only reported GS0584 at each USE, or not at all if the attribute was never applied. GS0585 now reports it once, on the attribute class's own declaration — primary constructor and every explicit init(...) overload alike — the way csc reports CS0181, and it shares its valid-parameter-type predicate with GS0202'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}) against EAttribute(Kinds []Status) reported GS0202 — "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 no ClrType until 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 on ClrType), while an IMPORTED enum element's array (@Days([]DayOfWeek{...})) reached a real, non-null ClrType and 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, and Array.SetValue refuses to store an int into a DayOfWeek[] slot ("Object cannot be stored in an array of this type") — a plain coercion bug, unrelated to erasure, now fixed with Enum.ToObject. Both shapes are read back through a MetadataLoadContext as 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 (an object[] container assignable to a Type[]/string[] parameter) to GS0202, 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 imported Handler<TOptions> declares where TOptions : SchemeOptions and MyOptions is 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 threw TypeLoadException: GenericArguments[0], 'System.Object', on the open Handler definition violates the constraint of type parameter 'TOptions' — a constraint MyOptions satisfies. A same-compilation class has no CLR type while binding, so the receiver closes over the System.Object surrogate 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's ldsfld still parented at Handler<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"} reported GS0157: 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 spelling Handler[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 the object surrogate and carries the symbolic vector beside it, exactly as the constructor path has since #671, so the emitter reifies the real Handler<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 reports GS0152 where it used to report the wrong GS0157. 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 / .Values now binds (issue #4033). for v in m.Values followed by a property read on the loop element — v.Length, lists.Count, b.Label — reported GS0158, or the internal-compiler-error GS9998 when the element was a constructed generic, for both the map and the Dictionary spelling, while the same member on the same value reached through m[k], through for k, v in m, or through TryGetValue bound fine. Dictionary<,>.Values is declared [Nullable({1, 0, 0})], and the two zeroes sit at the TKey / TValue positions: those are type parameters, so the byte is a placeholder for whatever is substituted, not an annotation. Read as concrete positions they made the element V?, and because G# requires narrowing or ?. before a property read on an explicit T? (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 open map[K, V].Keys spelling already did. The issue names a constructed generic as the failing ingredient; measured, a plain string value fails identically, .Keys is affected as much as .Values, a generic key is affected, and so is SortedDictionary[K, V] — while non-nested generic collections such as List, HashSet, Queue, Stack and LinkedList were never affected.

  • A map[K, V?] over a nullable value type emits verifiable IL (issue #4035). map[string, int32?] compiled clean and then threw InvalidProgramException, because its backing Dictionary was closed over the bare int32 while the literal, the indexer's out slot and the loop variable all carried a Nullable<int32> — two ILVerify StackUnexpected errors 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 the GetEffectiveClrType correction its sequence[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 m and for v in m.Values — and so was the key position, map[int32?, V] and map[DateTime?, V] alike. Over an imported underlying such as DateTime the wrapper is now built inside that type's own reflection context, alongside the open Dictionary, for the reason issue #4023 documents. The Dictionary[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 as map[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 program var c Coupled[A, List[Bee]] — with A and Bee unrelated 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.Object placeholder; both A and Bee therefore arrived as object and the vector handed to the constraint check was Coupled<object, List<object>>, where List<object> genuinely does satisfy IList<object>. The check answered honestly on what it was given — the information that distinguishes A from Bee was gone before it was asked — and emission then reified the real types. #4016/#4031's NormaliseErasedTypeArgsForConstraintCheck is not the culprit and this was verified rather than assumed: it operates on the vector after the collapse, which happens earlier in ProjectGenericArgument. 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 expected IList[A] is built from the symbolic vector and compared against List[Bee]'s own IList[Bee], recursing so a nested bound such as IList<List<T>> is answered at its leaf. The check is three-state on purpose: a definitive no reports GS0152, a definitive yes accepts, and anything the symbols cannot settle — a concrete position, a missing symbol, a VARIANT parameter (where IEnumerable[Derived] legitimately satisfies IEnumerable[Base]), or a BARE dependent bound at the TOP of a constraint (where the relation is assignability rather than identity, so Chain[ChBase, ChDerived] must keep binding) — falls back to the pre-existing CLR comparison rather than to a rejection, because a wrong “no” would be a GS0152 on a legal program. That last exit is load-bearing and measured: with it disabled and nothing else changed, the legal Chain[ChBase, ChDerived] fails with a false GS0152. 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 spellings Coupled[A, List[A]] and Coupled[string, List[string]], the covariant Covariant[CovBase, List[CovDerived]], an open forwarded instantiation, and #4031's AddScheme[TOptions, THandler] shape all stay green. The asserting row #4032 left pinned for this gap has been moved into ConstraintViolations, not deleted.

  • A map[K, V] whose key or value is an imported type now has its whole Dictionary member 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.Values and for k, v in m all reported GS0158 / GS0159 / the internal-compiler-error GS9998 on such a map, while the Dictionary[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 host typeof(Dictionary<,>) over a key or value resolved through the compilation's reference context, which yields a TypeBuilderInstantiation that 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 as List[int32], a same-compilation class extending an imported base, and the key position as well as the value position. m[k] and m[k] = v were already fixed by #4015 and are unchanged, and len(m) still reports GS0566 because ADR-0174 (D13) retired len as a built-in.

  • await for v in s now iterates an async sequence[T] whose element has no CLR backing (issue #4020). An async sequence[T] parameter with an open element reported GS0134 ("cannot be iterated with await for") while the closed async sequence[int32] spelling iterated and the synchronous for v in s over an equally open sequence[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 reads async sequence[T] rather than sequence[T], so it can no longer be mistaken for the synchronous sequence — which await for still correctly refuses.

  • A nil at a reference-constrained [T class] slot emits verifiable IL (issue #4027). A nil literal 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 bare ldnull against 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 to default(T), which the emitter has materialised as ldloca; initobj; ldloc since #774 — the same three opcodes csc emits for Takes<T>(null) under where T : class, measured. Six sinks were affected, not the one the issue names: a call argument, a local declaration, an assignment, a return, and — on a generic CLASS, where the slot is !T — a field write and a member return. Nothing about the VALUE changes: default(T) at a [T class] slot is the null reference in every instantiation, so the nil still arrives and the callee's x == nil still observes it. An UNCONSTRAINED T still refuses nil (#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-error GS9998 (NotSupportedException: TypeBuilder generic instantiation does not support resolving members). The backing type is built as typeof(Dictionary<,>).MakeGenericType(key, value), and .NET returns a TypeBuilderInstantiation — 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 and set_Item, m[k]'s TryGetValue, and m[k] = v's set_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, a Dictionary[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) and for k, v in m over a map whose value is a class — is tracked as #4023.

  • An inferred generic call over a map emits a MethodSpec over the real key and value (issue #4026, second face). Probes.CountAnyMap(entries) on a map[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 a map, so K and V were never recovered and the spec fell back to whatever the erasure presented (ImportedBase for a same-compilation class with an imported base, object for one without) while the value pushed was the real Dictionary<string, Derived>. A map is now restated as the constructed Dictionary[K, V] it is (ADR-0104) before unification, so it takes the same patterns the List and []T spellings always took — which is why those never had the bug — and IDictionary[K, V], IReadOnlyDictionary[K, V] and IEnumerable[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, both take[D]() over func take[T IDisposable]() and Box[D]() over open class Box[TB IDisposable] reported GS0152: 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. csc accepts the C# equivalent and so does the CLR. Binder.SatisfiesConstraint's CLR-interface arm bottoms out in a reflective walk over typeArgument.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 (where SourceSymbolImplementsImportedInterface was 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]] over class Cmp : IComparable[Cmp]) and NESTED ([T IComparable[List[T]]] over class 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.TryUnifyAndCheckConstraints gates extension-method candidates through the same predicate, so a constrained generic-receiver extension over a source class went from GS0159 to binding. Exactly one overload winner moves, and it moves TOWARD csc: with a same-named Describe[T IDisposable](T) and Describe(object), D().Describe() picked the boxing object before (the constrained candidate was not applicable) and picks the constrained one now, which is what csc picks — 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 still object, 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]() inside func callIt[T SchemeOptions]() — over NeedsScheme<T> where T : SchemeOptions — reported GS0159: Cannot find function NeedsScheme., and so did the same-compilation spelling class MyScheme : SchemeOptions and the dependent spelling func callIt[U SchemeOptions, T U](). csc accepts 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: a TypeParameterSymbol argument matched none of the symbolic arms, so the erased System.Object placeholder was asked whether it derives from SchemeOptions, 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, whose object/ValueType early-out would be a false ACCEPT here, since on this path a System.ValueType bound is the metadata shadow of where T : struct and 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]() inside func callIt[TBase DisposableBase, TDerived TBase]() reported GS0159: Cannot find function NeedsDisposable., while the generic-type spelling class Forwarded[TBase DisposableBase, TDerived TBase] : DisposableConstrained[TDerived] reported GS0580 — two different wrong answers to one question, on a program csc accepts, since TDerived : TBase : DisposableBase : IDisposable holds in every instantiation. Both askers read only ClassConstraint and never #4043's separate TypeParameterBound slot. 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 (GS0159 at a method, GS0580 at 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 taught SatisfiesClassConstraint to follow the dependent bound, which covers a G#-DECLARED generic, but class Fwd[TBase SchemeOptions, TDerived TBase] : SchemeConstrained[TDerived] over an imported SchemeConstrained<T> goes through the reflective path instead and still reported GS0580. 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 equivalent while/for-in/C-style-for-post shapes, reported Unexpected token <BreakKeyword>, expected <IdentifierToken>.: the label's spelling (Identifier :) is byte-for-byte identical to a struct literal's first field, and StructLiteralAllowedInSuppressedHeader treated any NON-empty brace content as unambiguously a struct literal — the same lookahead that #1575 already had to add for an EMPTY Ident{} colliding with an empty body, generalized here to non-empty content that happens to start Identifier : 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 , between if let bindings, 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 ref alias local is now legal in an async function, an iterator, or an async iterator as long as it is never live across an await/yield suspension point (issue #4222). { var ref alias = values[0]; alias = 42 } before an await (or a yield) used to report GS0258 unconditionally — 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. RefStructAsyncLivenessAnalyzer now also tracks native alias locals (identified by LocalVariableSymbol.RefKind, since an alias keeps its pointee type on Type) and treats yield as a suspension point alongside await, 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 reports GS0258, now with a precise reason instead of a blanket one. AsyncCaptureWalker and 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-existing RefKind-driven ldind/stind emission 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 MethodDef should be filed under, instead of always the package's synthesized <Program> (issue #4234). A receiver-clause func binds only at top level (its receiver is not in scope inside a shared { } 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's ChannelBatchExtensions check) 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 the MethodDef on instead, reusing the same static-owner routing an ordinary shared-block static method already gets; the owning class is emitted abstract sealed and [ExtensionAttribute]-stamped, exactly like <Program> already is, so C#/F# extension-method discovery is unaffected. A malformed use reports the new GS9306. 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 blanket GS0463 rejection 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) } — where First never reads outer itself, only calls Second — crashed emission with GS9998: InvalidOperationException: Variable 'outer' has no local slot or parameter index in the current method., regardless of whether Second was declared before First (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 inside First's own body that called Second on First'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 its FunctionSymbol, not through a delegate value, so LambdaBinder's existing transitive-capture folding — which already walks a PHYSICALLY NESTED function literal's CapturedVariables into its enclosing literal's own set — had no equivalent for a BoundCallExpression referencing a SIBLING local function's FunctionSymbol; nothing propagated Second's capture of outer into First's own environment, so MethodBodyEmitter.EmitGenericLocalClosureInstance had no field to read outer from when rebuilding Second's closure instance at the Second(0) call site inside First's body. CapturedVariableCollector now consults a per-binder map of every local function's already-known captures (keyed by FunctionSymbol) and folds a callee's captures into its caller exactly like the nested-literal case; LambdaBinder.ReconcileGenericLocalFunctionGroupCaptures re-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 ref operand, 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) and var ref alias = holder.Value (prop Value ref int32 { get { return ref slot } }) used to report GS0256 unconditionally — ExpressionBinder.IsLvalue and StatementBinder.IsLvalueForRefReturn accepted only a variable, field, array element, or *p dereference, never a call/property result, even a ref-returning one. Both classifiers (and return ref At(...)'s equivalent, previously GS0253) now also accept a call to a same-compilation FunctionSymbol/instance method or a read of a same-compilation property/indexer whose ReturnRefKind != None, via a shared RefCapabilities.IsNativeRefReturningCall classifier so the two do not drift. An imported/CLR ref-returning member (e.g. Span[T]'s indexer) needed no change — ConversionClassifier.AutoDereferenceRefReturn already wraps those in a BoundDereferenceExpression, which was already an lvalue. Forwarding is escape-checked, not merely permitted: RefCapabilities.TryGetRefReturnEscapeSources decomposes a ref-returning call into its instance receiver and its non-scoped ref/in/out argument storage, and HasFunctionLocalRefScope propagates through them — return ref Fwd(ref x) from inside the SAME function that declared x still reports GS0254, while return ref Fwd(ref value) over a caller-supplied ref parameter, and return 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, not ref readonly) getter with no setter now supports assignment without inventing one — holder.Value = 42 and GetHolder().Value = Next() (receiver/RHS each evaluated exactly once) store through the getter's managed pointer, reusing the existing imported-indexer write-through shape (BoundIndirectAssignmentExpression over BoundAddressOfExpression); a native indexer's writable getter (prop this[i int32] ref int32 { get {...} }) gets the same treatment for both box[i] = v and an alias. A struct receiver that would need a defensive copy to call the getter (a let-bound or otherwise non-addressable struct value) is rejected for both the alias and the assignment form — a let ref readonly view remains allowed, since a read-only view of a copy is still sound. cs2gs's ref expr translator, 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/ref of a T? pointee, where T is an OPEN, unconstrained type parameter, no longer emits an invalid reference-slot store/load for a real value-type instantiation (issue #4236, found self-hosting Compiler.Tests's channel matrix). Gsharp.Concurrency.Chan[T].ReceiveOrPark's out value T? parameter — T carrying no struct/class constraint — compiled and ILVerified cleanly, then threw InvalidProgramException the moment T was 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/EmitLoadIndirect pick stobj/ldobj only for a pointee that is PROVABLY a value type (NullableLifting.IsAnyValueTypeNullable) or a bare bound TypeParameterSymbol — a NullableTypeSymbol wrapping an OPEN, unconstrained type parameter satisfies neither check, so it fell through to the reference-slot stind.ref/ldind.ref arm, which is invalid IL once the CLR resolves T to a real struct (a gap ILVerify's abstract-over-open-T checking does not catch, but the runtime type loader does). ImportedMemberRefFactory.GetElementTypeToken already resolved this exact shape correctly (issue #814 / ADR-0084 §L5 — Nullable[T] when T is struct-constrained, bare T otherwise); EmitStoreIndirect/EmitLoadIndirect simply never routed into it for this case. Both now recognize NullableTypeSymbol over an open TypeParameterSymbol and route to stobj/ldobj with that already-correct token. A minimal, non-self-hosted regression (Issue4236OpenNullableTypeParameterIndirectTests) reproduces the crash directly against native gsc — a generic class method with an out value T? and a ref value T? parameter, instantiated over a user struct — confirmed red before the fix and green after; ilverify reports no new finding on the fixed self-hosted Gsharp.Runtime.Channels.dll (the pre-existing, tracked ReturnPtrToStack false positive on SelectRandom.Shuffle is 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/#elif as an error instead of silently choosing a branch, and the compiler source was free of them before translation (three test-only #if DEBUG sites 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 is accepts the full pattern grammar, including constant, type, property, relational, list, and composed patterns. while let introduces 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]T allocates storage, flat initializers use row-major order, and a[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. cs2gs now emits native block expressions, rectangular arrays, multi-target storage assignment, if let / while let bindings, native is pattern variables, explicit extensions, and value-position assignments instead of synthetic spill-heavy rewrites.

Added​

  • Full patterns in value is pattern and value !is pattern, with type-plus-property composition and narrowed right-hand and patterns.
  • 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 idiom if !(value is string text) { return } followed by uses of text. Variables are scoped to the regions where the match is known to have happened; Type name designations are also accepted in switch arms. cs2gs preserves C# is pattern syntax and names for these shapes instead of hoisting __spillN temporaries.
  • while let name = nullableExpression { ... }, including multiple bindings and normal labeled break / continue behavior.
  • 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 map or for entry in map.
  • Gsharp.Extensions.Sync.SyncMap[K,V] for goroutine-safe shared maps with atomic Update.
  • Structural display of plain user values in the REPL while preserving their emitted CLR ToString behavior.
  • dotnet watch hot reload for SDK projects.

Changed​

  • The as operator now has nullable result type T?; use if let, keep the nullable value, or apply !! when a prior check guarantees success.
  • The deprecated name = value named-argument spelling is removed. Use name: 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, and rnd built-ins and the string(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.4 snapshot from the live docs. The version dropdown lists 0.4, 0.3, and Next.

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.
  • cs2gs now 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 is pattern (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 a while whose exit depends on the pattern) report GS0532.

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 unsafe context now supports unmanaged raw pointers *T, stackalloc [n]T producing either safe Span[T] or unsafe *T storage, the fixed pinning statement, the unmanaged type-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, standalone System.Range values such as 1..3, expression-bodied members via ->, general goto / labels, collection initializers List[T]{…} / HashSet[T]{…} / Dictionary[K,V]{…}, and inferred-type arrow lambdas with statement-block bodies.
  • New declaration forms. partial classes, structs, and interfaces; anonymous-object literals object { … }; nested type declarations; user-defined conversion operators operator implicit / operator explicit; user indexer members prop this[i int32] T { get; set }; the shared { init { … } } static-initializer block; static imports through import Ns.Type; and top-level private mapping to IL assembly / internal.
  • cs2gs C#→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 gsgen host runs Roslyn analyzers and generators against native G# projects. gsc /analyzer:<asm> spawns gsgen as a sibling before compiling. A shared resx codebehind generator emits Resources.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 GS04xx range. 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 is a ?? b. The earlier ?: spelling is retired in that role; cond ? a : b remains the ternary expression.
  • Runtime array allocation [n]T. A length-bearing [n]T allocates a zero-initialised array at runtime, complementing array literals.
  • Nullable array-element spelling. []T? is an array of nullable elements; []?T is 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.
  • char bitwise and shift operators promote to int32. Enum == / != comparisons against the integer literal 0 are 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 cs2gs tooling page was added.
  • The Docusaurus site cuts a new 0.3 snapshot from the live docs and retires the 0.2 snapshot. The version dropdown lists 0.3 and Next.

Fixed​

  • Extensive cs2gs translator hardening across nullability promotion, extension-call lowering, deconstruction and indexer targets, pattern binding, named-argument lowering, and source-generator-shaped constructs.
  • Numerous gsc binder, emitter, and interpreter correctness fixes across imported generic interface methods, nullable value-tuple boxing, data class equality and with, overload resolution, async lambda inference, and smart-cast narrowing.

Known limitations​

  • gsc --help advertises /implicitimports[+|-]; the + / - suffix form is not currently accepted by the Release parser. Use /noimplicitimports.
  • Migration coverage in cs2gs is 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…while loops with labeled break / continue; if let / guard let / while let smart-cast bindings; null-coalescing compound assignment ??=; null-conditional indexing a?[i]; arrow lambdas x => body; canonical (T1, T2) -> R function-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 bare default; variadic ...T parameters in function and anonymous function-type clauses; class / struct / init() constraint flag spellings; default-interface methods; reified generics; Gsharp.Extensions.Optional and Gsharp.Extensions.Sequences; friendly numeric type aliases; and the canonical map type clause map[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.
    • type keyword for type declarations (type Foo struct { … } → struct Foo { … }).
    • record keyword, replaced by data class or data struct.
    • := short variable declaration, replaced by let / var, with diagnostic GS0305.
    • name = value named-argument separator, replaced by name: value, with diagnostic GS0315.
    • func(T) R legacy function-type clause, replaced by (T) -> R, with diagnostic GS0303.
    • Go-flavored map[K]V type clause, replaced by map[K,V], with diagnostic GS0366.
    • static func on interface methods is removed. Static-virtual interface members now live inside the interface shared { … } block. A body-less func inside that block is an abstract static-virtual slot; a func with a body is the default. Static private helpers also move into the shared { … } block as private func, while instance private helpers stay directly in the interface body. The old static func … shape now produces a parser error, and GS0330 fires when a non-func member appears inside an interface shared { … } block.
  • Body-less func now requires ;. A func declaration 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 interface shared { … } block. A body-less interface func missing its ; reports GS0368; a func carrying a body still takes no ;.
  • Native interop end-to-end. P/Invoke via @DllImport, source-generator-shaped @LibraryImport, struct and class marshalling, ref / out / in parameter marshalling, function-pointer marshalling, and @MarshalAs parameter 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-ins len, cap, append, make, and delete now require import Gsharp.Extensions.Go. Diagnostics GS0316 / GS0317 point at the missing import.
  • Tooling polish. LSP completion understands async-shaped types such as async (T) -> R and async sequence[T]; textDocument/codeAction offers nil-related quick fixes; and null now produces a nil "did you mean" diagnostic GS0273.
  • Diagnostics catalogue extended. v0.2 introduces GS0273 and GS0288–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, and double are 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 canonical TypeSymbol at 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, so type int = string and equivalents are rejected with GS0102.
  • Null-conditional indexing a?[i]. a?[i] evaluates receiver a exactly once. If it is nil, the whole expression yields nil; 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 ?, preserving cond ? [arr] : [arr] ternary parses. Diagnostics GS0300 and GS0301 cover 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 include GS0227, GS0228, GS0229, GS0230, and GS0231.
  • Named delegate types. delegate Name(...) declares a real CLR MulticastDelegate-derived type so C# consumers see a conventional handler type and G# events can carry first-class custom delegate types. Diagnostics GS0233–GS0234 cover invalid forms.;
  • ref / out / in parameters. Declaration-site and call-site ref-kind modifiers are supported, including inline out var / out let / out _ declarations. Diagnostics GS0235–GS0243 cover the rules. Passing a value to an in parameter without writing in at the call site is warning GS0242 rather than a silent spill.
  • Ref-aliasing locals. let ref m = arr[i] and var ref v = c.Field produce locals whose IL slots are T& and alias another lvalue. Diagnostics GS0256–GS0258 cover invalid aliases.
  • Ref returns. func f(...) ref T { return ref <expr> } is supported. Diagnostics GS0248–GS0255 cover escape rules, async and iterator bans, and override matching.
  • Conditional ref-arguments. The narrow ref cond ? a : b form is supported inside ref-kind argument payloads. Diagnostics GS0260–GS0262 cover invalid forms.
  • Generalized ternary expression. cond ? a : b is now a normal expression. GS0259 is retired in value contexts; GS0263 covers "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–GS0267 cover 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 delegate Invoke. Diagnostics GS0244–GS0247 cover invalid usage. The legacy name = value form is deprecated this release with diagnostic GS0315; migrate .copy(...) and attribute argument lists alongside ordinary call sites.
  • scoped parameter modifier. scoped constrains a ref struct or managed-pointer parameter from escaping, enforced by GS9004 / GS9006.
  • data struct synthesis completed. Every data struct synthesizes Equals(object), Equals(T), GetHashCode(), ToString(), op_Equality, op_Inequality, and Deconstruct(...). Hand-written versions are rejected with GS0232.
  • Editor features. Hover for CLR XML docs, live pull-based diagnostics, CodeLens reference counts on members of structs, interfaces, and enums, implicit this for properties, methods, and events, hover for this, 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 requires let for immutable bindings or var for mutable bindings. The lexer still recognizes := so the parser can emit GS0305 with context-sensitive migration suggestions such as x := 1 → let x = 1, for i := 0 ... 10 → for i in 0 ... 10, and case 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.md block-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 names get / set / add / remove / raise, and ref-kinds ref / out), operators (:=, ?., ??, ? / :, !!, ..., =>), an @Annotation scope, and a /// documentation-comment scope with @tag highlighting. The VS Code snippet set was rewritten to match current grammar.
  • The Docusaurus site cuts a new 0.2 snapshot from the live docs and retires the 0.1 snapshot. The version dropdown lists 0.2 and Next; the 0.1 URL 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; GS0257 is 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, and float64, plus bool, char, string, object, decimal, nint, nuint, and void.
  • Nullable T? types with nil, ?., ??, and !!.
  • Structs, classes, interfaces, enums, data struct, record as a data struct alias, and inline struct value 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 iterator yield support.
  • Control flow including if, for, for in or range forms, switches, switch expressions, try, catch, finally, throw, using, and defer.
  • Go-shaped concurrency with go, scope, channels, channel send and receive, make(chan T), close, and select.
  • async func, await, async lambdas, awaitable-shape support, and await for over 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​

  • gsc compiler 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, .gsproj projects, 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.