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.
- Create a project with the .NET tools: install the pinned templates and run a
.gsproj. - Model data directly: explore data classes, structural equality, and copy-with-update.
- Coordinate work with channels and scopes: use the current channel syntax, including
chan[T]and member-based operations. - Keep formatting consistent: gsfmt supplies the canonical source format.
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
- Pin the intended SDK in the project and keep a reproducible baseline.
- Read the matching documentation, not an unrelated Next page.
- Update affected source forms and run the application's tests.
- Check format separately with
gsfmt --check. - 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], whereopen class GsHandler[TOptions SchemeOptions]is declared in the same file, compiled with no diagnostic and emitted IL that ILVerify rejects with[UnsatisfiedMethodParentInst] … Method parent instantiation has unsatisfied class type parameter constraints. It now reports the sameGS0580, with the same remedy — forward the constraint,class Fwd[T SchemeOptions] : GsHandler[T], which verified before and still does. The rule is identical; only the substrate differs. A G#-declared generic base is closed by symbol substitution in the type-clause binder and never byType.MakeGenericType, so #4037's checker never saw it, and the implication test is written against the symbolic constraint surface instead. 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.gscorpus was swept: 0 files affected. -
A bare imported dependent constraint (
where TDerived : TBase) is now checked over erased same-compilation classes (issue #4063). #4041 taught the imported constraint check to answer a dependent bound on the symbolic vector, but with INVARIANT IDENTITY — right at a generic argument position, wrong at the top of a bound, where the relation is assignability. Rather than answer with the wrong relation it declined there, andvar c Chain[ChA, ChB]over two unrelated same-compilation classes compiled and threwTypeLoadExceptionnamingTDerivedthe first time the program touched the type. That position now asks the symbols for assignability, through the very predicate the G#-declared spelling of the same bound has used since #4043, so the two spellings cannot drift apart. It reportsGS0152naming the bound the author wrote —TDeriveddoes not satisfy theTBaseconstraint — rather than theSystem.Objectthe erasure substitutes into it.Chain[ChBase, ChDerived],Chain[IFoo, ChImpl],Chain[object, string]andChain[object, int32]all keep binding; an indeterminate answer still falls through to the pre-existing comparison rather than to a rejection. The whole.gscorpus was swept: 0 files affected. -
A generic declaration must now FORWARD the constraints its imported generic base requires (issue #4037).
class Unforwarded[T] : Handler[T], where the importedHandler<TOptions>declareswhere TOptions : SchemeOptions, compiled with no diagnostic and emitted anextendsrow that names an instantiation which is not valid for everyT; ILVerify rejected it with[UnsatisfiedMethodParentInst] … Method parent instantiation has unsatisfied class type parameter constraints, and the type only became unloadable if something ever closed it. It now reports the newGS0580, which names the type parameter, the bound it does not carry, and the remedy — forward the constraint,class Forwarded[T SchemeOptions] : Handler[T], which verified before and still does. This is a different question fromGS0152(#4032), which asks whether a CLOSED type argument satisfies a bound; here there is no argument, so the rule is whether the declaration's own constraint set implies the base's. C# spells this CS0314 — measured againstcsc, not CS0311 as the issue reported, because CS0311 is the concrete-argument case G# already spellsGS0152. The same measurement set the scope:cscreports it at a base clause, a field type, a local declaration, an interface list entry and a return type alike, so the rule sits at the shared generic-type construction sites and every spelling inherits it. A type argument that merely MENTIONS a type parameter (Handler[List[T]]) and a bound that mentions another of the definition's own parameters (where U : IList[T]) are both still skipped — the rule only adds rejections it can prove. The whole.gscorpus was swept: 0 files affected. -
G# formatting is now canonical and option-free (ADR-0179).
gsfmtand the language server always use 4-space indentation, K&R braces, a fixed 120-column width, and LF output; LSP tab-size and insert-spaces options are ignored. Range and on-type formatting are now available, and the VS Code extension enables format-on-save for.gsfiles by default. -
chan Tis respelledchan[T](ADR-0174 D2). The element type moves inside brackets likesequence[T]andmap[K, V];in chan[T]/out chan[T]are the receive-only / send-only handles (ChannelReader<T>/ChannelWriter<T>),chan[T]?is a nullable channel andchan[T?]a channel of nullable — the(chan T)?grouping carve-out is gone. The legacy spelling is rejected withGS0567, which names the exact replacement. -
make(chan T[, n])is retired in favour ofchan[T]()(rendezvous — capacity 0, Go's unbuffered channel),chan[T](n), andChan.Unbounded[T]()(the wave-1 behavior ofmake(chan T), now named).GS0566names the replacement per site; note the semantic change for the no-capacity form. -
close(ch)is retired in favour of the memberch.Close()(GS0566). Closing twice throws (Go's panic);Dispose()is the idempotent close, sousing letworks.len(ch)/cap(ch)becomech.Length()/ch.Capacityon a channel you constructed. -
Receiving from a closed channel no longer raises and swallows
ChannelClosedException— the zero value is delivered directly (about 400× faster on that path). -
import Gsharp.Extensions.Gois gone (ADR-0174 D13): the concurrency syntax is the language, the namespace and its marker type are deleted, and the gate diagnosticsGS0316/GS0317are retired. The concurrency library lives in the implicitly importedGsharp.Concurrencynamespace (/noimplicitimportsdisables it). -
len,cap,append, anddeleteare retired (ADR-0174 D13). A call reportsGS0566naming the member replacement for that site:xs.Length,m.Count,m.Remove(k),ch.Length()/ch.Capacity;appendhas no member spelling — a slice is a fixed CLR array, so keep a growableList[T]and.Add;capon a slice is removed outright (its capacity was its length). A user-defined function of the same name is an ordinary call. -
A nullable argument at a non-nullable G#-declared parameter is now an error even when the parameter's type is still open (issue #3988). Passing a
chan[T]?to achan[T]parameter, a([]T)?to a[]Tone, or aT?to a reference-constrained[T class]one used to compile with no diagnostic at all, while every closed spelling of the same call reportedGS0154. It was not only a missing message: anilreached the non-nullable slot and failed at run time. All of these now reportGS0154, which names the parameter and both types; the remedies are!!at the call site, declaring the parameter nullable, or narrowing the value first — either an ordinary!= nilcheck or an assignment from a non-nullable value, both of which now lift these shapes (assignment narrowing did not, for a slice, array or channel whose element was still open). An UNCONSTRAINEDT?is deliberately unaffected — it erases toNullable<T>and keeps the value-type rules. -
A nullable
map,sequenceorasync sequenceat a non-nullable parameter is now an error, in every spelling (issue #3997). Two holes closed together, because closing either alone would have left the two spellings disagreeing the other way round. First,map[K, V]?,(sequence[T])?and(async sequence[T])?at a non-nullable parameter of their own type compiled with no diagnostic at all whenever the element was still an open type parameter — the shapes were missing from the list of "reference types whose CLR backing is unavailable while binding" that #3985 and #3988 filled in for channels, slices and arrays, so the null-safety gate declined to gate them; anilreached the non-nullable slot and threwNullReferenceExceptionat run time. Second, and contrary to the issue's own diagnosis, the CLOSED sequence spellings were not being rejected either:(sequence[int32])?at asequence[int32]parameter, and a plain importedIEnumerable[int32]?at anIEnumerable[int32]parameter, were both classified as an IDENTITY conversion by thesequence[T]≡IEnumerable[T]alias rule (issue #3093), which compared the two sides ignoring reference nullability. The enumerable shapes were the only reference types in the language exempt from Kotlin-model null safety. All of these now reportGS0154(orGS0155at an initializer), naming the parameter and both types; the remedies are the usual ones —!!, a nullable-typed parameter, or narrowing with!= nilor an assignment from a non-nullable value. Note the migration this implies for unannotated imported libraries: anIEnumerable<T>returned by a C# assembly with no nullable annotations arrives asIEnumerable[T]?(see Unannotated imported reference types are nullable by default, 0.3), so it now needs!!or a narrowing check at a non-nullablesequence[T]orIEnumerable[T]parameter — exactly as that same library'sstringreturn already does at astringparameter. The alias itself is untouched in the directions that drop nothing:sequence[T]andIEnumerable[T]still convert to one another as identity, and both still reach anIEnumerable[T]?slot, so LINQ flowing into asequence[T]parameter is unaffected. Measured blast radius across all 192.gsfiles in the repository and the SDK's own G# sources: zero. -
A bare
nilliteral at a non-null-tolerant parameter is now an error even when the parameter's type is still open (issue #4010). Non-null-tolerant means a slot whose CLOSED spelling already rejectsnil— every shape except a G#-declared class or interface, a reference-constrained[T class], and any?-annotated type, all of which keep accepting it. This is the other half of #3988, which taught the same clause to look at a nullable annotation: anilLITERAL has no annotation to inspect, so thenilargument intakesChan[T](nil),takesSlice[T](nil),takesMap[K, V](nil)ortakesSeq[T](nil)was still passed through unchecked whenever the substituted parameter type mentioned a type parameter — while every closed spelling of the same call reportedGS0154. It was unsound, not merely silent: forwardingtakesMap[K, V](nil)out of a generic and instantiating it at[string, int32]compiled, IL-verified, and threwNullReferenceExceptionwhen the callee readm.Count. All of these now reportGS0154. Three shapes the issue did not name change with them, each of which its closed sibling already rejected: an UNCONSTRAINEDTparameter, a fixed array[N]T, and a function type(T) -> T.nilstill reaches every null-tolerant open shape — a reference-constrained[T class], aT?, achan[T]?, a G#-declared open class or interface — and the remedies are the usual ones: annotate the parameter, or pass a real value. Measured blast radius across all 180.gsfiles undersamples/,src/Sdk/,bench/,e2etests/andtest-assets/: zero. -
Two constructions of one generic type no longer structurally project to each other (issue #4014).
let zs List[string] = xsfor anxs List[int32]used to compile, IL-verify, run, and print0— the ADR-0148 projection planner answered yes becauseList<int>andList<string>share a parameterless constructor and a settableCapacity, so the "conversion" built a NEW, EMPTYList<string>, copied a capacity, and dropped every element. The argument position of the same pair behaved identically. A projection between two constructions of one generic definition is now refused (GS0490at an assignment,GS0154at an argument), because the state the type argument names is exactly what a member-by-member copy cannot carry. The rule is scoped to CLR-backed pairs whose CLOSED types differ, so identity is untouched, a same-compilation G# generic is untouched — a userclass Box[T]still projectsBox[int32]toBox[int64]and carries the value, because a G# class's public fields ARE its state — and an ordinary projection into a generic CLR target from a DIFFERENT source type still works. The refusal is narrow in a second way: it applies only where the two constructions' public member SURFACES are type-identical, so the difference lives in state the author cannot name. Where the surface does carry it — an importedBox[T]whose settableValueisT— nothing changes, and ADR-0148 §B explicit object spread can still supply it (Box[string]{ ...boxOfInt, Value: "replacement" }still compiles); a spread that does NOT supply it still reports the sameGS0490naming the member. One pair that was already rejected changes diagnostic id: this pair at an IMPORTED CLR parameter moves fromGS0155toGS0159, because the candidate is now never applicable rather than applicable-then-unconvertible. Measured blast radius across all 180.gsfiles undersamples/,src/Sdk/,bench/,e2etests/andtest-assets/: zero. -
A generic instantiated over a fixed-length array is a distinct type per length (issue #3962).
List[[3]int32]andList[[4]int32]used to be the SAME type and converted to one another silently:[N]Tis backed by the plain CLRT[], so both spellings resolved to oneList<System.Int32[]>symbol before any conversion ran. The declared length is now kept beside the erased CLR shape, exactly as a same-compilation element or a named tuple already was, so the two reportGS0155/GS0154naming both lengths. This holds through a variant interface (IEnumerable[[3]int32]does not convert toIEnumerable[[4]int32]despiteIEnumerable's covariance, andIComparerlikewise contravariantly) and for a static generic receiver (EqualityComparer[[3]int32].Defaultis not anEqualityComparer[[4]int32]). A length that MATCHES is still one type, a covariant widening over the same length still widens, and an array recovered from METADATA still converts either way — a G#List[[3]int32]reaches a C#List<int[]>parameter, since reflection records no length to disagree with. The SLICE half of the same erasure is closed by #4024, below. -
A fixed-length array's declared length is part of its type (issue #3998). This is the bare rule underneath #3962, and it was wrong in both directions:
[3]int32converted implicitly to[4]int32,[]int32converted implicitly to[3]int32, andvar lit [3]int32 = [2]int32{1, 2}was accepted withlit.Lengthreporting 2.[N]Tis backed by the plain CLRT[]— the same type as[]Tand as every other length — so these pairs fell through to the general CLR reference-assignability rule; the #2516 invariance guard that stops array covariance fired only for a slice or imported-array SOURCE. The rule is now stated as a widening:[N]Tconverts implicitly to[]T(dropping a known length for an unknown one is a representation no-op, so a fixed array still reaches every[]Tslot), and nothing converts implicitly INTO a[N]Texcept a[N]Tof the same length. Rejected pairs reportGS0156(orGS0154at a call argument) and keep their explicitcast[…], exactly as slice covariance does; the cast reinterprets rather than converts, so a[3]int32cast to[4]int32still reports.Length3. An array recovered from METADATA is exempt in both directions — a G#[3]int32still reaches a C#int[]parameter and a C#int[]return still fills a[3]int32slot. The same change also closes two gaps it found.[2]Footo[]Foofor a same-compilationFooused to be REJECTED (GS0154/GS0156), because the widening path existed only for a CLR-backed element; it now widens like every other fixed array. And a covariant element used to reportGS0155— no conversion at all, socast[[]Base](d)did not compile either — and now reportsGS0156with a working cast, matching what[]Derived->[]Basehas had since #2516; element invariance for implicit conversions is unchanged. Zero of the 175.gsfiles undersamples/,src/Sdk/,bench/,e2etests/andtest-assets/relied on any rejected conversion. The member-slot gap this left is closed by #4012, below. -
An explicitly-implemented interface member is no longer a candidate for an ordinary call (issue #4013).
List[int32]().Add("x")COMPILED and threwArgumentExceptionat run time:List<T>implements the non-genericSystem.Collections.IList, whoseAdd(object)accepts anything, and the member-lookup walk that exists to surface DEFAULT interface methods admitted abstract interface members too — so when the argument did not fit the genericAdd(T), resolution fell through to the explicit implementation and silently widened the call site toobject. In C# such a member is not part of the implementing type's surface and is reachable only through an interface-typed reference; it now behaves the same way in G#. A class receiver keeps only genuine DEFAULT interface methods from that walk (they are non-abstract, and the class need not implement them at all); an INTERFACE receiver is unaffected, since a base interface's members really are members of the derived one, and an IMPLICITLY implemented member is on the class surface and still binds. Reaching the explicit member still works through an interface-typed receiver —cast[IList](ys).Add("x"). An inapplicable argument now reportsGS0159, which is what an imported overload with no interface sibling (Stack[int32]().Push("x")) already reported; where the explicit member was the ONLY candidate of that name the call used to compile and now reports the newGS0577, which names the member, the interface that declares it and how to reach it, rather than reading like a misspelling —map[string, int32]{}.Contains(k)is that shape, sinceDictionary<K, V>implementsContainsonly explicitly. Measured blast radius over every.gsfile undersamples/ande2etests/: 27 such members were being offered, every one of them alongside another candidate of the same name on the type's own surface, and the corpus still compiles unchanged. Two call shapes are deliberately NOT narrowed, because the compiler still depends on the member there, and both keep the hole: an ADR-0117 collection literal (and the #3096 spread form), which fills aDictionary[K, V]through the explicitICollection<KeyValuePair<K, V>>.Add; and a call whose argument carries a same-compilation type, whereIList.Add(object)is an erasure escape hatch wherever two erasures of one type disagree (List[System.Action[Mode]].Add((item Mode) -> …)for a same-compilation enum presents the receiver asList<Action<object>>and the lambda asAction<int>). Both turn on something the call site has rather than on the member, so the reported hole — a genuinestringat a genuineint32— is closed;List[int32]{"x"}and the same-compilation-argument form went on compiling and throwing, and are closed by #4028 below. -
A generic's member slot over a fixed-length array follows the retained
[N]T, not the erased CLR shape (issue #4012). Two gaps, one family. First,List[[3]int32]().Add([4]int32{…})was accepted, withxs[0].Lengthreporting 4:List[[3]int32]keeps[3]int32in its type arguments (#3962), but the helper that rewrites an imported member's parameter through them admitted a type argument only when it needed symbolic projection — and a[3]int32has a perfectly realint32[]CLR type, which is exactly the problem, since that type is shared with[]int32and with every other length.Add's parameter therefore stayed theint32[]reflection reports off the erasedList<int32[]>. It is now the[3]int32the receiver carries, so a differing length reportsGS0156(and[]Fooat a[3]Fooslot does too), withcast[…]still available exactly as at the bare level.Stack[[3]int32]'sPushisolates the rule —List[T]also exposesIList.Add(object), which accepts anything (#4013). Second,List[[3]Foo]().Add([3]Foo{…})for a same-compilationFooreportedGS0159 "Cannot find function Add"and was simply unreachable: an argument with no CLR type of its own is ridden through to its erasure so overload resolution can rank candidates, and[]Tand[N,M]Thad that ride-through while[N]Tdid not — so resolution never ran at all. It now presents asobject[], exactly as[]Fooalready did; what a genuineobject[]parameter accepts is unchanged. The third sub-case of the issue —List[[]int32]converting toList[[3]int32], a slice type argument erased before any comparison can see it — is a different mechanism and is closed by #4024, below. -
A generic instantiated over a slice is a distinct type from one instantiated over a fixed-length array (issue #4024).
List[[]int32]converted toList[[3]int32], silently — the last open sub-case of #4012, and the residue #3962, #3998 and #4012 each declined in turn. The cause was one step earlier than any conversion rule: #3962 taught the binder to RETAIN a[N]Ttype argument symbolically, soList[[3]int32]carries[3]int32, but a slice argument was still erased toT[]. That leftList[[]int32]carrying an EMPTY symbolic argument vector — which is exactly what aList<int[]>recovered from METADATA carries, and a metadata array's shape genuinely is unknowable — so the metadata carve-out correctly kept the lenient CLR comparison and there was nothing left to compare. Both G# array spellings are now retained, at the same four gates (declared type, construction, fully-qualified construction, static generic receiver), so the two reportGS0155/GS0154naming both spellings, in both directions, at any nesting depth, in a value position of a multi-argument generic (Dictionary[string, []int32]), and through a variant interface. Note this deliberately does NOT mirror the bare rule, where[3]int32still widens implicitly to[]int32: a generic's type argument is matched by identity, not convertibility, soListbeing invariant inTmakes the two instantiations as unrelated asList[Derived]andList[Base]. The sharpest case is a COVARIANT interface — aList[[3]int32]at anIEnumerable[[]int32]parameter is now refused, even thoughIEnumerableis covariant and read-only and the bare element conversion is implicit — which is the same line #3962 drew when it refusedIEnumerable[[3]int32]toIEnumerable[[4]int32], through the same shared predicate. What does not change: a member slot over a slice keeps the bare widening (List[[]int32]().Add([3]int32{…})still binds), because[]Tand the CLRT[]describe the same shape and need no symbolic rescue; member parameters, indexers and iteration over aList[[]T]are byte-for-byte as before, since a retained slice is still not a substitutable type argument. One member position DOES move, and it is the only breaking change here beyond type identity: a generic's METHOD RETURN follows the retained slice, because that path projects on a nonempty symbolic vector without consulting substitutability. SoStack[[]int32]().Pop()now surfaces[]int32rather than the erasedint32[], and assigning it to a[3]int32goes from accepted to rejected — which is what #3998's bare rule already required of every other position, the old acceptance being the metadata carve-out firing on a type that only looked reflected because the slice had been erased. The explicitcast[[3]int32](…)still works. The indexer is unchanged and is pinned as such; the #1354 nullable-flags path is untouched in substance, because an ANNOTATED slice ([]string?) was already retained before this change and never took that branch, so the arguments newly diverted off it are exactly those with no annotation to lose — the emittedNullableAttributebyte arrays forList[[]string],List[[]string?]andList[string?]are identical before and after; and an array recovered from METADATA is still exempt in both directions and against both spellings — a G#List[[]int32]and a G#List[[3]int32]both still reach a C#List<int[]>parameter, and a C#List<int[]>return still fills either slot. Zero of the 180.gsfiles undersamples/,src/Sdk/,bench/,e2etests/andtest-assets/change a single diagnostic. -
A
map[K, V]reaches the interfaces itsDictionary[K, V]backing implements, not only the class (issue #4011).var d IDictionary[K, V] = mreportedGS0155whenever the key or value was still open, while the closed spelling of the very same conversion (map[string, int32]at anIDictionary[string, int32]) was accepted — the one row where the two spellings disagreed, and it disagreed in the direction that rejects working code. #3987 taught the lattice that amap[K, V]IS aDictionary[K, V]by shape once an element is open, and stopped at the concrete class; the interfaces had no rule of their own, and the reflective assignability rules that would otherwise answer need a CLR type the open map does not have. Amap[K, V]now converts implicitly toIDictionary[K, V],IReadOnlyDictionary[K, V], andICollection/IReadOnlyCollection/IEnumerableofKeyValuePair[K, V](and tosequence[KeyValuePair[K, V]], the alias spelling of the last) whenever the elements match, in that direction only — anIDictionary[K, V]is not necessarily aDictionary[K, V]. The conversion emits no IL: the value already is that interface. One side effect, at a G#-declared parameter: a(map[K, V])?at a non-nullableIDictionary[K, V]now reportsGS0154instead of being silently accepted, because the null-safety gate (#3999) declines to blame nullability when the non-nullable form would not reach the parameter either — and until now it would not. The closed spelling of that row already reportedGS0154, so the two now agree. At an imported parameter nothing changes and nothing disagreed: both spellings are accepted, before and after, which isBindClrParameterConversions' general leniency about a CLR parameter's declared nullability rather than this gap. -
A closed generic type clause whose type argument violates the definition's constraint is now an error (issue #4032).
class Bad : Handler[string], where the importedHandler<TOptions>declareswhere TOptions : SchemeOptions, compiled with NO diagnostic and threwTypeLoadException: GenericArguments[0], 'System.String', on 'Handler`1[TOptions]' violates the constraint of type parameter 'TOptions'the first time the program touched the type.Type.MakeGenericTypevalidates constraints for a live runtime definition and does not for one loaded by aMetadataLoadContext, which is where every/referenceassembly lives; the imported generic METHOD path has compensated for exactly that since #750, and the generic TYPE path never did. Every user-facing construction of a closed imported generic now reportsGS0152naming the type argument, the type parameter and the constraint that failed — the four type-clause spellings (a base clause, a field or local type, a function parameter or return type, a nested type argument) and the three expression spellings that never pass through a clause at all: a direct constructor callHandler[string](), a static member through a closed receiverHandler[string].Describe(), and an object literalHandler[string]{…}. Only CLOSED instantiations are asked: a type argument that still mentions a type parameter is erased to anobjectplaceholder, and asking a constraint of a placeholder is how #4016's projection broke a SIBLING parameter's constraint, so an open instantiation is left alone. A same-compilation class is NOT skipped — the check reads the symbol's own base chain rather than itsobjectsurrogate — soclass MyOptions : SchemeOptionsstill satisfieswhere TOptions : SchemeOptions. The GS0149 half of #4032 as reported (a SATISFYINGclass MyHandler : Handler[MyOptions]rejected as "not generic") did not reproduce on565b9a04across eleven shapes and four reference kinds; those shapes are pinned as green fixture rows. Not covered, each pre-existing, measured on the parent and filed separately: a G# generic class that derives from a constrained imported base without forwarding the bound (class H[T] : Handler[T]) still compiles and emits IL that ILVerify rejects withUnsatisfiedMethodParentInst(issue #4037, the forwarded spellingclass H[T SchemeOptions] : Handler[T]verifies); a DEPENDENT constraint over two distinct same-compilation classes (Coupled[A, List[Bee]]againstwhere U : IList<T>) was still accepted because both erase to the sameobjectplaceholder (issue #4041, closed in this same release by answering a dependent bound on the symbolic vector; the same mismatch spelled with imported types was already caught here); and an object literal of an imported generic over a same-compilation type argument does not bind at all, reportingGS0157(issue #4042, the constructor spelling of the same instantiation works). One violation reports ONE diagnostic at every spelling. The binder PROBES the same receiver several times before committing to a reading of it — a qualified accessor is walked three times through three different entry points, an unqualified one twice — so the first form of this check reported the identicalGS0152three times forSystem.Nullable[string].Valueand twice forHandler[string].Describe()— a count that was a function of how many internal paths the binder happened to take; the report is now made once per expression, in the shared checker, so a future caller cannot reintroduce the fan-out. The qualified spelling also used to FOLLOW the accurate error with a false one:GS0149 "Type 'Nullable' is not generic"is a last-resort fallback the qualified walker reached whenever a closed construction failed for any reason, andSystem.Nullableis of course generic — it is the argument that is wrong. That fallback is now suppressed when the failure has already explained itself, soSystem.Nullable[string].ValuereportsGS0152alone. A genuinely non-generic type named with type arguments still reportsGS0149. Two spellings still emit a CONSEQUENTIAL follow-on rather than a duplicate — an unqualified receiver (Nullable[string].Value) falls through to ordinary index-expression binding and adds twoGS0125s, andHandler[string]()adds aGS0130— tracked as issue #4051.-after(d)andtick(d)round a delay or period UP to a whole millisecond (ADR-0174 D8/D9 erratum 46, issue #4008).System.Threading.Timertruncates aTimeSpanto whole milliseconds, which armed a timer at an interval SHORTER than the one asked for — and, below 1 ms, at a period of zero, which per theTimer.Changecontract disables periodic signalling. Atick(d)with any0 < d < 1mstherefore produced exactly one tick and then went silent for ever, with no error and no diagnostic: measured over a busy-polled 500 ms window, periods of 0.1, 0.5 and 0.9 ms each produced 1 tick against 1 ms's 426, and none of the three reached 20 ticks in 10 s. Both timers now arm a strictly positive interval rounded up to the next whole millisecond, so the interval armed is never shorter than the one requested. Two visible changes for existing programs: a sub-millisecondticknow keeps ticking, at 1 ms; and a fractional period above the cliff slows to the next millisecond instead of running fast — a period of 1.5 ms ticked at 1 ms (426 in 500 ms) and now ticks at 2 ms (249), and 2.5 ms ticked at 2 ms (249) and now at 3 ms (166). (There is no duration literal in G#; both timers take aTimeSpan, so these aretick(TimeSpan.FromMilliseconds(1.5))and so on.)aftermoves with it, so a sub-millisecond delay is no longer truncated to an immediate fire (mean latency over 200 samples: 0.9 ms was 0.007 ms and now lands with the 1 ms row at ~1.19 ms).after(0)is still immediate, andtickstill rejects a period of zero or less withArgumentOutOfRangeException— the same shape Go's ticker panics on. Rejecting a sub-millisecond period was the alternative and was declined: it would maketickpartial on inputs Go'stime.Tickeraccepts, and would say so as a runtime exception inside a program that had been running. -
An explicit type argument naming a BASE of the real element is refused rather than compiled to unverifiable IL (issue #4026).
Probes.CountAny[ImportedBase](List[Derived]())COMPILED, ran, printedany:0— and ILVerify rejected the assembly (StackUnexpected: aList<Derived>found where aList<ImportedBase>is expected). The type argument is genuine, so the MethodSpec really is closed overImportedBaseand the parameter really is the INVARIANTList<ImportedBase>; only the argument's erasedList<object>surrogate made it look applicable, and the #3989/#4006 check that exists to refuse exactly that was waived because the slot's open declaration readsList<T>and so counted as "possibly erased". It is not erased when the call site PINNED it: a slot whose open declaration mentions only method type parameters the call supplied with types that have a CLR identity of their own is no longer exempt, and the call reportsGS0159— the same diagnosticList[Derived]at a genuineList[ImportedBase]parameter already reported. Themapspelling of the same mistake is refused with it, and so is a call that pins one type parameter genuinely beside one it does not (TakeMap[ImportedBase, Derived](map[Derived, Derived]{})), whose genuine invariant KEY mismatch used to be waived along with the whole parameter. Nothing else narrows: a type argument that is itself an erasure SURROGATE (a same-compilation class arriving as its imported base, which is what #4016 made the placeholder produce) is not genuine and keeps the exemption, so every #4016 spelling goes on binding, and a genuinely COVARIANT slot still takes a base type argument —Probes.CountSeq[ImportedBase](List[Derived]())at anIEnumerable<T>parameter, andProbes.CountArray[ImportedBase]([]Derived{…})at aT[]one, since CLR reference arrays are covariant. -
A collection literal, and a call carrying a same-compilation argument, no longer reach a widening explicitly-implemented interface member (issue #4028). #4013 closed this for an author-written call but had to cut two exemptions at candidate COLLECTION — which is memoized on
(Type, name)and cannot see arguments — soList[int32]{"x"}andDictionary[string, int32]{"a": "x"}went on compiling and throwingArgumentExceptionfrom insideIList.Add(object)/IDictionary.Add(object, object), as didList[int32]().Add(someUserTypedValue). The discrimination is now made at applicability, on the collected candidate list, where the receiver's and the arguments' real types are both in hand: an abstract member of a NON-GENERIC interface is kept only when there is a genuine erasure to repair — either the RECEIVER carries a same-compilation type, so its own members' parameter types are surrogates that cannot judge anything (List[System.Action[Mode]].Add((item Mode) -> …)), or the receiver's own same-named member WOULD have taken the call on the arguments' real types (a[]Celsiusspread into aList[float64], whose user-defined conversion the erasure hid). Everything else reportsGS0159, which is what the author-writtenList[int32]().Add("x")already reported. A GENERIC interface's member is never affected: on a constructed class receiver it is closed over the receiver's own type arguments and cannot widen, soDictionary[K, V](){ ...pairs }goes on filling throughICollection<KeyValuePair<K, V>>.Add. -
A user-defined implicit conversion is applied at a call on a generic collection, not only in the spread form (issue #4036).
List[float64]().Add(someCelsius)compiled to verifiable IL and threwArgumentExceptionfrom inside the BCL, while the spreadList[float64](){ ...someCelsiusSlice }printed the value — the same pair of types, two answers. A same-compilation struct has no CLR type while binding, soCelsiuspresents to overload resolution asobject; against aList<double>that is an IDENTITY match forSystem.Collections.IList.Add(object)and no match at all forAdd(double), so the widening member won and theCelsiuswas boxed and handed over as-is. The spread form escapes because it types its item placeholder as the collection's ELEMENT type and converts BEFORE bindingAdd, so applicability never sees the erased argument. The two forms now AGREE rather than share a path: where the receiver's own non-widening surface would have taken the call and some position needs a user-defined implicit conversion to do so, that position is converted before applicability runs. #4028's decision is untouched — the widening member is still KEPT whenever a conversion exists; it simply stops WINNING, because a realdoublebeats a boxed one. Six neighbouring forms close with it, two of them previously silent rather than loud: the collection literalList[float64]{ celsius },Dictionary[string, float64]().Add("a", celsius)andlist.Insert(0, celsius)threw;list.Contains(celsius)answeredFalseandlist.IndexOf(celsius)answered-1; andlist.Remove(celsius)reportedGS0124becauseIList.RemoveisvoidwhileICollection<T>.Removereturnsbool. Named arguments bind correctly at both halves of the decision: the candidacy filter #4028 added, and this repair, each compared SOURCE-order arguments against DECLARATION-order parameters, solookup.Add(value: someCelsius, key: "a")asked whether aCelsiusfitskey: stringand reportedGS0159for a valid call — pre-existing on the candidacy side, measured. Both now reorder through the resolver's own named-argument mapping, the same one #4026 reached for. Still refused, unchanged: an argument with no conversion to the element type (List[int32]().Add(someCelsius),List[int32]().Add("x"), or a reorderedDictionary[string, int32]().Add(value: someCelsius, key: "a")) reportsGS0159. -
A same-compilation user-defined implicit conversion now participates in ordinary imported CLR method overload resolution (issue #4054).
Math.Abs(someCelsius)reportedGS0159even though the sameCelsius -> float64operator already worked at a G# parameter and, since #4036, at a generic collection member. The argument has no CLR type while its assembly is being emitted, so the reflection resolver saw only itsobjecterasure and rejectedAbs(double)before the conversion binder could apply the operator. Imported static, instance, inherited and extension method calls now retain the symbolic argument only for one narrow callback: classify whether the candidate parameter is reachable through the existing implicit user-conversion resolver, including its ambiguity rules. The winning argument is still converted by the established CLR-parameter conversion pass. Erased arguments gain no general applicability, explicit-only operators remain inapplicable, and imported arguments stay entirely on the existing CLR classifier, so their conversion precedence is unchanged. The rule covers ordinary, overloaded, generic andparamsmethods and both value- and reference-returning conversion operators; unrelated conversion targets remain ambiguous and a target with no conversion still reportsGS0159. -
A constraint violation on an unqualified generic receiver no longer cascades into false diagnostics (issue #4051). #4032 made gsc validate declared generic constraints at every construction site, and at the qualified spellings the resulting
GS0152stands alone. At the unqualified ones it did not: the resolver returned "not a type" after it had already REPORTED, so the binder re-readName[Arg]as an ordinary index expression and lookedNameandArgup as values — telling the authorGS0125: Variable 'Nullable' doesn't exist.andGS0125: Variable 'string' doesn't exist.about a predefined type keyword. The resolver now says whether a failure already explained itself, and each caller stops instead of inventing a second reason. Twelve spellings reportGS0152exactly once and nothing else: the eight in the issue plus a static-field read, write and compound assignment (GS0125 x2each) and the generic-name receiver shapeHandler[string?](GS0124+GS0159). The constructor spellingHandler[string]()is one resolver over — it addedGS0130 Function 'Handler' doesn't existabout a type that plainly exists — and now claims the call as a refused CLR construction. Nothing individually correct is suppressed:GS0152is still reported at every spelling, and every satisfying instantiation goes on compiling, IL-verifying and running. Not covered, each pre-existing, measured on the parent and filed separately: a fully-qualified generic CONSTRUCTOR callSystem.Nullable[int32]()reportsGS0157 Cannot find type System, about a namespace (issue #4055); and a static compound assignment through a generic receiver over a same-compilation type argument (Handler[MyOptions].Field += "z") emitsHandler<object>and throwsTypeLoadExceptioneven though the constraint is satisfied (issue #4056, the generic-CONSTRUCTION placeholder erasure of #4016/#4041 one receiver shape over — since fixed, and listed under Fixed below).
Added
-
awaitis legal in a plainfunc, and colours it (ADR-0174 D4'sawait g()row, issue #3954). Awaiting makes the awaiting function suspending, so colourless Go-style code can await an ordinaryTask/ValueTaskwithout becomingasync func. Where inference may not change a signature — an entry point, anopen/override/interface member, an accessor, a constructor, an operator, an iterator, a function literal, anunsafeorfixedbody —GS0574asks forasync func(an observableTask[R]) orsuspend func(implicitly awaited,R).GS0132keeps only the case where no enclosing function exists. -
An awaitable means in G# what it means in C#. The compiler awaits for you only where the syntax is a channel operation —
ch <- v,<-ch,select, channelfor..in— never because a library method returns a task.ReceiveBatch/SendBatchare ordinaryValueTask[int32]calls: writeawait ch.SendBatch(items), and.AsTask()on one names the task exactly as in C#. -
Gsharp.Runtime.Channels: a C#-authored channel runtime bundled with the SDK (tools/channels/) and auto-referenced by every project.Chan<T>is a rendezvous-capableChannel<T>subclass with Go-exact close semantics, a two-value receive, batch transfer, and the transactionalselectwaiter protocol;gsccopies it beside an emitted program that references it. -
Directional channel types and the D2 operation matrix: foreign BCL channels, readers, and writers flow into
chan[T]/in chan[T]/out chan[T]with no adapter — and back out, so achan[T]is accepted wherever a C# API asks for aChannel<T>, aChannelWriter<T>, or aChannelReader<T>(issue #3976). The lattice reads the types rather than the spelling, so a plain BCLChannel<T>now converts implicitly to a writer or reader target — thechan[T]->out chan[T]row written in CLR names, and the same conversion C# has always had throughChannel<TWrite, TRead>'s implicit operators. This holds when the element type is still open: insidefunc f[T](ch chan[T]), the channel reaches an imported constructor or method takingChannel<T>/ChannelReader<T>/ChannelWriter<T>(issue #3876), where it previously reported that no overload was applicable. -
A
: base(...)initializer argument whose type carries an open type parameter now resolves against the base's constructors (issue #3984).class MyList[T] : List[T] { init(items []T) : base(items) {} }reported thatListhad no one-argument constructor; the argument was never ranked at all.[]T,map[K, V],chan[T]and a tuple with a symbolic element all bind in that position now, and a base call that really is inapplicable reportsGS0267(naming applicability) rather thanGS0214claiming an arity the base does declare. Two shapes that used to compile become errors, because they were emitting unverifiable IL: an argument whose element does not match the base's type argument, such as[]stringor(int32, object)passed to aBase[T]'sIEnumerable<T>/(int32, T)parameter. -
A C#-authored generic extension on
ChannelReader<T>orChannelWriter<T>is now found on thechan[T]spelling callers actually write (issue #3877), so a C# library can publish an extension onChannelReader<T>/ChannelWriter<T>and have it bind on a whole channel. Method type inference contributes the channel's element through the direction lattice; the lattice itself is unchanged, so anout chan[T]still finds no reader surface, anin chan[T]no writer surface, and the element still has to match. The batch surface (D10) drops the fourChannel[T]overloads that existed only to work around this. -
A C#-authored generic method or CONSTRUCTOR over a
ChannelReader<T>orChannelWriter<T>also binds when the channel's element type is declared in the same program —chan[Pair]for a G#struct Pair, as well as an openchan[T]inside a generic function (issue #3982). Applicability compares the channel against the candidate at the same level of erasure, and the winning candidate is closed over the real element, so the emitted call carriesPairrather thanobject. The lattice is unchanged: anout chan[T]still finds no reader surface, anin chan[T]no writer surface, and the element still has to match. A genuineChannelReader<object>parameter stays out of reach, so a library that declares one beside its generic overload does not change which one a channel call picks. -
A nullable channel reaches a non-nullable imported
Channel<T>parameter whatever its element type (issue #3985).chan[T]?there used to reportGS0155where the closedchan[int32]?— and astring?at a non-nullablestringparameter — was accepted; both spellings now behave the same way, so!!is no longer needed at such a call, and the same now holds at a generic method's or generic class'sChannelReader<T>/ChannelWriter<T>parameter. This is a CLR-boundary rule only: passing achan[int32]?to a G#-declaredchan[int32]parameter still reportsGS0154, and direction and element are still enforced. -
A nullable channel also reaches a non-generic imported
ChannelWriter<T>/ChannelReader<T>parameter (issue #3992), so the last slot where achan[int32]?behaved differently from achan[int32]is gone:Plain.W(c)used to reportGS0159for the nullable form and bind for the non-nullable one. Applicability had already been ignoring the annotation wherever the CLR shapes were related (achan[int32]?reaches aChannel[int32]parameter exactly as astring?reaches astringone); only the fallback that ranks a candidate on G#'s structural shapes still re-asked the question on the annotated type, which is why the ADR-0174 direction view — the one relation the CLR does not carry — was the only one affected. Nothing becomes applicable whose non-nullable form was not already applicable, and the same limits hold: direction and element still decide, and a G#-declared parameter still reportsGS0154. -
A nullable channel reaches a non-nullable imported
Channel<T>parameter whatever its element type (issue #3985).chan[T]?there used to reportGS0155where the closedchan[int32]?— and astring?at a non-nullablestringparameter — was accepted; both spellings now behave the same way, so!!is no longer needed at such a call, and the same now holds at a generic method's or generic class'sChannelReader<T>/ChannelWriter<T>parameter. A nullable channel still does not reach a non-genericChannelReader<T>/ChannelWriter<T>parameter (issue #3992). This is a CLR-boundary rule only: passing achan[int32]?to a G#-declaredchan[int32]parameter still reportsGS0154, and direction and element are still enforced. -
A
map[K, V]reaches aDictionary<K, V>parameter, and a tuple aSystem.ValueTuple<…>one, when an element is still open (issue #3987).map[K, V]ISDictionary<K, V>(ADR-0104) and a G# tuple IS aValueTuple<…>(ADR-0158), and with closed elements the compiler said so — but the rule compared the two sides' CLR types, and an open element leaves neither side with one to read, sofunc makeMap[K, V](entries map[K, V]) MapHolder[K, V] { return MapHolder[K, V](entries) }reportedGS0155against the parameter it already matched. Both spellings are now recognised by shape, in either direction and at any call position. One shape that used to compile becomes an error, because it was emitting unverifiable IL: a tuple whose element does not match the target's type argument, such as(int32, object)passed to a(int32, T)parameter. -
An argument whose type carries a same-compilation or open element no longer binds to a genuine
objectslot (issue #3989). A same-compilation type argument erases toobjectwhile binding — that erasure is what lets[]Pair{…}.Count()reach LINQ — but at the erased level it was indistinguishable from a realobject, soList[Pair]bound to a parameter that genuinely isList<object>and emitted IL ILVerify rejects; the covariant sibling (IEnumerable<object>) went further and threwEntryPointNotFoundExceptionat run time, because CLR variance does not reach a value-type element. Applicability now asks whether this slot'sobjectCOULD have come from erasure — the same question #3982's channel gate asks, with the opposite answer acted on — and refuses the bind when it could not. What the erasure exists to serve is untouched: an inferred generic slot (Count[TSource](IEnumerable[TSource]),M[T](List[T])), a parameter that genuinely isobject, a reference element riding real covariance, and every non-generic target (System.Array,IEnumerable,ICollection,IList,IDictionary) all bind exactly as before. A library that declares anobjectoverload beside a generic one now picks the generic one instead of the overload that cannot hold the argument. -
An erased CLASS surrogate no longer binds to the imported base it erased into (issue #4006). A same-compilation class has no CLR identity while binding, so it is presented as its IMPORTED BASE — and at that level
List[Derived]was indistinguishable from a genuineList<ImportedBase>. Both spellings of the confusion emitted IL ILVerify rejects, becauseList<T>andDictionary<K, V>are INVARIANT, and each reached the parameter by a different route:map[string, Derived]matched a genuineDictionary<string, ImportedBase>by IDENTITY, which #3989's gate declined to question because it only looked for a nestedobject;List[Derived]was not applicable by CLR shape at all, and was instead admitted by the ADR-0148 structural-projection fallback, which asked the projection planner directly and got a nonsense "yes" (twoLists share a parameterless constructor and aCapacity) for a pair the conversion layer refuses outright — so nothing was emitted and the raw list was pushed. One gate now covers both: the erased-argument check derives its surrogate set from the argument itself rather than looking only forobject, and applicability asks it on the structural-projection arm too. Everything legitimate is untouched: covariance still binds (IEnumerable<ImportedBase>, and[]Derivedat anImportedBase[]parameter, since CLR arrays really are covariant), a genuineList[ImportedBase]orDictionary[string, ImportedBase]still binds, an inferred generic slot (M[T](List[T]), and an imported delegate's erasedInvoke) is never second-guessed, and a real ADR-0148 projection at an imported method parameter still binds and emits. The same-compilation ENUM surrogate had no such hole —List[MyEnum]at aList<int>parameter was already refused, and still is. -
A symbolic tuple at an imported generic method's parameter now emits verifiable IL (issue #4000). Inside
func firstOf[T](pair (int32, T)), the callProbes.FirstOf[T](pair)compiled, ran, and printed the right answer while emitting IL ILVerify rejects: the argument was converted against the ERASED parameter shapeValueTuple<int32, object>— boxing the element and rebuilding the tuple — and handed to a call closed over the realT, which wantsValueTuple<int32, T>. The erased-slot recovery that already handles a bareT, aT?, a same-compilation type, a lambda and a channel now handles a tuple too, so the argument is identity and nothing is rebuilt. This holds for the open element in either position and nested one level down. Nothing else changes: a parameter that GENUINELY is(int, object)still boxes and rebuilds, and themap[K, V]sibling of the same call, which was already correct, is unaffected. -
A
map, slice or fixed-length array of a same-compilation class reaches an inferred generic slot when the type arguments are written explicitly (issue #4016).Probes.CountAnyMap[string, Derived](entries)reportedGS0159while theListspelling of the same call bound, and while the very same map call bound when the type arguments were INFERRED. Two projections of one class disagreed: every structural spelling presents a same-compilation class as its IMPORTED BASE (class Derived : ImportedBasearrives asImportedBase), but an explicit type argument closed the candidate over a flatSystem.Objectplaceholder — so aDictionary<string, ImportedBase>argument was ranked against aDictionary<string, object>parameter, which is invariant, and no candidate applied. The placeholder now uses the same erasure the arguments use, so the two sides agree; this is the correction #3087 already made for symbolic TUPLE type arguments. The issue's own boundary was narrower than reported and is corrected on it: inference always bound, a same-compilation class with NO imported base always bound (its erasure was alreadyobject), and the failure was nevermap-specific —[]Derivedand[N]Derivedat aT[]slot failed identically. Nothing is loosened: #4006's rejections stand, so an erased class surrogate still does not satisfy a GENUINE (non-erased)List<ImportedBase>orDictionary<string, ImportedBase>slot in either spelling. -
GS0548(advisory:chan[T]()is a rendezvous channel),GS0549/GS0550(send/receive through the wrong directional handle),GS0554(a channel-receive binding form with the wrong number of targets),GS0555(while let v = chwhere a receive was meant). -
go { … }(ADR-0174 D14): the block form ofgospawns the block as a goroutine, capturing the enclosing locals (per iteration for afor … invariable) —go func() { … }()without the ceremony. -
scopeis completed (ADR-0174 D5/D6): everygoinside a scope reports to the scope's frame (noTaskper goroutine); the block binds an implicitctx(Gsharp.Concurrency.Context) that the first goroutine failure cancels promptly; exit follows the documented precedence table (ScopeExceptionwith the cause first, a failing body rethrown unwrapped) and, inside a suspending function, is awaited rather than blocked. A nested scope'sctxis linked to the enclosing one (ctx.Parent), andreturnfrom inside a scope still counts as returning on every path. A freegooutside any scope is fail-fast on an unhandled exception (GoroutineRuntime.UnhandledGoroutineExceptionto observe). -
A small concurrency library (ADR-0174 D9):
after(d)andtick(d)are selectable timers —case <-after(d)is the timeout arm, and a select receive arm now accepts anything selectable, not only a channel — andmerge(a, b, …)fans several channels into one receive-only channel that closes when the last input does. They live in the implicitly importedGsharp.Concurrencynamespace and are called by bare name; a program that declares its ownafterkeeps its own. -
Batch channel operations and
chunks(ADR-0174 D10). A channel handle gainsTryReceiveBatch/TrySendBatch(aSpan[T], never waits) andReceiveBatch/SendBatch(aMemory[T], can park, and returns aValueTask[int32]youawaitlike any other — issue #3954 — a destination that survives a park cannot be ref-like).chunks(ch, n)reads a channel in batches:for batch in chunks(input, 1024)is ordinary channel iteration wherebatchis aReadOnlyMemory[T]that the receiver owns, so one lock acquisition and one park are amortized across the batch. A batch cut short by a close or a cancellation returns the count it moved rather than throwing, so a retry cannot duplicate elements.GS0562warns when the channel is a rendezvous, where batching is correct but pointless. -
async let(ADR-0174 D15):async let name = exprstartsexpras a child of the enclosingscopeand bindsnameto its eventual result, read asawait name. Both children of twoasync letbindings run concurrently; the binding is a value of typeR, never a handle, so a spawn cannot outlive the block that owns it. A binding you never read has its child cancelled and joined at block exit, and a failure nobody read still reaches the block'sScopeExceptionrather than being dropped; a failing child does not cancel its siblings. New diagnostics:GS0551(no enclosingscope),GS0559(never awaited),GS0569(read withoutawait). -
Three more
selectarm shapes (ADR-0174 D8). Every arm may carry awhenguard, evaluated once when the select is entered; a false guard keeps its arm out of the select entirely, which is how G# spells Go's "set the channel tonilto disable this case".case await taskandcase let v = await tasklet aTaskorTask[T]race the channels on the same waiter.case cancelledreplaces Go'scase <-ctx.Done(): the ambient context's cancellation becomes an arm instead of an unwind. New diagnostics:GS0556(a guard that is not abool),GS0557(acancelledarm with no context to observe, so it would be unreachable), andGS0564(one channel both sent to and received from by the same select). Cancellation also now reaches aselectwritten in a callee with noscopeof its own, through the caller's context. -
selectis rebuilt on one registered waiter (ADR-0174 D8): arms are attempted in uniform-random order like Go's, so two ready arms are both chosen instead of the first one written always winning; a select with no ready arm registers on every arm at once and parks the state machine rather than blocking a thread onTask.WhenAny; and winning is the transfer, so a value can no longer be stolen between choosing an arm and running it. Aselectinside a cancelledscopeunwinds. -
Cancellation unwinds parked channel operations (ADR-0174 D7): a receive, send, or channel loop inside a
scopeparks on that block'sctx, so the first goroutine failure collapses siblings that are waiting on a channel instead of leaving them parked. An operation that already committed keeps its value — cancellation wins only before the transfer. Hosts can retune the budgets and observe the diagnostics throughGsharp.Concurrency.GsharpRuntime(DeferGraceBudget,ScopeStallTimeout,DeferGraceExpired,ScopeStalled). Cancellation crosses calls: a suspending function receives the caller's context as a trailing optional parameter, so an operation inside a callee unwinds with the caller's scope. Declaringctx Contextin a signature uses that parameter instead, and a C# consumer calls the signature as written or passes aContextexplicitly. -
defercleanup is shielded (ADR-0174 D7): a deferred body runs under a cancellation-immune context, so cleanup that drains a channel or signals completion still runs while the block around it unwinds. A grace budget bounds it (GSHARP_DEFER_GRACE_MS, five seconds by default) so cleanup cannot hold cancellation up forever;GsharpRuntime.DeferGraceExpiredreports an abandoned one. Cleanup outside any scope costs nothing. -
Suspension is inferred (ADR-0174 D4): a plain
functhat performs a channel operation, or calls a function that suspends, is compiled as a suspending function — aValueTask[R]state machine labelled[Suspending]— with no keyword, as a fixed point over the call graph. Go-shaped pipelines keep their shape and stop holding threads. Inference stops atasync func,open/override/abstract methods, interface members and their implementations, constructors, accessors, operators, P/Invoke, iterators,Dispose, and function literals; a call to a suspending function from one of those blocks through the runtime's root bridge and warns withGS0558. Every emitted G# method that suspends now returnsValueTask/ValueTask[R]instead ofR— a binary change for C# callers of such functions. -
suspend func(ADR-0174 D4, the declared form): a suspending function compiles to aValueTask[R]-returning state machine on the pooling builder, carries[Suspending], and is awaited implicitly by every G# call site — callers writelet v = take(ch)and seeR; an explicitawait take(ch)is accepted as the same thing. From a function that is neither suspending norasyncthe call blocks the thread andGS0558warns; the entry point is the silent root. Inference of suspension for plainfuncfollows in a later step of Phase 3. -
Debugging through suspension (ADR-0174 P3-8): every async or suspending kickoff carries
[AsyncStateMachine]and[DebuggerStepThrough], soEnvironment.StackTraceand debuggers name the logical function instead of<f>d__1.MoveNext, and the Portable PDB carries the async-method-stepping blob (yield/resume offsets per await) that lets a debugger step over a parked channel receive onto the next source line. Fixed: bodies rewritten by the compiler — everyasync funcbody, and now every suspending one — carried no line information at all, so breakpoints inside them never bound; each source line of such a body now maps into the state machine. -
Channel operations inside an
async funcsuspend instead of blocking (ADR-0174 D4, first step): a receive, two-value receive, send,for v in ch, orwhile let v = <-chin anasync func(or async lambda /async sequence) parks the state machine, not a thread — hundreds of parked receives no longer pin thread-pool threads. A channel operation inside alockbody keeps the blocking lowering (the monitor is thread-affine). -
Observable completion (ADR-0174 D3): the two-value receive
let (v, ok) = <-ch(orv, ok = <-ch) distinguishes a delivered zero value from a closed channel;while let v = <-ch { … }andfor v in ch { … }loop until the channel is closed. Achan[T?]element that isnilis delivered, not mistaken for close. -
A type parameter may be constrained by another type parameter (issue #4043). C#'s
where TDerived : TBasehas a G# spelling —[TBase, TDerived TBase]— that the parser already accepted and the binder then rejected withGS0153: Type 'TBase' cannot be used as a type-parameter constraint because it is neither an interface nor a class. The constraint resolver walked an interface branch, a user-class branch and an imported-class branch and simply fell off the end for a type parameter. All three placements are now bound and emitted: a generic function's own list (func Dependent[TBase, TDerived TBase]()), a generic type's own list (class Pair[TBase, TDerived TBase]), and a generic method's parameter bounded by its ENCLOSING type's parameter (class Box[T] { func Accept[U T](u U) }). The bound projects onto aGenericParamConstraintmetadata row namingVAR(n)orMVAR(n)— the encodingcscemits for the same C# source, measured by reflecting over both outputs — so the assembly IL-verifies. A type argument that does not equal, derive from or implement the argument supplied for the bounding parameter reportsGS0152naming the bounding parameter; where the relation has no closed answer because either side still mentions an unsubstituted type parameter, the instantiation is accepted, since the CLR only loads it once it is closed. A dependent bound deliberately does NOT propagate the bounding parameter's reference-type nature:[TBase class, TDerived TBase]does not makenillegal atTDerived, because the CLR constraint row carries no such implication and the looser reading is the #4027 unverifiable-ldnulldefect one release later. A circular chain ([T T],[A B, B A]) reports the newGS0581, mirroring C#'s CS0454. This was the single compile error the self-migration gate reported fortest/Core.Tests.
Fixed
-
An inferred imported generic overload now keeps a type-parameter identity conversion ahead of a genuine
objectoverload (issue #4086). Insidefunc callIt[T DisposableBase](value T),Overloads.Take(value)overTake<T>(T) where T : IDisposableandTake(object)erased the caller'sTtoSystem.Object. Both candidates therefore appeared to take the argument by identity, and the later parameter-specificity tie-break preferred the non-genericobjectslot;cscinstead picks the inferredTake<T>(T). The resolver now carries source argument symbols beside their CLR erasures and checks the candidate's OPEN parameter: a method type-parameter slot keeps identity only when its recovered symbolic argument is the same source parameter and no genuineobjectbound also fixed that method parameter; otherwise the real reference/boxing conversion is ranked. The prerequisite thatT : DisposableBaseimpliesIDisposableis supplied by #4070/#4088's rule, which this change relies on rather than duplicates. This is not a generic-over-non-generic preference: an actual or mixed-inferenceobjectargument still selects the non-generic overload. Symbolic method arguments also survive constrained imported instance/static dispatch through async rewriting/spilling into conversion and MethodSpec emission — measured, that path also stops emitting IL ILVerify rejected with[StackUnexpected] … [found value 'T'][expected ref 'object']. Two neighbouring divergences are pinned rather than ridden along: a contravariantAction[object]/IComparer[object]argument still does not raise the inferred type argument the waycscdoes (issue #4133), and the named-argument mapping this needs also makes a reordered NAMED argument into aparamsslot 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, mixedTplusobjectinference, constrained instance/static calls, and genuine-objectcontrols. -
A
typeofattribute argument now keeps the type arguments supplied by an enclosing generic (issue #4095).@Marker(typeof(Outer[Status].Inner))compiled, IL-verified and ran, but the custom-attribute blob wroteP.Outer`1+Innerwith no instantiation, so reflection handed back the OPENOuter<T>.Innerrather than the type the source names — a wrong answer that resolves, which is why nothing threw. A nested type records the encloser's vector onEnclosingTypeArgumentsand leavesTypeArgumentsholding only what its own level declares, so the constructed-generic arm added in #4073 never fired for it. The serializer now writes the flattened enclosing-then-own vector, matching the GenericParam rowsTypeDefEmittercopies down each nesting level (ADR-0087 §3 R1). A nested ENUM takes the same repair:EnumSymbol.ConstructNestedrecords the identical vector, andtypeof(Outer[Status].Kind)wrote the openP.Outer`1+Kind. Two further shapes were wrong for a second reason and are fixed with it: the blob computed a nested name's backtick suffix from the FLATTENED parameter count while the TypeDef declares only the parameters of that level (issue #2916), sotypeof(Outer[Status].GenericInner[string])namedP.Outer`1+GenericInner`2— a type the emitted metadata does not declare, which made the attribute undecodable rather than merely wrong; the suffix is now taken from the same expressionTypeDefEmitteruses, so the two cannot drift again. An operand whose identity still depends on an IN-SCOPE type parameter —typeof(Outer[T].Inner)insideclass C[T]— now reportsGS0202instead of silently serializing the erased close as a different closed type; an unbound generic DEFINITION such astypeof(Box[_])ortypeof(Outer[_].Inner)stays legal and reifies as the open type. Every other operand shape keeps the path it had. The nested class, the nested enum, a nested type with its own argument, a three-level nesting, and enclosing arguments that are themselves a source generic, an imported generic, an interface, a delegate, a nilable value type or a nilable reference are compiled, IL-verified, read back through raw metadata and aMetadataLoadContext, and materialized at run time. -
A method group used as an ordinary value now reports
GS0582before emit (issue #4065). Member chaining such asitems.Add.ToString(), targetless inference, ordinary object-typed arguments/returns, and targetless conditional arms previously retained an unresolved method group until IL emission, where they surfaced as the internal-compiler-errorGS9998. The binder now records whether contextual conversion supplied a delegate target and rejects only groups that escape without one. The public diagnostic names the group and distinguishes user/imported instance, static, and extension groups; successful calls, natural method-group interpolation, and delegate-targeted assignments, arguments, returns, and conditional arms are unchanged. -
Imported generic member projection now keeps both symbolic receiver arguments and their nullable flags (issues #4059 and #4066). A CLR generic constructed over a same-compilation G# type no longer reads members typed by that parameter as the binding-time
objectsurrogate, and a receiver imported as (for example)Dictionary<string, string?>no longer drops the value argument's?. Property reads/writes, fields, method returns/parameters/out, indexers, nested generic member types,.Values, enumerators andforiteration now substitute from the receiver's symbolic vector, while declaration-site nullable metadata is merged back over concrete positions. Annotated and non-annotated receivers therefore remain distinct through binding and emitted metadata. -
A class constraint reached through a dependent type-parameter bound no longer reports a false
GS0152(issue #4091). Infunc f[U SchemeOptions, T U](), constructingGsHandler[T]whereGsHandler[TOptions SchemeOptions]requiresSchemeOptionswas rejected even thoughT : U : SchemeOptions. The shared class-constraint predicate followed onlyClassConstraint, while dependent bounds deliberately live in the separateTypeParameterBoundslot. It now walks both links with reference-identity cycle detection, so every generic construction path accepts transitive class bounds without imposing an arbitrary depth limit. -
A dependent constraint now propagates the bounding parameter's
classconstraint, sonilis legal at[TBase class, TDerived TBase](issue #4062).return nilat aTDerivedslot reportedGS0155: Cannot convert type 'nil' to 'TDerived'.wherecscaccepts it: C# propagates a bounding parameter's special constraints to the bounded one, andTDerived : TBase : classprovesTDerivedis a reference type in every instantiation.[T class]on its own was already accepted (#2354's rule); the reference-ness simply did not travel down aTypeParameterBoundchain. #4043 kept that slot separate fromClassConstrainton purpose — roughly forty consumers readClassConstraint != nullboth as "this is a reference type" AND as "this is the type it derives from", and merging them is #4027's defect shape — so the repair answers only the reference-ness half, in one depth-bounded chain walk read by exactly one consumer. The emitted IL is unchanged and needed no new path:nilat any bare type-parameter target has been lowered todefault(T)(ldloca; initobj; ldloc) since #4027, and the dependent-bound body is asserted opcode-for-opcode identical to the bare[T class]one. Every sink is covered — areturn, a local initialiser, an assignment, and an argument — as are a three-link chain, a root carrying a class-BASE bound rather than the bare flag, and a method parameter bounded by its enclosing type's parameter. This is a completeness fix, never a soundness one: a chain whose root is unconstrained,struct-constrained, or interface-constrained still reportsGS0155, because those admit a value type andnilat a value-type slot is exactly what #4027 fixed. -
An imported generic method constrained to an interface now binds when the type argument is a type parameter whose CLASS bound implements it (issue #4070).
Probes.NeedsDisposable[T]()insidefunc callIt[T DisposableBase]()— overNeedsDisposable<T> where T : System.IDisposableandDisposableBase : System.IDisposable— reportedGS0159: Cannot find function NeedsDisposable.whilecscaccepts the C# equivalent.T's own bound proves the interface in every instantiation, so the candidate was legal; it was FILTERED out of overload resolution rather than diagnosed, which is why the author saw "cannot find function" instead of a constraint message. The question "does this type parameter satisfy an interface constraint" is asked in exactly two places: the generic-TYPE construction path walked the parameter's class-constraint chain (since #4037), and the generic-METHOD path did not (since #750/ADR-0088), soclass Forwarded[T DisposableBase] : DisposableConstrained[T]bound while the method call did not. That walk is now EXTRACTED and called from both, so one rule answers one question; the shared leaf that reads only the declared interface bounds is left byte-identical, because its other callers need that narrower meaning. Every arm is covered: an imported class bound, an interface inherited from the bound's own base, a same-compilation class bound reaching the interface through its imported base, and a same-compilation class implementing the imported interface directly. The rule skips a bound that MENTIONS a type parameter, which is the same skip the generic-type path applies: an unguarded first version boundProbes.NeedsEquatable[T]()underwhere T : IEquatable<T>from a same-compilationclass GsEq : IEquatable[GsEq], and that program compiled and then threwSystem.Security.VerificationException— bothIEquatable<T>and the erasedIEquatable<GsEq>project toIEquatable<object>, so the walk proved an implication that does not hold, andcscreports CS0311 on the C# equivalent. The skip is narrow: a constructed bound over concrete arguments (where T : IEnumerable<string>from[T StringBag]) still forwards, and a self-referential bound carried by the parameter itself ([T IEquatable[T]]) never reached the new rule at all. The change is monotone — it can only turn a rejection into an acceptance — so nothing the CLR refuses is newly accepted, and an unconstrained parameter, a class bound implementing a different interface, and a class bound implementing none are all still refused. No overload winner moves, which was measured on both sides rather than reasoned about: the explicit spelling of a call with a constrained and an unconstrained sibling reportedGS0159before (an explicit type argument removes the non-generic sibling from the candidate set entirely) and now picks the constrained one, ascscdoes, while the inferred spelling picks the boxing overload both before and after. Three neighbouring gaps stay open and are filed rather than ridden along: a BASE-CLASS constraint at a generic method (#4083), which lives on the non-interface fallthrough every imported generic method call takes; a DEPENDENT bound (#4084), which neither path reads; and that inferred spelling's divergence fromcsc(#4086), which this change leaves exactly where it found it. -
A generic attribute's
System.Typeargument now names the type the source wrote, not its erased surrogate (issue #4073).@JsonConverter(typeof(JsonStringEnumConverter[ConstructStatus]))over a same-compilation enum serialised the constructed generic withSystem.Int32in the argument slot, and reflection materialisation threwGenericArguments[0], System.Int32, on JsonStringEnumConverter<TEnum> violates TEnum : struct, Enum— the assembly passed ILVerify both before and after, because the defect is in the custom-attribute blob rather than in any instruction. A same-compilation type has no CLR type while binding, so a constructed generic over one closes over a surrogate and keeps the real argument only on its symbolic vector; the attribute binder preferred the CLR type whenever it was non-null, and for a constructed generic it always is. The enum is not special: measured across the argument kinds, a same-compilation CLASS wroteSystem.Object, a nested generic wrote the surrogate at depth, a two-parameter generic wrote one of each, a slice argument wroteSystem.Int32[]and a nilable enum wroteSystem.Object. Sweeping the sibling shapes found two more kinds the issue did not name. A same-compilation GENERIC —typeof(MyBox[Status])— serialised as the bareP.MyBoxwith neither arity suffix nor argument list, because a constructed user generic carries its arguments onTypeArgumentsand leavesTypeParametersempty; that name does not resolve, so the attribute could not be decoded at all, with an imported argument (MyBox[int32]) just as much as an erased one. And an attribute whoseType[]argument contained a same-compilationtypeofwas dropped from the assembly ENTIRELY with no diagnostic, because #3684 widened the constant container toobject[]to hold the placeholder and the constructor matcher then refusedobject[]for aType[]parameter. All three are fixed together, and every position is covered: positional, named, and array element alike. A fully imported argument such astypeof(Box[int32])has a faithful CLR type and keeps the byte-identical path it always took, as does a scalartypeofof a same-compilation type. A nullable REFERENCE annotation (typeof(Box[string?])) is deliberately left on the CLR path: it is metadata over an entirely faithful runtime shape and ECMA-335 II.23.3 has nowhere to record it. One shape in this family is measured and left alone — a nested type inside a generic OUTER (typeof(Outer[Status].Inner)) still loses its enclosing argument, which needs a different vector and a decision about the nested TypeDef's declared arity; it is filed as issue #4095. 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 theType[]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 threwCould not find type 'map' in assembly 'P.dll'; the tuple form failed asThe given assembly name was invalid, and the function and channel forms named(Status) -> boolandchan. Each structural symbol computes its own CLR type by closing an open BCL definition over its components, a same-compilation component has none while binding, so the symbol's CLR type came out null and the serialiser fell through to a baretype.Name. These operands are only reachable NESTED inside a generic argument: at top leveltypeof(map[string, Status])is rejected byGS0202, which gates on the top-level operand's CLR type, while an imported generic closed over the erased surrogate HAS one, so the component reached emit unchecked. Five kinds are repaired together rather than one at a time, each taking its projection from the symbol that already owns it: a map becomes aDictionary<K, V>, a sequence anIEnumerable<T>orIAsyncEnumerable<T>through the same shape probe the rest of the compiler uses, a channel theChannel<T>/ChannelReader<T>/ChannelWriter<T>its direction already selects, a tuple theValueTuplefamily with the tail nested intoTRestabove arity 7, and a function type anActionor aFuncby the same void rule its CLR builder applies. Every open definition is resolved through the COMPILATION's reference set rather than the emitting host's, so the assembly named is the one the emitted assembly actually references, and the components are closed over SYMBOLICALLY — never throughMakeGenericType, which is what has no answer here. The arms are deliberately not gated on a null CLR type, and that matters:map[string, List[Status]]has a component whose CLR type is non-null — aList<object>closed over the erasure surrogate — so the map's own CLR type resolved, and the old fallback wrote a name that RESOLVED, toDictionary<string, List<int>>. A row that reads back as a plausible wrong type is worse than one that throws, and a null-check guard would have left it in place. The existing wrapper arms for slices, arrays, rectangular arrays and nilables already recurse symbolically without such a guard, for the same reason. The same kinds over components that DO have a CLR type —map[string, int32],(int32, string),func(int32) bool,chan[int32]— are what identify the trigger, and they are asserted unchanged. A function type above the shippedFunc/Actionarities still has no spelling at all and is left as it was rather than given an invented name. -
A static compound assignment through a generic receiver over a same-compilation type argument emits verifiable IL (issue #4056).
Handler[MyOptions].Field += "z", where the importedHandler<TOptions>declareswhere TOptions : SchemeOptionsandMyOptionsis a class declared in the same compilation, compiled with no diagnostic, was rejected by ILVerify with[UnsatisfiedFieldParentInst] Field parent instantiation has unsatisfied class type parameter constraints, and threwTypeLoadException: GenericArguments[0], 'System.Object', on the open Handler definition violates the constraint of type parameter 'TOptions'— a constraintMyOptionssatisfies. A same-compilation class has no CLR type while binding, so the receiver closes over theSystem.Objectsurrogate and the real construction is recorded beside it; the read spelling and the simple-write spelling both carried that symbol onto the bound node, and the compound-assignment path dropped it, so the surrogate reached emit. Both halves of the compound needed it: repairing the write alone left the read'sldsfldstill parented atHandler<object>and ILVerify stayed red. The affected shapes are wider than the issue's static field — a static property, a non-+=operator such as*=, an unconstrained generic, a same-compilation type NESTED in the argument (Registry[List[Item]]), and an in-scope type-parameter receiver inside a generic function all emitted the surrogate the same way. A receiver over an IMPORTED type argument has a real CLR type, records no symbolic view, and is unchanged. This is wrong emission of a construction the language already accepts, so no diagnostic was added and none was suppressed. -
An object literal of an imported generic over a same-compilation type argument now binds (issue #4042).
Handler[MyOptions]{Tag: "z"}reportedGS0157: Cannot find type Handler. Are you missing an import?— not a message about the literal but the fall-through for a type that could not be named at all — while the constructor spellingHandler[MyOptions]()of the same instantiation compiled, IL-verified and ran. The literal branch was gated on the type argument having a FAITHFUL closed CLR type, and a same-compilation class has no CLR type at all while binding (its TypeDef is produced at emit), so the branch was skipped entirely. That predicate is right about what it says and was being used for the wrong thing: it decides the RESULT TYPE, not whether to enter the branch. The literal now closes over theobjectsurrogate and carries the symbolic vector beside it, exactly as the constructor path has since #671, so the emitter reifies the realHandler<MyOptions>— asserted as the runtime type, not merely as the printed value. An in-scope type parameter works the same way. The three lossy-but-real spellings that the predicate does admit —[]T(#4024),[N]T(#3962) and a named tuple (ADR-0172) — take the same path they always took. Widening the branch does not widen what is accepted:Handler[NotOptions]now reportsGS0152where it used to report the wrongGS0157. A member whose type IS the erased type parameter still reads as the surrogate — on the constructor spelling too, which is pre-existing and unchanged; parity with that spelling is what the issue asked for. -
A member on an element from a dictionary's
.Keys/.Valuesnow binds (issue #4033).for v in m.Valuesfollowed by a property read on the loop element —v.Length,lists.Count,b.Label— reportedGS0158, or the internal-compiler-errorGS9998when the element was a constructed generic, for both themapand theDictionaryspelling, while the same member on the same value reached throughm[k], throughfor k, v in m, or throughTryGetValuebound fine.Dictionary<,>.Valuesis declared[Nullable({1, 0, 0})], and the two zeroes sit at theTKey/TValuepositions: those are type parameters, so the byte is a placeholder for whatever is substituted, not an annotation. Read as concrete positions they made the elementV?, and because G# requires narrowing or?.before a property read on an explicitT?(but not before a method call), the element lost its whole CLR property and field surface. The receiver's own type arguments are now projected through the open property whenever it mentions a type parameter, which is what the openmap[K, V].Keysspelling already did. The issue names a constructed generic as the failing ingredient; measured, a plainstringvalue fails identically,.Keysis affected as much as.Values, a generic key is affected, and so isSortedDictionary[K, V]— while non-nested generic collections such asList,HashSet,Queue,StackandLinkedListwere never affected. -
A
map[K, V?]over a nullable value type emits verifiable IL (issue #4035).map[string, int32?]compiled clean and then threwInvalidProgramException, because its backingDictionarywas closed over the bareint32while the literal, the indexer'soutslot and the loop variable all carried aNullable<int32>— two ILVerifyStackUnexpectederrors in that repro, one or more in every affected shape. A nullable type symbol answers its underlying type's CLR type, since a nullable reference has no distinct runtime type, and the map's construction read that directly instead of theGetEffectiveClrTypecorrection itssequence[T]sibling already used. Every value-position operation was affected — the literal,m[k],m[k] = v,m[k] = nil,TryGetValue,ContainsValue,for k, v in mandfor v in m.Values— and so was the key position,map[int32?, V]andmap[DateTime?, V]alike. Over an imported underlying such asDateTimethe wrapper is now built inside that type's own reflection context, alongside the openDictionary, for the reason issue #4023 documents. TheDictionary[string, int32?]spelling was already correct, so the two spellings of one type disagreed and assigning one to the other failed to verify; they now agree. Nullable reference values such asmap[string, string?]were never affected and are unchanged. -
A DEPENDENT generic constraint is no longer answered after erasure has collapsed two distinct same-compilation classes (issue #4041). Against the imported
Coupled<T, U> where U : IList<T>, the programvar c Coupled[A, List[Bee]]— withAandBeeunrelated classes declared in the same compilation — compiled with no diagnostic and threw at run time:System.TypeLoadException: GenericArguments[1], 'System.Collections.Generic.List`1[P.Bee]',on 'HelperLib3.Coupled`2[T,U]' violates the constraint of type parameter 'U'.A same-compilation class has no CLR type while binding, so it projects to a
System.Objectplaceholder; bothAandBeetherefore arrived asobjectand the vector handed to the constraint check wasCoupled<object, List<object>>, whereList<object>genuinely does satisfyIList<object>. The check answered honestly on what it was given — the information that distinguishesAfromBeewas gone before it was asked — and emission then reified the real types. #4016/#4031'sNormaliseErasedTypeArgsForConstraintCheckis not the culprit and this was verified rather than assumed: it operates on the vector after the collapse, which happens earlier inProjectGenericArgument. The declared bound is now kept UNSUBSTITUTED, and when it mentions another type parameter whose argument arrived erased the question is put to the SYMBOLS instead — the expectedIList[A]is built from the symbolic vector and compared againstList[Bee]'s ownIList[Bee], recursing so a nested bound such asIList<List<T>>is answered at its leaf. The check is three-state on purpose: a definitive no reportsGS0152, a definitive yes accepts, and anything the symbols cannot settle — a concrete position, a missing symbol, a VARIANT parameter (whereIEnumerable[Derived]legitimately satisfiesIEnumerable[Base]), or a BARE dependent bound at the TOP of a constraint (where the relation is assignability rather than identity, soChain[ChBase, ChDerived]must keep binding) — falls back to the pre-existing CLR comparison rather than to a rejection, because a wrong “no” would be aGS0152on a legal program. That last exit is load-bearing and measured: with it disabled and nothing else changed, the legalChain[ChBase, ChDerived]fails with a falseGS0152. It left the imported spelling of a bare dependent bound over two UNRELATED erased classes still accepted, which needs symbolic assignability rather than identity — issue #4063, closed in this same release by asking that position the same predicate the G#-declared spelling of the bound already used. The matched spellingsCoupled[A, List[A]]andCoupled[string, List[string]], the covariantCovariant[CovBase, List[CovDerived]], an open forwarded instantiation, and #4031'sAddScheme[TOptions, THandler]shape all stay green. The asserting row #4032 left pinned for this gap has been moved intoConstraintViolations, not deleted. -
A
map[K, V]whose key or value is an imported type now has its wholeDictionarymember surface (issue #4023).m.Count,m.Clear(),m.Remove(k),m.ContainsKey(k),m.ContainsValue(v),m.Add(k, v),m.TryGetValue(k, out v),m.Keys,m.Valuesandfor k, v in mall reportedGS0158/GS0159/ the internal-compiler-errorGS9998on such a map, while theDictionary[K, V]spelling of the same type — which ADR-0104 says is the same type — kept every one of them. The backing dictionary was built by closing a hosttypeof(Dictionary<,>)over a key or value resolved through the compilation's reference context, which yields aTypeBuilderInstantiationthat answers no members; it is now built inside the key/value's own reflection context instead. The failing shapes were every imported type, not only classes: an imported struct, an imported generic such asList[int32], a same-compilation class extending an imported base, and the key position as well as the value position.m[k]andm[k] = vwere already fixed by #4015 and are unchanged, andlen(m)still reportsGS0566because ADR-0174 (D13) retiredlenas a built-in. -
await for v in snow iterates anasync sequence[T]whose element has no CLR backing (issue #4020). Anasync sequence[T]parameter with an open element reportedGS0134("cannot be iterated withawait for") while the closedasync sequence[int32]spelling iterated and the synchronousfor v in sover an equally opensequence[T]iterated too. The operand check recognised the shape only through its CLR type, which an in-scope type parameter or a same-compilation class element leaves null. A diagnostic naming an async sequence now readsasync sequence[T]rather thansequence[T], so it can no longer be mistaken for the synchronous sequence — whichawait forstill correctly refuses. -
A
nilat a reference-constrained[T class]slot emits verifiable IL (issue #4027). Anilliteral is legal at a[T class]slot — deliberately, since #2354, because such a slot is a reference in every instantiation — but nothing lowered the accepted literal, so emit produced a bareldnullagainst a signature slot typed!!T(or!T). ECMA-335 has no rule admitting a null reference at a generic-parameter slot, constraint or no constraint, so ILVerify rejected it:[StackUnexpected] [found Nullobjref 'NullReference'] [expected value 'T']. The program ran and printed the right answer, which is why it went unnoticed. It is now lowered todefault(T), which the emitter has materialised asldloca; initobj; ldlocsince #774 — the same three opcodescscemits forTakes<T>(null)underwhere T : class, measured. Six sinks were affected, not the one the issue names: a call argument, a local declaration, an assignment, areturn, and — on a generic CLASS, where the slot is!T— a field write and a memberreturn. Nothing about the VALUE changes:default(T)at a[T class]slot is the null reference in every instantiation, so thenilstill arrives and the callee'sx == nilstill observes it. An UNCONSTRAINEDTstill refusesnil(#4010). -
A
map[K, V]whose key or value is an ordinary CLR type no longer crashes the compiler (issue #4015).map[string, DateTime]{},map[string, TimeSpan]{},map[string, List[int32]]{}and every map over a type imported from another assembly reported the internal-compiler-errorGS9998(NotSupportedException: TypeBuilder generic instantiation does not support resolving members). The backing type is built astypeof(Dictionary<,>).MakeGenericType(key, value), and .NET returns aTypeBuilderInstantiation— which refuses to resolve members — whenever a type argument is not itself a host runtime type, as every type the compiler resolved through its reference context is. Only a handful of G# primitives (int32,float64,bool,string,object) escaped it, which is why the shape went unnoticed. The three emit sites that reflected on the closed dictionary — the literal's constructor andset_Item,m[k]'sTryGetValue, andm[k] = v'sset_Item— now fall through to the reified-TypeSpec MemberRefs already used for a map with an open element. The issue reported this as specific to an imported reference type in the value position; it was neither specific to imports nor to reference types nor to the value position. A same-compilation class, aDictionary[K, V]()spelled directly, and the sibling slice and tuple literals were never affected and are unchanged. A separate, pre-existing binding gap on the same types —m.Count,m.Remove(k),m.ContainsKey(k)andfor k, v in mover a map whose value is a class — is tracked as #4023. -
An inferred generic call over a
mapemits a MethodSpec over the real key and value (issue #4026, second face).Probes.CountAnyMap(entries)on amap[string, Derived]— no explicit type arguments, ordinary code — compiled, ran and did NOT IL-verify: the symbolic recovery that decides what the MethodSpec is closed over had no arm for amap, soKandVwere never recovered and the spec fell back to whatever the erasure presented (ImportedBasefor a same-compilation class with an imported base,objectfor one without) while the value pushed was the realDictionary<string, Derived>. Amapis now restated as the constructedDictionary[K, V]it is (ADR-0104) before unification, so it takes the same patterns theListand[]Tspellings always took — which is why those never had the bug — andIDictionary[K, V],IReadOnlyDictionary[K, V]andIEnumerable[KeyValuePair[K, V]]formals are covered with it. -
A same-compilation class that implements an imported interface now satisfies that interface's constraint (issue #4124). For
class D : IDisposable, bothtake[D]()overfunc take[T IDisposable]()andBox[D]()overopen class Box[TB IDisposable]reportedGS0152: Type argument 'D' for type parameter 'T' does not satisfy the 'System.IDisposable' constraint.— a false rejection of ordinary, idiomatic code, which is the worse of this rule's two failure modes.cscaccepts the C# equivalent and so does the CLR.Binder.SatisfiesConstraint's CLR-interface arm bottoms out in a reflective walk overtypeArgument.ClrType.GetInterfaces(), and a same-compilation class has no CLR type until emit, so the walk answered "no" without ever reading the interface list the symbol already holds. This is the FOURTH appearance of that one blindness — #4061 repaired it at the imported class-chain walk, #4068 at the dependent-bound path (whereSourceSymbolImplementsImportedInterfacewas written), and #4092's review round at the G#-declared forwarding rule, each at the site that happened to hit it. Rather than add a fourth caller-side patch, the fallback moves INTO the shared leaf: the reflective body is split out and the symbolic walk now wraps every exit of it, and the two copies sitting at #4068's and #4092's call sites are DELETED. All three symbolic askers now bottom out in one predicate, so a fifth site cannot appear. The widening is monotone in the satisfaction direction and only consulted for an argument with no CLR type, which is exactly what reflection cannot see, so nothing the reflective path already answers changes: a class implementing nothing, a class implementing a different imported interface, and a generic bound with the wrong symbolic argument are all still refused. The source base chain and a G#-declared interface that extends the imported one both count, and a self-referential generic bound now works too — flat ([T IComparable[T]]overclass Cmp : IComparable[Cmp]) and NESTED ([T IComparable[List[T]]]overclass C : IComparable[List[C]]), both red before, on both halves of the predicate, because the argument-for-parameter substitution the two halves share is now recursive rather than a whole-argument identity test. The blast radius named by the issue was measured rather than reasoned about:BoundScope.TryUnifyAndCheckConstraintsgates extension-method candidates through the same predicate, so a constrained generic-receiver extension over a source class went fromGS0159to binding. Exactly one overload winner moves, and it moves TOWARDcsc: with a same-namedDescribe[T IDisposable](T)andDescribe(object),D().Describe()picked the boxingobjectbefore (the constrained candidate was not applicable) and picks the constrained one now, which is whatcscpicks — verified by compiling and running the C# equivalent. It is this change's move rather than #4086's: re-measured against a parent carrying #4108's fix for #4086, the answer there is stillobject, because #4108 fixed inferred type-parameter identity during ranking while this fixes candidate applicability before ranking runs. The rankings that do not move were measured too: two same-name GENERIC extensions still tie (GS0266, both sides), the free-function pair still picks the generic one (both sides), and a receiver that fails the constraint still falls back to the unconstrained sibling. -
An imported generic method constrained to a BASE CLASS now binds when the type argument is a type parameter carrying that bound (issue #4083).
Probes.NeedsScheme[T]()insidefunc callIt[T SchemeOptions]()— overNeedsScheme<T> where T : SchemeOptions— reportedGS0159: Cannot find function NeedsScheme., and so did the same-compilation spellingclass MyScheme : SchemeOptionsand the dependent spellingfunc callIt[U SchemeOptions, T U]().cscaccepts all three. This is #4070's defect one bound-kind over, on the non-interface FALLTHROUGH every base-class constraint of every imported generic method takes: aTypeParameterSymbolargument matched none of the symbolic arms, so the erasedSystem.Objectplaceholder was asked whether it derives fromSchemeOptions, said no, and the candidate was silently FILTERED out of overload resolution — which is why the author saw "cannot find function" rather than a constraint message. The generic-TYPE construction path has walked the parameter's own class-constraint chain since #4037; that walk is now extracted and the generic-METHOD path calls the SAME one. It is deliberately not a call to its parent, whoseobject/ValueTypeearly-out would be a false ACCEPT here, since on this path aSystem.ValueTypebound is the metadata shadow ofwhere T : structand is settled by the special-constraint arms before the type-bound loop runs. The new arm carries #4070's guard — the unsubstituted bound must mention no type parameter — so a CRTP base bound cannot be "proved" on the erased vector, and it is monotone in the satisfaction direction: an unbound parameter and a class bound naming a different base are both still refused. -
A dependent bound now forwards the bounding parameter's interface and base-class constraints (issue #4084).
Probes.NeedsDisposable[TDerived]()insidefunc callIt[TBase DisposableBase, TDerived TBase]()reportedGS0159: Cannot find function NeedsDisposable., while the generic-type spellingclass Forwarded[TBase DisposableBase, TDerived TBase] : DisposableConstrained[TDerived]reportedGS0580— two different wrong answers to one question, on a programcscaccepts, sinceTDerived : TBase : DisposableBase : IDisposableholds in every instantiation. Both askers read onlyClassConstraintand never #4043's separateTypeParameterBoundslot. They now share #4067's existing symbol walk, which follows both slots and terminates on a visited set rather than a depth limit, so one chain answers the question on both paths. That walk's 32-element cap is removed with it: it was belt-and-braces on top of the visited set and it was lossy, so a 40-link dependent chain was truncated and its terminal bound reported unforwarded (GS0159at a method,GS0580at a type — measured). The deduplication already visits each distinct parameter at most once, so the walk terminates on any graph without it. The base-class half at an IMPORTED generic type was still open too and is closed here: #4102 taughtSatisfiesClassConstraintto follow the dependent bound, which covers a G#-DECLARED generic, butclass Fwd[TBase SchemeOptions, TDerived TBase] : SchemeConstrained[TDerived]over an importedSchemeConstrained<T>goes through the reflective path instead and still reportedGS0580. A dependent bound forwards the bounding parameter's constraints and nothing else, so a chain ending in a class that implements a different interface is still refused.
0.4
The fourth pre-1.0 line focuses on sound defaults, expressive control flow, and faithful CLR interop. Collection zero values are now usable without hidden nulls, pattern matching works directly in boolean and loop conditions, rectangular CLR arrays have native syntax, and explicit extension receivers preserve extension semantics for owned types and enums. Tooling adds synchronized-map support, clearer REPL display, and dotnet watch hot reload.
Highlights
- Sound collection zero values. Initializer-less maps, slices, fixed arrays, rectangular arrays, and sequences receive usable empty instances, including collection fields nested through structs. Channels remain explicit: globals and fields require
make(chan T), while locals use definite-assignment analysis and nullable channels use(chan T)?. - Richer conditions and blocks. Boolean
isaccepts the full pattern grammar, including constant, type, property, relational, list, and composed patterns.while letintroduces a narrowed, body-scoped binding that is re-evaluated before each iteration. General block expressions can contain declarations and statements before their trailing value. - Native rectangular arrays.
[,]T,[,,]T, and higher ranks preserve CLR type identity.[rows, columns]Tallocates storage, flat initializers use row-major order, anda[i, j]supports reads, writes, compound assignment, address-taking, null-conditional access, iteration, and interop. - Cleaner extension declarations.
func extension (receiver T) Name()explicitly declares an extension for an owned type or enum without turning it into an instance member. - Migration fidelity.
cs2gsnow emits native block expressions, rectangular arrays, multi-target storage assignment,if let/while letbindings, nativeispattern variables, explicit extensions, and value-position assignments instead of synthetic spill-heavy rewrites.
Added
- Full patterns in
value is patternandvalue !is pattern, with type-plus-property composition and narrowed right-handandpatterns. - Pattern variables in boolean
is(ADR-0166):value is string text && text.Length > 3,value is { Length: > 0 } text,box is { Value: Dog d },values is [1, ..rest], and the guard idiomif !(value is string text) { return }followed by uses oftext. Variables are scoped to the regions where the match is known to have happened;Type namedesignations are also accepted inswitcharms.cs2gspreserves C#ispattern syntax and names for these shapes instead of hoisting__spillNtemporaries. while let name = nullableExpression { ... }, including multiple bindings and normal labeledbreak/continuebehavior.- General value-producing block expressions such as
{ let x = compute(); x + 1 }. - Native rectangular array types, allocation, initialization, indexing, iteration, nullable forms, metadata, and expression-tree support.
- Explicit extension receiver clauses through
func extension. - Map iteration through
for key, value in maporfor entry in map. Gsharp.Extensions.Sync.SyncMap[K,V]for goroutine-safe shared maps with atomicUpdate.- Structural display of plain user values in the REPL while preserving their emitted CLR
ToStringbehavior. dotnet watchhot reload for SDK projects.
Changed
- The
asoperator now has nullable result typeT?; useif let, keep the nullable value, or apply!!when a prior check guarantees success. - The deprecated
name = valuenamed-argument spelling is removed. Usename: value;=in an argument is an ordinary assignment expression. - Multi-target assignment accepts all writable storage forms, including fields, properties, indexed elements, nested members, and pointer dereferences.
- Nil comparison is available for every reference-backed built-in type. Comparisons against bare non-null collection types are diagnosed as statically constant.
- Legacy
print,input, andrndbuilt-ins and thestring(T)conversion are removed; use CLR APIs and explicit formatting/conversion methods. - The website specification, guides, tutorials, feature matrix, diagnostics catalogue, bridges, and tooling pages now describe the complete 0.4 surface without implementation-tracker references outside the design-decision index.
- The Docusaurus site cuts a new
0.4snapshot from the live docs. The version dropdown lists0.4,0.3, andNext.
Fixed
- Generic delegate, enclosing-generic, function-pointer, method-group, map, imported override, XML documentation, channel capture, boxing, and tuple-return emission now preserve the source type and storage semantics across compilation boundaries.
- Null-conditional delegate invocation, nullable-flow analysis, rejected-call diagnostics, interpolation spans, and source-migration output are aligned with the emitted behavior.
Known limitations
- A non-empty rectangular-array initializer requires constant non-negative dimensions and a flat row-major element list.
- Pattern variables introduced by a boolean
ispattern (value is string text, ADR-0166) are read-only and are in scope only where their match is known to have happened; C# forms that rely on full definite-assignment data flow (for example a variable read after awhilewhose exit depends on the pattern) reportGS0532.
0.3
The third pre-1.0 line is a breadth-and-interop release. The language gains a large batch of C#-parity expression and declaration constructs, an unsafe pointer surface, partial types, and anonymous-object literals. Tooling adds the cs2gs C#→G# migration tool and the gsgen Roslyn source-generator host for native G# projects. The language server gains incremental binding, an incremental semantic-model pipeline, and a cross-session cold-start cache. This release also cuts a fresh 0.3 docs snapshot from the live docs and retires the 0.2 snapshot.
Highlights
- Unsafe pointer surface. An
unsafecontext now supports unmanaged raw pointers*T,stackalloc [n]Tproducing either safeSpan[T]or unsafe*Tstorage, thefixedpinning statement, theunmanagedtype-parameter constraint,sizeof(T), and pointer compound-assignment and cast lowering. - New expression and statement forms. Throw expressions, value-producing increment and decrement expressions
++/--, from-end indexes^n, standaloneSystem.Rangevalues such as1..3, expression-bodied members via->, generalgoto/ labels, collection initializersList[T]{…}/HashSet[T]{…}/Dictionary[K,V]{…}, and inferred-type arrow lambdas with statement-block bodies. - New declaration forms.
partialclasses, structs, and interfaces; anonymous-object literalsobject { … }; nested type declarations; user-defined conversion operatorsoperator implicit/operator explicit; user indexer membersprop this[i int32] T { get; set }; theshared { init { … } }static-initializer block; static imports throughimport Ns.Type; and top-levelprivatemapping to ILassembly/ internal. cs2gsC#→G# migration. The new translator lowers C# source to canonical G#, with construct coverage, gap triage, and a build-time strategy that reproduces generated code rather than freezing it. See the new cs2gs tooling page.- Source generators for native G#. The
gsgenhost runs Roslyn analyzers and generators against native G# projects.gsc /analyzer:<asm>spawnsgsgenas a sibling before compiling. A shared resx codebehind generator emitsResources.Designer.gs. - Language-server performance. Incremental binding, instance-keyed semantic-model memoization, a cross-session cold-start cache, completion-as-you-type triggering, and unified member resolution reduce repeated work across binder and LSP flows.
- Diagnostics catalogue extended. v0.3 adds diagnostics through the
GS04xxrange. The Diagnostics reference is reconciled against the compiler source and lists the current per-ID cause and fix detail.
Added
- Null-coalescing operator respelled
??. The null-coalescing operator isa ?? b. The earlier?:spelling is retired in that role;cond ? a : bremains the ternary expression. - Runtime array allocation
[n]T. A length-bearing[n]Tallocates a zero-initialised array at runtime, complementing array literals. - Nullable array-element spelling.
[]T?is an array of nullable elements;[]?Tis a nullable array. - Nullable function-type spelling. Nullable function types are spelled and displayed with the appropriate parenthesisation.
- Expression-tree lambda conversions. A lambda converts to
Expression[TDelegate]where the target demands an expression tree. - Delegate return-type covariance. Delegate return types can be covariant, including lambda target-typing on CLR method calls.
- Predefined type aliases as static-member receivers. Friendly numeric aliases can be used as receivers for static member access.
- Assembly-attribute parity with C#. Assembly-level attributes are accepted with C#-equivalent behavior.
- Collection spreads. Array, slice, and CLR collection initializers accept
...source, preserving lexical evaluation order and applying ordinary element conversions. - Safe structural projections. Compatible public object shapes project into safely constructible concrete targets;
Target{ ...source, Member: override }provides explicit object-spread mapping. - Explicit interface qualifier clauses. Methods, properties, indexers, events, and static interface members can use
(IFace)to provide distinct implementations without source-visible mangled names. - User-defined compound-assignment operators. Classes and structs can declare in-place
operator +=and related one-operand, void-returning instance operators.
Changed
- C#-compatible numeric conversions. Numeric literal narrowing and widening, plus implicit numeric promotion at call sites, now align with C# behavior.
- Unannotated imported reference types are nullable by default. Imported reference types without nullable annotations bind as nullable.
charbitwise and shift operators promote toint32. Enum==/!=comparisons against the integer literal0are also permitted.- The website spec, feature matrix, diagnostics reference, CLR-interop reference, guides, tour, tutorials, and tooling docs were refreshed to match compiler ground truth for the 0.3 surface; a new
cs2gstooling page was added. - The Docusaurus site cuts a new
0.3snapshot from the live docs and retires the0.2snapshot. The version dropdown lists0.3andNext.
Fixed
- Extensive
cs2gstranslator hardening across nullability promotion, extension-call lowering, deconstruction and indexer targets, pattern binding, named-argument lowering, and source-generator-shaped constructs. - Numerous
gscbinder, emitter, and interpreter correctness fixes across imported generic interface methods, nullable value-tuple boxing,data classequality andwith, overload resolution, async lambda inference, and smart-cast narrowing.
Known limitations
gsc --helpadvertises/implicitimports[+|-]; the+/-suffix form is not currently accepted by the Release parser. Use/noimplicitimports.- Migration coverage in
cs2gsis still expanding. C# source generators are reproduced at build time rather than translated, and some constructs remain on the gap-triage backlog.
0.2
The second pre-1.0 line is a syntax-and-ergonomics release. The parser, binder, and emitter absorb substantial additions; several legacy Go-flavored spellings are retired in favour of canonical G# forms; and the native-interop and default-interface-method surfaces ship end-to-end. This release also formally introduces docs versioning: the 0.1 snapshot is removed and a fresh 0.2 snapshot is cut from the live docs.
Highlights
- New language surface.
while/do…whileloops with labeledbreak/continue;if let/guard let/while letsmart-cast bindings; null-coalescing compound assignment??=; null-conditional indexinga?[i]; arrow lambdasx => body; canonical(T1, T2) -> Rfunction-type clauses; lambda binding-type inference; Kotlin/Swift-style type-declaration grammar; if-as-expression completion; smart-cast extensions; discriminated-union enum payloads;default(T)and target-typed baredefault; variadic...Tparameters in function and anonymous function-type clauses;class/struct/init()constraint flag spellings; default-interface methods; reified generics;Gsharp.Extensions.OptionalandGsharp.Extensions.Sequences; friendly numeric type aliases; and the canonical map type clausemap[K,V]. - Removals and migrations. Legacy spellings now produce focused, span-accurate diagnostics with canonical replacements so IDE quick-fixes can patch most migrations in one edit.
typekeyword for type declarations (type Foo struct { … }→struct Foo { … }).recordkeyword, replaced bydata classordata struct.:=short variable declaration, replaced bylet/var, with diagnosticGS0305.name = valuenamed-argument separator, replaced byname: value, with diagnosticGS0315.func(T) Rlegacy function-type clause, replaced by(T) -> R, with diagnosticGS0303.- Go-flavored
map[K]Vtype clause, replaced bymap[K,V], with diagnosticGS0366. static funcon interface methods is removed. Static-virtual interface members now live inside the interfaceshared { … }block. A body-lessfuncinside that block is an abstract static-virtual slot; afuncwith a body is the default. Static private helpers also move into theshared { … }block asprivate func, while instance private helpers stay directly in the interface body. The oldstatic func …shape now produces a parser error, andGS0330fires when a non-funcmember appears inside an interfaceshared { … }block.
- Body-less
funcnow requires;. Afuncdeclaration without a{ … }block is terminated by the universal no-body marker;. This already held for P/Invoke (func getpid() int32;) and now also applies to abstract interface methods and abstract static-virtual slots inside an interfaceshared { … }block. A body-less interfacefuncmissing its;reportsGS0368; afunccarrying a body still takes no;. - Native interop end-to-end. P/Invoke via
@DllImport, source-generator-shaped@LibraryImport, struct and class marshalling,ref/out/inparameter marshalling, function-pointer marshalling, and@MarshalAsparameter overrides are supported. - Go-flavored concurrency moved behind an opt-in import.
go,chan,select, channel send and receive,make(chan T), and the built-inslen,cap,append,make, anddeletenow requireimport Gsharp.Extensions.Go. DiagnosticsGS0316/GS0317point at the missing import. - Tooling polish. LSP completion understands async-shaped types such as
async (T) -> Randasync sequence[T];textDocument/codeActionoffers nil-related quick fixes; andnullnow produces anil"did you mean" diagnosticGS0273. - Diagnostics catalogue extended. v0.2 introduces
GS0273andGS0288–GS0366. The Diagnostics reference has the per-ID cause and fix detail.
Added
- Friendly numeric type aliases.
int,uint,long,ulong,short,ushort,byte,sbyte,float, anddoubleare accepted everywhere a type name is accepted, as a strict superset on top of the canonical width-bearing names (int32,uint32, …). The alias resolves to the canonicalTypeSymbolat the binder, so diagnostics,typeof,nameof, hover, and emitted IL always print the canonical spelling. Canonical names remain preferred in documentation and public library APIs; aliases are appropriate inside function bodies and local code. Aliases are reserved type names, sotype int = stringand equivalents are rejected withGS0102. - Null-conditional indexing
a?[i].a?[i]evaluates receiveraexactly once. If it isnil, the whole expression yieldsnil; otherwise the result is the indexed value lifted to the nullable form of the indexer's return type. It works on arrays, slices, maps, and CLR indexers on both emit and interpreter paths. Chained forms (h?.Data?[i]?.c) short-circuit on the first nil. The new token?[is recognized only when[immediately follows?, preservingcond ? [arr] : [arr]ternary parses. DiagnosticsGS0300andGS0301cover non-nullable receivers and assignment left-hand sides. - Documentation comments. Markdown-authored
///documentation comments round-trip losslessly to CLR XML doc. Hover renders merged documentation for both G# declarations and imported CLR APIs. New warnings includeGS0227,GS0228,GS0229,GS0230, andGS0231. - Named delegate types.
delegate Name(...)declares a real CLRMulticastDelegate-derived type so C# consumers see a conventional handler type and G# events can carry first-class custom delegate types. DiagnosticsGS0233–GS0234cover invalid forms.; ref/out/inparameters. Declaration-site and call-site ref-kind modifiers are supported, including inlineout var/out let/out _declarations. DiagnosticsGS0235–GS0243cover the rules. Passing a value to aninparameter without writinginat the call site is warningGS0242rather than a silent spill.- Ref-aliasing locals.
let ref m = arr[i]andvar ref v = c.Fieldproduce locals whose IL slots areT&and alias another lvalue. DiagnosticsGS0256–GS0258cover invalid aliases. - Ref returns.
func f(...) ref T { return ref <expr> }is supported. DiagnosticsGS0248–GS0255cover escape rules, async and iterator bans, and override matching. - Conditional ref-arguments. The narrow
ref cond ? a : bform is supported inside ref-kind argument payloads. DiagnosticsGS0260–GS0262cover invalid forms. - Generalized ternary expression.
cond ? a : bis now a normal expression.GS0259is retired in value contexts;GS0263covers "no common type" failures. - Method overloading and optional parameters. User G# functions can carry overload sets that differ by parameter types or ref-kinds, and optional parameters can use compile-time-constant defaults. Diagnostics
GS0264–GS0267cover invalid overloads and defaults. - Named arguments at call sites.
Foo(timeout: 30, retries: 3)works for free functions, user methods, user constructors, extension functions, inherited CLR methods, and delegateInvoke. DiagnosticsGS0244–GS0247cover invalid usage. The legacyname = valueform is deprecated this release with diagnosticGS0315; migrate.copy(...)and attribute argument lists alongside ordinary call sites. scopedparameter modifier.scopedconstrains aref structor managed-pointer parameter from escaping, enforced byGS9004/GS9006.data structsynthesis completed. Everydata structsynthesizesEquals(object),Equals(T),GetHashCode(),ToString(),op_Equality,op_Inequality, andDeconstruct(...). Hand-written versions are rejected withGS0232.- Editor features. Hover for CLR XML docs, live pull-based diagnostics, CodeLens reference counts on members of structs, interfaces, and enums, implicit
thisfor properties, methods, and events, hover forthis, bare static-member access from instance methods, chained-member hover, and six VS Code color themes inspired by the G# logo: Ember, Magma, and Synthwave in dark and light variants. :=short variable declaration removed. Every binding site now requiresletfor immutable bindings orvarfor mutable bindings. The lexer still recognizes:=so the parser can emitGS0305with context-sensitive migration suggestions such asx := 1→let x = 1,for i := 0 ... 10→for i in 0 ... 10, andcase v := <-ch→case let v = <-ch.
Changed
- The website spec, feature matrix, FAQ, bridges page, guide pages, and design-decisions index were refreshed to match compiler ground truth. Outdated statements such as "Parameters do not have default-value syntax" and "Named arguments — Partial" were rewritten.
- The repo
docs/lexical.mdblock-comment paragraph is corrected, and a documentation-comments subsection was added. - The VS Code TextMate grammar adds contextual keywords (
data,inline,record,delegate,event,prop,init,shared,scoped, accessor namesget/set/add/remove/raise, and ref-kindsref/out), operators (:=,?.,??,?/:,!!,...,=>), an@Annotationscope, and a///documentation-comment scope with@taghighlighting. The VS Code snippet set was rewritten to match current grammar. - The Docusaurus site cuts a new
0.2snapshot from the live docs and retires the0.1snapshot. The version dropdown lists0.2andNext; the0.1URL space is no longer served.
Fixed
- Numerous IL-emit, determinism, language-server, and editor hardening rounds improve CLR verification, byte-for-byte reproducibility, property access, CodeLens accuracy, and stale-tree handling.
Known limitations
- Full ref-safe-to-escape analysis is partial;
GS0257is reserved for a future pass. - Unsupported async state-machine emit shapes continue to report
GS0190.
0.1
The 0.1 version base identifies the first pre-1.0 line. This is not a dated stable release announcement; it summarizes the major capabilities implemented in the repository at that point.
Language and libraries
- Packages, imports, import aliases, top-level declarations, and multi-file or multi-package compilation.
- Width-bearing primitive names such as
int32,uint64,float32, andfloat64, plusbool,char,string,object,decimal,nint,nuint, andvoid. - Nullable
T?types withnil,?.,??, and!!. - Structs, classes, interfaces, enums,
data struct,recordas adata structalias, andinline structvalue wrappers. - Generic functions and types with square-bracket type parameters and arguments, constraints, method inference, and CLR variance support where applicable.
- Fixed arrays, slices, maps, tuples, function values, delegates,
sequence[T],async sequence[T], and iteratoryieldsupport. - Control flow including
if,for,for inorrangeforms, switches, switch expressions,try,catch,finally,throw,using, anddefer. - Go-shaped concurrency with
go,scope, channels, channel send and receive,make(chan T),close, andselect. async func,await, async lambdas, awaitable-shape support, andawait forover async sequences.- CLR interop for imported constructors, methods, overload resolution, fields, properties, indexers, events, delegates, extension methods, optional CLR arguments, operators, conversions, attributes, and generic types.
Tooling
gsccompiler driver with immediate execution when no/out:is supplied and saved managed executables or libraries with/out:.- Managed PE and metadata emission without Roslyn, optional reference assemblies, target-framework-aware reference resolution, runtime configuration output, and Portable PDB support.
- MSBuild SDK support through
Gsharp.NET.Sdk,.gsprojprojects,dotnet build,dotnet run, templates, and SDK-side response-file invocation. - VS Code extension and language server support for diagnostics, hover, definitions, references, symbols, formatting, completions, signature help, rename, code actions, CodeLens, semantic tokens, inlay hints, and debugging integration.
- Stable diagnostic IDs in the
GS####form, with warning suppression and warning-as-error controls.
Pre-1.0 notes
- The language is still evolving; source compatibility may change before a stable release.
- Some surfaces are intentionally documented as current implementation behavior rather than final specification guarantees.
- The Playground page exists, but browser-hosted execution is deferred.
Future release-note format
Use reverse chronological order. Each version entry should identify the version and, when a real release process exists, its date. Do not invent dates or version numbers; derive versions from the repository's release process. Write for end users: describe what changed, what it means, and any migration steps they should take.
## X.Y.Z
Short summary of the release.
### Added
- New language, tooling, documentation, or interop capabilities users can try.
### Changed
- Behavior changes, breaking changes, renamed features, or migration notes.
### Fixed
- Short grouped quality notes for user-visible correctness, diagnostics, emit, interpreter, or tooling improvements.
### Known limitations
- Important limitations users should know before upgrading.