Skip to main content
Version: 0.4

Release notes

G# is pre-1.0. The repository's version base is currently 0.4, 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.

0.4.591 at a glance​

This documentation snapshot follows the published v0.4.591 Git tag. The website's installation flow and showcase programs are checked against that SDK, rather than a development build.

G# remains pre-1.0. Read the detailed compatibility changes below before upgrading. The release's source documents described some work as the forthcoming 0.5 line; those implementation notes are preserved here as they stood at the published tag. Later development changes belong to Next release notes.

What an upgrade can change​

If your earlier project uses... Review this change 
chan T or make(chan T, n) Use chan[T] and chan[T](n). The no-capacity constructor is a rendezvous, not an unbounded queue. 
import Gsharp.Extensions.Go The channel syntax no longer needs this gate. Remove the retired import; other helper namespaces still have their own imports. 
close, len, cap, append, or delete built-ins Use the corresponding members or an appropriate growable collection. Follow the diagnostic's specific replacement. 
Custom formatting conventions gsfmt supplies the canonical format. Review formatting-only changes separately from semantic changes. 
Generic constraints or nullable library results Compiler fixes can expose previously accepted invalid contracts. Read the diagnostic and the detailed migration entry rather than suppressing it blindly. 

Before upgrading​

  1. Pin the intended SDK in the project and keep a reproducible baseline.
  2. Read the matching documentation, not an unrelated Next page.
  3. Update affected source forms and run the application's tests.
  4. Check format separately with gsfmt --check.
  5. Review feature support and the detailed notes below for your actual dependencies.

For a known runnable project on this release, use Trail. Its download, source, expected report, and SDK pin are verified together.

Implementation notes from the release​

Breaking changes​

  • 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. Three positions are still not asked about and each is a fixture row rather than a sentence: a composite open argument (GsList[[]T]), a bound that mentions another of the definition's own parameters, and a G#-declared generic INTERFACE, whose own parameters have not resolved their constraints yet when a class body binds (the CRTP shell lifecycle of #2519; filed as #4089, together with the closed-argument gap at the same site as #4090). 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).

Added​

  • 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.

  • 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.

Fixed​

  • 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']. Two neighbouring divergences are pinned rather than ridden along: a contravariant Action[object]/IComparer[object] argument still does not raise the inferred type argument the way csc does (issue #4133), and the named-argument mapping this needs also makes a reordered NAMED argument into a params slot bind — which the parent rejected outright — in parameter rather than source evaluation order (issue #4134, whose PR repairs it). 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 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. 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); 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).

  • 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.

  • 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.

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.

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.