Repository navigation
🐛 fix: a collection and an indexer answer as .NET's do - #707
Merged
edgarmesquita merged 78 commits intoOct 11, 2026
Merged
Conversation
…wner A nested class had no module, resolved to a top-level class of its name, and a nested record or struct wrote a module a top-level one of its name writes too (#584). Each nested class, record and struct gets a twin named by its owner and itself, joined by $ (Cart$Item), which every reference names.
A class declared inside another one had no module, so constructing it named nothing, and where a top-level class had its name it resolved to that one (#584); a nested record wrote the module a top-level record of its name writes, which the build refused (EQ2009); a nested static class outside a component had no module, and one inside a component was its module's alone. Every class, static class, record and struct declared inside another type is now a module named by the types that contain it and its own name joined by `$` (`Cart$Item`, `A$B$C`), a name no C# type can take: TwinTypeName, from a symbol or a declaration, is what the parser, the dependency resolver, the emitters and every strategy that writes a type (a construction, a type test, a static member, an operator, a zero, an annotation, a hydration map) read, and a reference imports the twin as it imports any type's. One inside a server-only class, an exception or an attribute has none, as its owner has none. A record's text keeps its C# name. The runtime's one nested type regenerates as `CodeBlock$CodeMetrics`, its annotations typed, and the component identity test reads a `$` in a twin's name rather than taking the owner's. The 18 conformance cases through the module graph pass in both annotation modes, where on the class branch all of them failed (EQ2009 for the record pair) or one of the component's two threw. Compiler 1615, Server 220, Web 941, Conformance 3739 and the runtime's 2050 are green, and the dashboard sample's markdown, code and diff screens render their code blocks with no console error.
…ports the name it calls The review of this branch found the server on the other side of the rule. - The server named the page it serves by the CLR simple name, in the route table and in the shell's configuration, while eqc names a nested component's module by its owner (`Admin$Users`), so a page declared inside a class asked for a module nothing writes. One rule, `TwinName.OfType`, in the file the compiler and the server already share, names both, and the server's tests read their own nested pages by it. - A static method reached bare and a conversion operator registered a runtime type's import by its simple name and called it by its twin, so a nested runtime type was imported under one name and called by another. A reference writes one name now, `TwinReference`: a nested type's twin where eqc writes it, and the simple name a top-level type and a hand-written runtime twin keep. - The identity check over the shared library's twins keys them by that name, a nested component included, where it skipped every name with a `$`. - The `[ServerOnly]` check has one home, and `SymbolOf` has its doc comment back.
Main brought #618 (the code engine's completion), #628 (a target's 24dp floor) and #607 (a char's search). Two files conflicted where main moved what an annotation crosses as into TsStandIn: a member's type (TypeDeclarationExtensions) and a local's (LocalDeclarationStrategy). Each now asks the stand-in first, then names a nested type by its twin, as this branch did, with main's OrNull for a nullable one. The ledger line moves after the entries main brought.
A list's construction is one array, as C# builds it: what the constructor copies, spread, then each element of the initializer in order. A capacity with an initializer dropped the elements, a source with one was joined to them by a comma (`source, [3]`), and the target-typed form kept the elements and dropped the source, or built an empty list from a source with no initializer at all (#564).
ToDictionary, ToLookup, GroupBy and Distinct hand their comparer to the fence every collection's construction passes (EQ2007): one that asks for the key type's own equality, ordinal strings or null is dropped, since the lowering already finds the keys that way, and any other is refused. ToDictionary refused every comparer, StringComparer.Ordinal included (EQ1004), GroupBy refused every one too (EQ2008), ToLookup called a comparer as an element selector or had no form after one, and Distinct dropped whatever it was handed (#578).
A call bound to ICollection<T>'s own Add or Clear goes through the runtime, which asks the value what it is, as Contains, Remove and CopyTo already did: an array appends or empties in place, a set adds a value it lacks, a linked list adds last, a dictionary adds the pair and refuses a key already there in its own words, and a twin calls its own add and clear. They were an array's push and splice, which a HashSet, a LinkedList, a SortedSet and a dictionary's pairs lack, and the same push was what a member's collection initializer added through (#593). The runtime's count reads a twin's own count before walking it, so a twin of the app's own behind the interface counts itself.
An access through the indexer of IList<T>, IReadOnlyList<T> or IList is a Place of its own kind, read and written by the runtime's $eq.collections.item and setItem, which answer an array's subscript and a twin's item and setItem alike, whichever the face holds when it runs. Every writer takes it: the assignment, the compound, the step, the coalescing write, the null-conditional read, an object initializer's entry and a key from the end, which counts through $eq.collections.count. A Count read through the face counts through the runtime too, and the runtime's count reads a twin's own count. A receiver typed as an array or a list keeps its subscript. A twin read through the face read a JavaScript subscript and a length, which a twin does not answer: `r[0] + r[2] + r.Count` over a twin was null where .NET says 19 (#586).
A range used as an index reads the member the bound tree names. Over a type eqc writes, with a Length or a Count and a Slice(start, length), it calls that Slice as C# lowers the range: the receiver once, then the endpoints in the order they are written, then the count, read only where an endpoint counts from the end or the end is open, as measured on .NET, then Slice(start, end - start). It called JavaScript's slice(start, end) on the twin, whose slice is that Slice, so `new Strip()[1..3]` held three elements where .NET holds two, and `[3..1]` sliced where Slice(3, -2) throws (#585). A range handed to an indexer that takes the Range itself is refused with EQ2004, since a System.Range value has no translation. A range over a string, an array or a list keeps JavaScript's slice. The strategy moves to the IR, and leaves the migration baseline.
The fence's own tests hold the operators that now pass through it (#578): ToDictionary, ToLookup, GroupBy and Distinct refuse a comparer that changes equality with EQ2007 and nothing else, and build with no error at all from one that asks for the default.
…tion The comparer ToDictionary and ToLookup drop was matched to the written argument by its node, and a null-conditional rebuilds the call over its receiver with copies of its arguments, so `words?.ToDictionary(k, EqualityComparer<int>.Default)` passed the comparer as an element selector, which named a class nothing defines. It is matched by its position among the arguments the bound tree names, as the indexer's place finds its keys (#578).
…nswers The shared components, charts and code engine the runtime transpiles regenerate (EQ_UPDATE_TRANSPILED=1): an element or a count read through IReadOnlyList<T> or IList<T> goes through $eq.collections.item and count, so a twin of the app's own handed to one of them is read through its indexer and its count, where a subscript and a length read undefined (#586). An array behind the face reads as it did.
… component is its twin Copilot's first round on #654. Every site that writes a twin into the output names it through IntroduceTwin, which registers the import with the name: an operator, a unary operator, a compound assignment and a tuple equality on a nested type no expression names, a conversion, the zero of a nested struct an alias names, a type test, and every construction, the target-typed `new()` with no initializer included. A component is known by its twin's name in the parser and the resolver, so a top-level component and a nested plain class of its simple name are never taken for each other, and a nested component crosses where its owner does. The archived delta carries the server half the main spec already said. New cases: a nested component built and tested as Host$Page, a component beside a nested class of its name, and operators, a conversion and an alias's zero reached by inference.
…otation names every nested twin Client code that builds a nested type of a [ServerOnly] owner, an exception or an attribute, tests for it or reads its statics is refused with EQ2010, where the page met a twin nothing wrote at load. The server publishes no route and maps no page for a page declared in such an owner, by the rule the compiler writes modules by (TwinName.OwnersCross). A nested type inside an array or a generic is annotated by its twin in a class and in a record, each name where its type stands, so two owners' Inner name two twins. The record path compares against the normalised spelling, as the class emitter does, so a qualified nested member reaches its twin too.
The runtime's item and setItem took the list as unknown, so in the twins TypeScript checks every element read through IReadOnlyList<T> or IList<T> was unknown and tsc refused them (#586). They take an array-like or a twin of the element type, which the twin's own annotation names, and fall back to any where the receiver is untyped, as mapGet's value does.
…e reaches it, and one the runtime provides is the runtime's Copilot's third review of #654 found client code that reached a nested type of a [ServerOnly] owner, an exception or an attribute without meeting EQ2010, and each path wrote a twin nothing declares or a zero that was undefined: an alias, a static member or a call through using static, an operator or an implicit conversion its value brings, default(), an array's fill and a field's or a record member's zero. Each now reports EQ2010 where the code names the type. The node a fence reports at spans the settling, where an implicit conversion is written, and an emitter passes the declaration whose zero it asks for. Under the fences sits a net: the import every module makes. A twin the build writes no module for, by the resolver's own scan, is reported there rather than dropped, unless a fence already said it, so a path no fence sees, a field initialized to null, is refused too. A nested type of an owner the runtime provides is the runtime's, as the parser reads it, and the runtime carries it under its owner's twin name: it is imported from @equantic/runtime, where it named a sibling module nothing writes. The attribute is read by its name as written too, as [ServerOnly] is. MapPage refuses a page declared inside an owner that never crosses, at startup, as it refuses a type that is not a component. The same diagnostic at the same place is reported once.
… own words The test read only the code, and EQ2004 is every untranslated member's, so any other refusal that reached the same access passed it. It reads the message RangeExpressionStrategy writes for a range handed to an indexer over System.Range (#585).
The four twins the merges left in conflict (AppBar, BottomNavigation, CodeDiffLayout, NavigationRail), written again with EQ_UPDATE_TRANSPILED=1: main's parameter names and null-conditional answer beside this branch's reads through a list's face, and nothing else moved.
The runtime's count read a size or a length before a count, so a twin of the app's own with a Length beside its Count, a polyline's, counted by its length through IReadOnlyList<T>: `p.Count` was 12.5 where .NET says 2, and `p[^1]` and a loop to the count read past the end (#586). A count is asked first, after an array and a string, which the runtime's own collections never hold beside a size or a length.
…ki pages The list face's requirement gains the scenario of a twin with a Length and a Size beside its Count (#586), and the documentation task names the wiki pages that change with it, in English and Portuguese.
…'s face The record moves from 201,883 to 203,748 gzipped bytes. 629 of them are this branch's, measured against main's own bundle (203,119): the twins of the shared components, the charts and the code engine read 99 elements and 106 more counts through a list's face, by $eq.collections.item and count, where a subscript and a length read undefined over a twin of the app's own (#586), and the runtime gains add, clear, item and setItem (#586, #593). The other 1,236 are main's, which grew inside the 1% the record allows without writing it.
…'s specs openspec archive moves the change under openspec/changes/archive/ and writes its requirements into transpiler-bcl (a list built with an argument and an initializer, a list's face, ICollection's Add and Clear), transpiler-sequences (a LINQ operator's comparer) and transpiler-expressions (a range over a type with a Slice).
…ollection's Add Three pins still held what this branch replaced. ToDictionary with a null comparer was refused (EQ1004), where it is now the call written without it (#578), and the test compares the two conversions. An Add on IList<T>, which C# binds to ICollection<T>'s, was the array's push, where it now reaches the runtime's add, which pushes onto an array and adds to whatever else stands behind the face (#593). And GroupBy no longer reports EQ2008, so the diagnostics baseline drops its site.
…er the third round and main's merge EQ2010 is reported by the record emitter too, the module's import net under the fences, with the same words the fences use. The served runtime grows 1,231 gzipped bytes over main's (201,883 to 203,114), the twins' nested names and their text included.
…arrow The arrow the last fix wrote around the comparison is a function the lowering writes by hand, which the introduced-functions list refuses. The comparison says it instead: every CompareTo(object) reads its receiver twice, so the writer binds a receiver with an effect and runs it before the null is read. A number takes the ordered three-way there, and a char answers a null as the code unit before its own, which keeps its subtraction and makes it 1. A double's and a long's receiver join the cases.
218,583 gzipped bytes, 183 more than the 218,400 the head of #752 serves.
…-as-dotnet-does Brings fix/a-nested-class-is-its-owners at edf747a, which holds #654, #715, #752 and main at d7f61c0 (#741), the pull requests this one merges after. No source file conflicted: #715's sequences named by their parameter and #654's twin names sit beside this branch's list, comparer, range and face lowerings as they are. The served runtime's record is the base's, regenerated next with the twins, and the ledger line moves to the end of the events.
One gap of #715's baseline closes with this branch, 402 entries to 401: ToLookup(source, keySelector, comparer) handed a null comparer built its lookup in .NET and called the comparer as an element selector in the browser. The comparer is dropped by the collection fence now (#578), so the probe agrees and its entry leaves. No entry is new.
…t gap that closes Against fix/a-nested-class-is-its-owners at edf747a, what main is when this merges, 97 of the 115 new conformance cases and 14 of the 16 new Compiler cases fail, the served runtime grows 686 gzipped bytes, and ToLookup's null comparer leaves the null-argument baseline.
…time-provided owner Found by Copilot's review of #707, in this pull request's code. MapPage refuses a page whose owner TwinName.OwnersCross refuses, and its message listed [ServerOnly], an exception and an attribute as types that never reach the browser. A [RuntimeProvided] owner is refused too, and it does reach the browser, inside the runtime. The message names the four and says what they share: the build writes no module for them.
Found by Copilot's review of #707. The comment said an unmarked copy is not taken for another type, above an assertion that it compares as the marked one: an unmarked value compares as any closed type, and only two marked differently differ.
edgarmesquita
added a commit
that referenced
this pull request
Oct 11, 2026
…he translated surface (#715) Closes #569 ## What One measurement instead of one fix per report. `NullArgumentConformanceTests` calls every member of the BCL surface eqc translates with a null for each of its reference parameters, on .NET and in the browser, and compares what the call did: whether it threw and the exception's type, its `ParamName`, its message where the SDK composes it, and the value a call that returned answered, a string, a bool, a char, a number or an array of those. - The surface is derived, never listed. Its types are the ones the BCL audit's committed record names (`bcl-surface.baseline.txt`), its members are what reflection finds on them, constructors, indexers and generic methods included, and eqc's own diagnostics say which of them it translates. A guard fails where a member the audit grades `native` or `eq` takes a reference parameter and no null reaches it on both sides. - Each probe runs a control, the same call with the null replaced by a canonical value. The canonical text holds white space around and inside it, which is what a null separator or a null set of trimmed characters stands for in .NET. Where .NET answers the control and the browser answers it otherwise, the member differs whatever the argument, and the gap is recorded as `control` rather than charged to the null. A probe whose other arguments .NET refuses on their own measures those arguments, and is counted rather than compared. - The gaps left are `null-argument-gaps.baseline.txt`, which may only shrink: a new gap fails, an entry whose aspect moves fails, and an entry that starts to agree fails until it is removed. Each entry carries its aspect and a reason, and the file is regenerated with `EQ_UPDATE_NULL_ARGUMENT_BASELINE=1`. - The twins that read a null through null refuse it by name now, as .NET does: LINQ's `Max`, `Min` and `ToDictionary`, a sequence a LINQ operator reads through `seq`, named by the parameter it binds to (`second`, `inner`, a `Zip`'s `first`) where every one was `source`, `CompareTo(object)`, which answers 1 for a null, `new Guid(text)`, which names its text `g`, `char.GetUnicodeCategory(text, i)`, and the cancellation pair's `Register` and `CreateLinkedTokenSource`. For someone using the SDK: a null handed to one of those members is caught by `catch (ArgumentNullException e)`, and `e.ParamName` names .NET's parameter. Nothing an app writes changes. The public surface gains `Eq.GuidOf`, the developer surface does not move, and the transpiled twins that concatenate sequences are regenerated: their `seq` calls name `first` and `second`. ## Why A .NET method that cannot take a null refuses it with an `ArgumentNullException` naming the parameter. Several twins read the argument first, so the browser threw a `TypeError`, which the runtime reads as a `NullReferenceException`, and since #561 a typed catch lets that through. #561 fixed the seven its review named, by hand, and the rest of the surface had never been looked at. What the measurement leaves, 402 probes, each with its reason: - 213 are members lowered to JavaScript's own method: LINQ over a list (168), an array's or a list's method, a string's search, a spread. They read a null through null, and refusing it by name means a guard at every call. That is a design decision, so I left it out of this pull request. - 84 are provider overloads that eqc writes with the name `CultureInfo`, which the runtime does not export, so the call is a ReferenceError in the browser and nothing fences it at build time. eqc reads a culture only where it formats or parses a number, and a culture held in a local fails the same way. - 26 are `Convert` of an object or of a string (#401), 21 are members the runtime does not have (most of the sorted set's set operations, `CopyTo` on the queue, the stack and the sorted set, and `TryParse` of a `DateTime`, a `DateTimeOffset`, a `TimeOnly` and a `TimeSpan`), 6 are collection constructors whose twin reads a null as no collection, and 3 drop a culture or a calendar argument. - The other 49 are lowerings that are wrong whatever the argument, found on the way: `Convert.ToBase64String` and `ToHexString` answer `String()` of the array, `FirstOrDefault(defaultValue)` lowers the default as a predicate, the `Zip` of three sequences lowers as the one with a selector, `SequenceEqual` with a comparer answers its first sequence, `ToLookup` with a comparer reads it as an element selector, `Average` over longs adds a BigInt to a number, `LastIndexOf(value, start, count)` ignores its count and `LastIndexOf(value, start)` reads its start as JavaScript does (#724), six `String.Split` overloads hand their separators to JavaScript's `split` whole (#753), `new decimal(bits)` and `new Guid(bytes)` name classes the runtime does not export. The list was 443 when this pull request opened. Main moved under it since: #748 gave the builder its members, which took the 22 probes of #679 out and left two whole-array `AppendFormat` ones among the lowerings, and #660 refuses a constructor that takes a calendar at build time, 8 probes. Some cheap fixes stay in the list on purpose, because their files are in open pull requests: the collection constructors and the `Contains` and `Zip` twins (#707), `string.Format` and `string.Concat` with a whole array (#637), and the `ToHashSet` and `ToDictionary` shapes (#707). ## OpenSpec `a-null-argument-is-refused-as-dotnet-refuses-it`, archived, adds a requirement to `transpiler-exceptions`: a null argument is answered as .NET answers it, measured over the translated surface. ## Proof The surface is 30 types and 823 members, 1205 probes, each a member and the parameter handed null: 874 are compared on their null, 32 are counted because .NET refuses their other arguments, 293 are members eqc refuses at build time, 6 bind another overload as written and are left out, and 250 need an argument no canonical value can write. Every one of the 256 lines the audit grades `native` or `eq` that take a reference parameter is reached. A/B against main (7636da4), with main's version of every source file this changes, built with 0 errors: `NullArgumentConformanceTests` fails 17 of its 26 tests there. The theory fails with exactly the 67 probes the fixes close, and 13 of the 14 cases one canonical value cannot reach fail too (a negative or false receiver of `CompareTo(object)`, a set handed to `Concat`, a sequence as a `Zip`'s receiver, a null selector caught by a typed catch). And 3 of the 7 cases of a receiver with an effect fail there for the answer itself, a double's, a char's and a bool's. The 14th is the control that a value still compares, and the four checks of the instrument pass on both sides. On this branch, 26 of 26 pass. The fixes took the baseline from 510 entries to 443, and it holds 402 now, for what main and the wider measurement moved since. Copilot's first round found two. A receiver of `CompareTo(object)` that its comparison reads once never ran when the argument was a null literal, the null test answering before it reached the receiver. Every `CompareTo(object)` reads its receiver twice now, which makes the writer bind a receiver with an effect and run it first. With the strategy as it was before that round, 3 of the 7 cases of `TheReceiverOfCompareToObject_RunsBeforeItsNullIsRead` fail, an int's, a double's and a char's receiver, and here the 7 pass. And the measurement compared no array and its canonical text held no white space, so a split by null passed whatever the browser made of it. It compares an array of strings, chars, bools or numbers now, over a text that holds white space, and that is what found the six `String.Split` probes (#753) and moved `LastIndexOf(value, start)` to `control` (#724). Each suite alone, read by its exit code and its total, on this branch's head, a tree the merge of main 7636da4 left as it was: Compiler 1841, Web 1021 against this branch's wiki page, Server 367, the runtime's 2316 in 170 files (`tsc` and vitest through the embedded bun), and Conformance 4731 with 1 skipped. No run aborted. The served runtime is 218,583 gzipped bytes, 183 more than the 218,400 main serves, measured the same way. ## Checklist - [x] `dotnet test` on the affected test projects, and `dotnet build src/eQuantic.UI.Runtime -t:TestRuntime` - [ ] This PR's checks include a run named CI with jobs in it - [x] `./scripts/check-openspec.sh` passes: the change validates strictly and is archived - [x] `dotnet build samples/DefaultUIDashboard` - [x] A broken contract has a line in the migration notes: none is broken - [x] The wiki's SupportedFeatures page in English and Portuguese, on the wiki branch `fix/a-null-argument-is-refused-as-dotnet-refuses-it` - [x] One `docs/LEDGER.md` line for this event, citing #569 - [x] The diff reviewed by its author before the PR opened, and Copilot's first round answered: two findings, both fixed - [x] No "widget" in prose --------- Co-authored-by: Edgar Mesquita <edgar@equantic.tech>
edgarmesquita
pushed a commit
that referenced
this pull request
Oct 11, 2026
…as-dotnet-does The head of the queue, f03bcb6, holds #707, #654, #715, #752 and main at d7f61c0. Six files conflicted, and each keeps both sides. - IdentifierStrategy names a static's home by its twin (#654) and a method by the name it holds on its twin (#563). - InvocationStrategy calls a static on its home's twin (#654) by the name the method holds there (#563). - ObjectCreationStrategy builds a type by its twin's name (#654), with the types a generic exception class is built with (#611), inside the closing #689 puts around a generic record's. - The transpiler-classes, transpiler-exceptions and transpiler-sequences specs keep the requirements of both sides, the queue's first. - PlainClassModule gets back the using of ExceptionTypes the merge dropped, which #654's OwnerKeptOut reads. - The ledger line moves back to the end of the events. #654 keeps a type nested in an exception out, as an exception had no module before #611. The next commit settles what the two say together.
edgarmesquita
pushed a commit
that referenced
this pull request
Oct 11, 2026
#715 landed on main as 7d3018c, a squash of a head this branch already holds through the queue's. Recorded with ours, so the tree does not move: a real merge changes three files only, and each is already at the queue's later state here. The null-argument baseline is without ToLookup's null comparer, which #707 closes, the served runtime's record is this branch's, and the exceptions spec holds main's requirement and this change's.
8 of 9 tasks
edgarmesquita
added a commit
that referenced
this pull request
Oct 11, 2026
… does (#708) Closes #563 Closes #611 Closes #612 #563 is a sub-issue of #164 (the transpiler's fences hold on every path), and #611 and #612 are sub-issues of #565 (the same, continued). ## What A method that hides an inherited one (#563): - A method declared with `new`, or with the same signature and no `override`, holds a name of its own on its twin, `name$1`, and `name$2` for one that hides that one. Every call, method group, base call, null-conditional call, initializer's `Add` and `Deconstruct` the model binds to it reaches it by that name, so a call through the base type reaches the base's method, as .NET's does. An override of a `new virtual` method fills that method's slot. - A member the runtime reads by its .NET name is answered by .NET's: `((object)c).GetHashCode()` over a `public new int GetHashCode()` is object's. - EQ1007 compares the names each method holds on the twin, so a method that hides one is no longer refused. One that hides one and answers an interface is, since a call through the interface reaches the hidden member's name. - A method beside an explicit interface implementation of its name holds a name of its own too, so a call through the class reaches it and one through the interface the explicit one. They were written under one name, the later replacing the other. One that also answers another interface is refused (EQ1007). A class that implements `IEnumerable<T>` (#612): - Each consumer walks the `GetEnumerator()` that C# binds for it, through `$eq.linq.iterate`: a `foreach` over the class its own public one (a struct enumerator's included), and LINQ, a spread, `string.Join` and a `foreach` over the interface the `IEnumerable<T>` one, explicit or not, which the twin's `[Symbol.iterator]` calls. So the two walk different sequences where C# lets them. An enumerator the app wrote is walked by its `MoveNext` and `Current` and disposed when the walk ends, a class that is its own enumerator included, and a class with only a public `GetEnumerator()` is walked by `foreach` too. - The non-generic `GetEnumerator()` beside the generic one, and an explicit `Current` beside a public one, are not written. Each replaced that member and called itself. - A twin has one iteration and one `Current`, so what it cannot answer as C# does fails the build (EQ1007): a type that implements `IEnumerable<T>`, or `IEnumerator<T>`, over two types, and a non-generic `GetEnumerator()` or a `Current` that does not hand back what the kept member answers. The usual `=> GetEnumerator()` and `object IEnumerator.Current => Current` hand it back, and build. An exception class of the app's (#611): - It is a class, with a module and a twin of its own, over the runtime's `$eq.exceptions.Exception`: the browser's `Error`, carrying the class's .NET types, which a typed `catch`, a type pattern and an `as` read as before. Its fields, properties, constructors and methods are its own. - Its base call hands what a `new` of its .NET base hands since #558: the message, the text that base's constructor writes where none or a null one is given, and a parameter's name or an inner exception, each by its parameter. - The members it inherits are accessors an override replaces, the virtual `Message` and `ParamName` included, and a construction of a generic class carries its own types from before its constructor's body runs, through `$eq.exceptions.construct`. - An exception type is named as .NET names it at run time (``App.Failed`1[System.Int32]``, `App.Outer+Inner`), for a typed `catch` and for the message .NET writes where none is given. - The constructor only .NET's serialization calls, `(SerializationInfo, StreamingContext)`, is no branch of the twin, as a record's copy constructor is not, for an exception class and a plain class alike. So Visual Studio's exception template builds as it is written. A call of one the browser would make, a `new` or the base call, base clause or chain of a constructor the twin writes, fails the build (EQ1009), since with no branch for it the call ran another constructor. - An exception built or tested where a type argument it carries is a type parameter fails the build (EQ2014): `new Failed<T>()` in a generic method, `catch (Failed<T>)`, or `new Missing()` inside the generic class that declares it. See the decision below. Over #654, where a nested type is a module of its own: - An exception class declared inside another type is a class too, its twin named by its owner (`Holder$Nested`), and what an exception class declares crosses as it does (`Rejected$Reason`). #654 kept both out while an exception had no module, so the two changes say one thing together here: what a server-only type or an attribute declares has no twin (EQ2010), and what an exception class declares has one. The server's route table follows the same rule. - A snippet compiled on its own, with no project's compilation to read a class's bases from, still builds an app exception as the runtime's exception. The construction named a twin nothing writes there, and the page met `Failure is not defined`. What the class declares stays out of reach in that host, as it was on main, which is #754. For an app: a `B` held as an `A` answers A's `Name()`, a sequence of your own works in `foreach` and LINQ, and `new Failure().Code` reads what the class declares. ## Why Each of these built green and answered otherwise in the browser, measured through the module graph an app's build writes, both sides executed: - A `class B : A` declaring `public new string Name()` was refused (EQ1007) where A was the app's, legal C# and all, and answered B's where A was .NET's. - `foreach` over a class that implements `IEnumerable<T>` threw, since no twin had a `[Symbol.iterator]`, and its explicit `IEnumerable.GetEnumerator()` was written over the generic one. - An app exception was an `Error` built by its symbol, so `new Failure().Code` was null, `Describe()` threw, and `: base("failed")` read an empty message. ## A decision to confirm EQ2014 refuses valid C#: an exception that carries a type parameter where it is built or tested. JavaScript erases the type argument, so `Make.Of<int>()` threw an exception that carried `T` itself and `catch (Failed<int>)` missed it in silence, which is why it is fenced, as `new T()` (EQ2003) and a `ServerTopic<T>` built over a type parameter (EQ2013) are. It can be overturned. The alternative is to carry the type arguments a generic method or class receives at run time, which changes every generic call and is a change of its own. ## OpenSpec - `openspec/changes/archive/2026-10-08-a-class-dispatches-and-enumerates-as-dotnet-does`, archived into `openspec/specs/transpiler-classes`, `openspec/specs/transpiler-sequences` and `openspec/specs/transpiler-exceptions`. ## Proof - A/B per case against the head this merges over, #707's at f03bcb6, which main holds now, each side built with 0 errors and the same probe run on both, each case alone through the module graph, as TypeScript and as JavaScript. #707's head moved to 5e5b5ca since, by a server message and a spec file that no case reads: - `ClassDispatchConformanceTests` holds 71 cases, 18 for #563, 20 for #612, 30 for #611 and 3 for the template, so 142 runs. - On that head, 66 of the 71 cases fail (130 of the 142 runs). The five that pass are answers it had right, and the branch keeps them: the default message, no inner exception is null, a catch of the .NET type it derives from, the text of the .NET base an app exception calls, and an exception nested in a generic class caught by its construction. - On the branch, 71 of 71 pass (142 of 142 runs). - A/B of Copilot's second round, before against after. Before is the head with the round's seven source files taken back to what they were, built with 0 errors: - The 13 Compiler cases of the round fail before and pass after: 6 of `ASequenceItsTwinWouldWalkOtherwise_IsRefused`, 3 of `AnExceptionTypeNamedWhereATypeArgumentIsATypeParameter_IsRefused` and 4 of `ACallOfASerializationConstructor_IsRefused`. - Through the module graph, before, each shape answered otherwise than .NET in silence: `other` for `int` where `Make.Of<int>()` threw and `catch (Failed<int>)` waited, `0a:a` for `3:a` over a class with two `IEnumerable<T>`, `1|1` for `x|1` over a non-generic `GetEnumerator()` of its own, `1,2|3` for `10,20|30` over an explicit `IEnumerator<int>.Current` of its own, and `strings` for `serialization` where `new Snap(info, context)` ran `Snap(string, string)`. After, each is a build error. - Copilot's first round found three defects, each fixed in its own commit with its tests: 13e5617 (two GetEnumerators that walk different sequences), 31ad19a (a generic exception's construction and the names .NET writes) and db5f840 (a constructor with no model). - Copilot's third round, the last, found one, in code that came with #654 and is on main: a static reached through `using static` on a nested type of a `[RuntimeProvided]` owner in an app's namespace was imported as a module of the app's, which nothing writes, so the call reached an undefined `Kit$Part`. The import's bucket is the symbol's now, its owners included (121a943). `AStaticOfARuntimeProvidedOwnersNestedType_ReachedBare_IsTheRuntimes` fails on the head before it and passes here. - The served runtime grows 500 gzip bytes over what main serves, from 219241 to 219741, both sides built and measured the same way (gzip, SmallestSize). 219241 is also what main's record says. - Suites, each alone through the machine's suite lock, every run exiting 0 and none aborted, on c9e0a2c with this branch's wiki at 4dda3aa: Compiler 1891 passed, Web 1021 passed, Server 372 passed, Conformance 4863 passed and 1 skipped of 4864, and the runtime's `TestRuntime` 2339 tests in 170 files. `dotnet build samples/DefaultUIDashboard` builds with 0 warnings and 0 errors there. After the third round's fix, a change to the compiler alone, the Compiler suite (1892 passed), Web (1021 passed) and Conformance (4863 passed and 1 skipped) ran again on 121a943, and the twins regenerated there did not move, so the runtime is the same bundle. The head records main's squashes of #715, #654 and #707 with ours and keeps that tree, byte for byte. - The runtime's twins and the BCL baseline, regenerated over the merge, did not move. The null-argument baseline of #715 is as main has it, 401 entries: nothing here closes or opens one. - The review (`/code-review high`) found ten things. Six were defects and are fixed, each with its case: a public `GetEnumerator()` that returns a struct beside the explicit ones ran out of stack, a class that is its own enumerator looped, a generic method that hides one over its own type parameter was refused, a `Name` of the app's on an exception read the type's name, an override of `ParamName` read the base's, and a build with no compilation gave an exception a module that extended nothing. - Visual Studio's exception template, which the review found refused, builds as it is written: its `(SerializationInfo, StreamingContext)` constructor is no branch of the twin. `VisualStudiosExceptionTemplate_BuildsAsItIsWritten` builds it as the IDE writes it, with SYSLIB0051 suppressed, beside a plain class with a constructor of the same shape. - Four stay as they are: - An exception class whose members reach what the browser cannot run is marked `[ServerOnly]`, as every class is, which is the break below. - A collection's own `GetEnumerator()` (`=> items.GetEnumerator()`) has no translation (EQ2004), as on main, and the docs say to yield the items. - A `new` method in a type the runtime carries would be named apart only in the runtime's own build. None exists today. - The rule for a .NET exception's arguments is one table read twice, by a `new` and by a base call, and its two renderings could be one builder. ## Checklist - [x] `dotnet test` on the affected test projects, and `dotnet build src/eQuantic.UI.Runtime -t:TestRuntime` if TypeScript changed (the embedded Bun runs `tsc`, then `vitest run`) - [ ] This PR's checks include a run named CI with jobs in it (`build-packages`, `test (ubuntu-latest)`, `test (windows-latest)`, `openspec`, `session-start`, `wiki-checkout`). A workflow whose expression does not parse creates zero jobs, and the ruleset's required checks then keep the PR blocked. GitHub lists the workflow by its file path instead of `CI`. While GitHub Actions has no credits, the same run is on eQuantic Space (`eqs runs ls`) - [x] `./scripts/check-openspec.sh` passes: the OpenSpec change validates strictly, and it is archived before the merge - [x] `dotnet build samples/DefaultUIDashboard` (and `PhotonDesktop` / `WalletMobile` if the native track changed). CI's `samples` job builds all three on macOS only (#149), and the other hosts are yours - [x] A broken contract has a line in the migration notes (we are in preview: break freely, hide nothing). An exception class of the app's is transpiled as any class is, so one the browser never sees, whose members reach what it cannot run, is marked `[ServerOnly]`. The emitted twin of a method that hides one has a name of its own, which TypeScript written by hand against it would meet. Four shapes that built and answered otherwise than .NET now fail the build: a type that implements `IEnumerable<T>` over two types, a non-generic `GetEnumerator()` or a `Current` that does not hand back the kept member's answer (EQ1007), a call of a serialization constructor (EQ1009), and an exception built or tested where a type argument it carries is a type parameter (EQ2014). - [x] The documentation changed with the behaviour: this repository's Markdown here, and the wiki page in English AND Portuguese, in one commit on a branch of the [wiki repository](https://github.com/eQuantic/equantic-ui/wiki) named exactly like this pull request's branch, merged into the wiki's master when this merges (the twin lives at `locale/pt-BR/<Page>-pt-BR.md` there) - [x] One `docs/LEDGER.md` line for this event, citing the issue - [x] The diff reviewed by its author before the PR opened (in Claude Code, `/code-review high`), every Copilot thread answered and resolved, a new round asked for (`gh pr edit <n> --add-reviewer @copilot`) only after a defect, and three rounds at most - [x] No "widget" in prose, the project's word is *component* --------- Co-authored-by: Edgar Mesquita <edgar@equantic.tech>
edgarmesquita
pushed a commit
that referenced
this pull request
Oct 11, 2026
Merges 5e5b5ca, the head of #707 as the queue's head holds it, the third of the steps that bring this branch to the head of the queue. No source file conflicts: #707's collection constructors, comparers and LINQ table sit beside this branch's formatter, and DIAGNOSTICS.md and its baseline merged on their own, with EQ2109 still retired and EQ2110 still this branch's. Six runtime twins conflict and take #707's side. Every twin is regenerated after the last step. docs/LEDGER.md and the PublicAPI files merged as a union, and this branch's ledger line moves back to the end of the events.
edgarmesquita
pushed a commit
that referenced
this pull request
Oct 11, 2026
Merges 78215e9, the head of #708 after #654 and #707 landed on main (d7a8c31, 178497c). Over the 25643d5 this branch already held it brings one commit, 121a943: InvocationStrategy buckets a bare `using static` call's class by its symbol, with its test in NestedTwinNameTests. The rest are merges of main's squashes that change no content. Nothing conflicts, and no twin moves.
5 of 6 tasks
edgarmesquita
added a commit
that referenced
this pull request
Oct 11, 2026
Closes #756 The bump to `0.2.0-preview.61`, cut from main at 9305100 (#637) at Edgar's request. It carries forty-five pull requests since `.60`: a component that hears the server on a typed topic (#647), a page that requires its authorization and a Server Action that refuses another site's request (#686, #737), a layout that follows its box and type that follows the window (#648, #665), a control's press and focus on every target with a 24dp floor under a pointer (#617, #628), the code editor's completion (#618, #653, #733), a hot reload under `dotnet watch` that keeps the page's state (#666, #672), the image optimizer on SkiaSharp (#730), the places where the producers drew one tree differently, with Photon's hit-testing and its text layout (#688, #690, #699, #739, #741, #749), a number and a date printed in the page's culture (#637), and a long family of browser answers made .NET's (#579, #588, #594, #607, #608, #621, #654, #656, #660, #661, #667, #668, #689, #696, #697, #705, #707, #708, #709, #715, #731, #748, #752), with a page that answers HEAD (#610) and the conformance harness's own comparison (#609). ## What this commit does - `Directory.Build.props`: `0.2.0-preview.60` → `0.2.0-preview.61`. - `./scripts/public-api.sh ship`: nine projects, 462 declarations join the shipped surface and 43 retire, and every `Unshipped` is empty afterwards. Checked after the fold, per project and compared as sets: the lines that left `Shipped` are exactly the 43 `*REMOVED*` lines declared on main, and the lines that joined are exactly its 462 declarations, so an orphan could not vanish unnoticed. The line diff reads +463 −549: the version line, 462 lines into `Shipped` and 43 out of it, and the 505 that leave the `Unshipped` files. - The analyzer accepts the fold: the solution builds with no RS0016 or RS0017. And it read the files: one bogus declaration appended to an `Unshipped` failed the build with RS0017. - Against `v0.2.0-preview.60` the `Shipped` diff has those 43 lines gone and no other, so no pull request rewrote a shipped line in place this time, as #512 did in `.60`. ## What the retirements are Read out of the `*REMOVED*` lines, each paired with the line that joined under the same member and attributed by the commit that wrote it, not from the log. Only `ServerActionHub` left with nothing in its place, and the rest changed shape: - `ServerActionHub` and its constructor (#647): the hub, its route `/_equantic/hub` and the `AddSignalR()` call `AddUI` made are gone. An app that injected `IHubContext<ServerActionHub>` publishes through `IServerEventPublisher`, and one that maps hubs of its own calls `AddSignalR()` itself. - `ICodeCompletionProvider.CompleteAsync` (#618) takes a `CodeCompletionContext` and answers a `CodeCompletionList`. `CurlyBraceLanguage.Keywords` was the protected abstract set a language overrode, and it is the public list a completion offers now. - `Drawing`'s constructor, `Drawing.Width` and `UI.Drawing` (#648) hold the width as a `SizeValue`. `new Drawing(art, 240)` converts, and a read into a `float` becomes `drawing.Width.Value`. - `ImageOptimizer()` and `BlurPlaceholderGenerator()` (#730) take the `ImageOptimizationOptions`. `UseImageOptimization` registers both, so only an app that built one with `new` meets it. - `HitRegion` (#690) gains `Drawn` after `Bounds`, so building one and deconstructing one both change. `CodeRegion` (#653), `SheetRegion` and `TextRegion` (#690) gain optional members at the end, so only a positional deconstruction changes. - `ServerActionsMiddleware`'s constructor (#737) takes an `IOptionsMonitor<ServerActionsOptions>`. `UseServerActions()` builds it, so only an app that constructs the middleware itself meets it. - Four signatures widened at the end by optional parameters, which compiles as before from source: `UI.Positioned` by the four fractions and the two shifts (#648), `UI.Pressable` by `canRequestFocus` (#653), `WebRealizer.Lower` by `density` (#688) and `PhotonRealizer.Realize` by `sizedLike` (#690). - `eQuantic.UI.Compiler`, which no app references, 21 lines: eleven of #661 (`JsTemplate`'s `Annotate`, with its constructor, its `Deconstruct` and the factory that took it, and six LINQ strategies that crossed to the IR and lost their text `Convert`), four of #608 (`ValueMember`, `ComponentDependencyResolver`'s constructor and `RecordTypeEmitter.CanEmit`), two of #637 (`Eq.AsInteger` and `Eq.AsSingle`), two of #654 (`UserDefinedOperators.Binary` and `Unary`), and `HashSetStrategy` (#594) and `RangeIndexerStrategy` (#707) crossing to the IR. One break is an addition, which no `*REMOVED*` line shows. `CurlyBraceLanguage.ReservedWords` (#618) is a new abstract member, so a language derived from it renames its `Keywords` override to `ReservedWords`. I listed every member that joined an interface or an abstract class already shipped, and it is the only one an implementer has to write: `ICodeLanguage.Keywords` and `ICodeCompletionProvider.ResolveAsync` have default implementations. The additions are 118 in `eQuantic.UI.Code` (the completion), 116 in `Primitives` (the server events vocabulary, `FluidSize`, `Positioned`'s fractions and shifts, `BoxStyle.Pressed`, `GridTrack.AutoFill`, `Touch.MinPointerTarget`), 98 in `Server` (server events, the origins a Server Action allows), 86 in the compiler, 19 in `Native.Components`, 12 in `Web`, 7 in `Components`, 4 in `Native.Framework` and 2 in `Images`. ## The rest of the surface `developer-surface.baseline.txt` gains two configuration sections since `v0.2.0-preview.60`, `EQuantic:ServerActions` (#737) and `EQuantic:ServerEvents` (#647), and loses nothing: no MSBuild property or template parameter came or went. What an app meets with no signature moving: - Three new build errors. EQ1009 (#608, #621, #708) stops two constructors that take the same number of arguments, in a class, a record or a struct that reaches the browser, where the twin kept one and a `new` that meant another ran it. EQ2014 (#708) stops an exception built or tested where a type argument it carries is a type parameter. EQ2013 (#647) stops a `ServerTopic<T>` built over a type parameter, which only new code can meet. - Refusals widened under existing codes: EQ1007 by a second indexer and a sequence over two types (#707, #708), EQ2004 by a stored `Range`, a method group over a .NET extension and a .NET collection's own `GetEnumerator()` (#656, #707, #708), EQ2007 by a comparer handed to `ToDictionary`, `ToLookup`, `GroupBy` or `Distinct` (#707), and EQ2010 by an extension method of a server-only class and a type nested in one (#654, #705, #731). - Two refusals a running app meets on the server: a Server Action called from another origin answers 403 until the app lists that origin (#737), and a class-level `[Authorize]` guards the page as well as its actions (#686). - `image/avif` in the optimizer's `Formats` stops the app at startup (#730), and a `Spacer` of weight below 1 or a negative `Flexible` weight throws where it is written (#741). - EQ2109 is retired and EQ2110 is about reading a number now, since a number written as text follows the page's culture on both sides (#637). The rest are browser answers made .NET's and drawings made alike across the producers, listed in the tag. ## The wiki's version marks `WikiVersionMarkTests` compares a mark only once a tag holds its symbol, so the `.61` tag is when this release's marks are judged for the first time. Emulated before the tag, with the test's own pattern and lookup and a `.61` at 9305100: 145 marks, no mismatch. The thirteen it judges first resolve to `.61`, as written: `StartFraction`, `GridTrack.AutoFill`, `canRequestFocus`, `DENSITY_COOKIE`, `Drawing`'s `SizeValue` width, `WithPageAuthorization`, `AllowServerActionOrigins`, `IServerEvents`, `MapGetAndHead`, `UseServerEvents`, `TwinIsData` on the tokens, `BoxStyle.Pressed` and `ScrolledThreshold`. ## The tag's gate, rehearsed `gate-templates` runs only on a `v*` tag, so a template that does not build costs a release attempt. Rehearsed on main at 9305100, before this branch: 50 packages packed at `1.0.0-dev`, the three web and the four native template shapes scaffolded and built in Release against that feed alone, with no warning and with every eQuantic package restored at the version packed, the published `runtime.js` identical byte for byte to the Server package's, and the dashboard template served, hydrated and navigated in a browser with no console error. ## Not in this release No pull request is open. Forty-eight bugs are, none of them a regression against `.60`. Three issues came out of this stretch and wait for a decision or their turn: #753 (`string.Split` with an array of separators, a count or a null separator), #754 (an app's exception class in a snippet compiled on its own) and #755 (whether generic code should carry its type arguments at run time, which EQ2014 fences). The release notes travel in the annotated tag. The wiki's Upgrading page carries the distilled entry in both languages, on the wiki branch `chore/0.2.0-preview.61`, which merges into the wiki's master when this merges and before the tag. The `docs/LEDGER.md` line for the release follows the tag, as `.60`'s did, because it names the commit the tag lands on. ## Checklist - [x] The diff reviewed by its author before the PR opened: a version line and a mechanical fold, verified as sets above - [x] `dotnet test` on this branch: Compiler 1898, Server 374, Conformance 5087 with one skipped, Web 1021, and the runtime's own 2355 in 170 files, none aborted - [ ] This PR's checks include a run named CI with jobs in it - [x] `./scripts/public-api.sh ship` run, every `Unshipped` empty, the retirements read from the diff - [x] The documentation changed with the release: the Upgrading entry in English and Portuguese, on the wiki branch `chore/0.2.0-preview.61`, with the wiki guards passing against it (9 of 9, and the vocabulary guard fails on that page when a word it refuses is written into it) - [x] Copilot's rounds answered: one round, approval recommended with no finding and no thread, so the loop ends there Co-authored-by: Edgar Mesquita <edgar@equantic.tech>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.


Closes #564
Closes #578
Closes #585
Closes #586
Closes #593
All five are sub-issues of #565 (the transpiler's fences hold on every path, continued).
What
A list built with an argument and an initializer (#564):
List<T>construction is one array, as C# builds it: what its constructor copies, spread, then each element of its collection initializer in order. A capacity puts nothing in it, but it is evaluated before the elements and a negative one is refused asList<T>'s constructor refuses it. An object initializer is applied once the list is built.List<int> c = new(source)is a copy, where it was an empty list.A LINQ operator's comparer (#578):
ToDictionary,ToLookup,GroupByandDistincthand their comparer to the fence a collection's construction already passes (EQ2007). One that asks for the key type's own equality, ordinal strings or null is dropped, since the lowering finds the keys that way already, and any other fails the build.A range over a type with a Slice (#585):
Slice(start, length). The receiver is read once, then the endpoints in their order, then theLengthorCount, read only where an endpoint counts from the end or the end is open, as measured on .NET.Rangeitself fails the build (EQ2004). A range over a string, an array or a list keeps JavaScript'sslice.RangeIndexerStrategymoves to the IR and leaves the migration baseline.A list's face (#586):
IList<T>,IReadOnlyList<T>orIListis a place of its own kind, read and written by the runtime's$eq.collections.itemandsetItem, which answer an array's subscript and a twin'sitemandsetItemalike. Every writer takes it: an assignment, a compound, a step,??=,?[], an object initializer's entry and a key from the end.Countthrough the face counts through$eq.collections.count, which asks a twin's owncountbefore asizeor alengthit may also declare. A read or a call through a face that holds null throws .NET's NullReferenceException, and a null-conditional one answers null.ICollection's Add and Clear (#593):
ICollection<T>'s ownAddandCleargo through the runtime's$eq.collections.addandclear, which add and empty as the collection behind the interface does: an array appends, a set adds a value it lacks, a linked list adds last, a dictionary adds the pair and refuses a key already there in its own words, and a twin runs its own. A member's collection initializer adds the same way.For an app:
new List<int>(source) { 3 }holds the source's elements and then 3,jobs.ToDictionary(j => j.Id, StringComparer.Ordinal)builds,strip[1..3]callsSlice(1, 2),r[0] + r[2] + r.Countover a twin behindIReadOnlyList<int>answers 19, andc.Add(2)over aHashSet<int>behindICollection<int>adds.Why
Measured on main, both sides executed by the conformance suite:
new List<int>(10) { 1, 2 }was[], andnew List<int>(source) { 3 }wassource, [3], a second declarator in a declaration and a second argument in a call (A list built with a capacity or a source and an initializer drops or garbles its elements #564).ToDictionaryrefused every comparer,StringComparer.Ordinalincluded (EQ1004), andGroupByrefused every one too (EQ2008).ToLookup(k, comparer)called the comparer as an element selector, andDistinctdropped whatever it was handed, soDistinct(StringComparer.OrdinalIgnoreCase)kept "a" and "A" (ToDictionary refuses StringComparer.Ordinal, which the comparer fence passes #578).new Strip()[1..3]called the twin'sslice(1, 3), three elements where .NET'sSlice(1, 2)holds two, and[3..1]sliced where .NET throws (A range over a type with Slice calls JavaScript's slice, which takes an end where Slice takes a length #585).r[0] + r[2] + r.Countover a twin read a subscript and alength, null where .NET says 19 (A twin read through an indexer of a BCL interface reads a JavaScript subscript #586).c.Add(2)over a set or a linked list threwc.push is not a function, andClearthrew the same way forsplice(ICollection<T>'s Add and Clear call an array's push and splice, which a set or a linked list behind it lacks #593).OpenSpec
openspec/changes/archive/2026-10-08-a-collection-and-an-indexer-answer-as-dotnet-does, archived intoopenspec/specs/transpiler-bcl,transpiler-sequencesandtranspiler-expressions.Proof
src/this merges onto (built with 0 errors) and over this branch. That tree isfix/a-nested-class-is-its-ownersat edf747a, main with 🐛 fix: a copy of a generic record or struct keeps its closed type #752, 🐛 fix: a null argument is refused as .NET refuses it, measured over the translated surface #715 and 🐛 fix: a nested class is a module of its own, named by its owner #654, and "main" below means it:CollectionAndIndexerConformanceTests: 97 of its 115 cases fail on main, and all 115 pass here.ICollection<T>andIList<T>, a target-typed list with a capacity, and the null-conditional and pattern reads of a null face. Seven are LINQ comparers that ask for the default: main dropped two of them harmlessly, fencedToHashSet's already, and refused the build of the fourGroupByrows with EQ2008, which the harness does not read. The last two are a write throughIList<T>that answered its value on main by accident, from a property named0.CollectionComparerFenceTestsandRangeIndexerTestsfail on main. The two that pass are builds main allowed,ToLookupwithEqualityComparer<int>.DefaultandDistinctwith a null comparer, whose answers the conformance cases judge.Lengthbeside itsCountwas counted by its length through the face. Its case fails on the commit before the fix, in both forms, and passes with it. The range refusal's test now reads the strategy's own message, since EQ2004 is every untranslated member's./code-review high) found two more, fixed here. A complex element initializer,new List<int> { { 1 }, 2 }, wrote[{}, 2], on main as on this branch before the fix: it hands its one expression toAddnow, and its case is in the conformance theory. And an indexer declared overRange?took the slice path, where one overRangeis refused: a row ofRangeIndexerTestsrefuses it now. Both fail on main.Countread through a null face answered 0, because the runtime's count counted a null as none: it throws .NET's NullReferenceException now, and so do the face's element reads and itsAddandClear, whiler?.Countstill answers null. And GroupBy read its arguments by position, soGroupBy(comparer: c, keySelector: k)refused its key selector: each argument plays its own parameter now. Against the head before that round, 12 of the 14 new conformance cases and 2 of the 3 new Compiler cases fail, and all of them pass now. The two conformance cases that pass there are the null-conditional rows, and the Compiler one is a refusal that landed on the key selector, whichANamedComparerWrittenFirst_IsRefusedWhereItIsWrittenpins to the comparer. AT[]behind the faces grows where .NET refuses, as it did before this pull request, and A T[] behind ICollection<T> or IList<T> grows and shrinks where .NET refuses the call #711 holds it.new List<int>(Capacity()) { Item() }never calledCapacity, and a negative one built a list where .NET throws. It is evaluated first now, through$eq.collections.listCapacity, which refuses a negative one in .NET's words, and a constant the list takes still writes the bare array. Five of its six new conformance cases fail on the head before the fix, and the sixth, a capacity read from a variable, fails on main. The capacity pin inCollectionConstructionTestsfollows it.ToLookup(source, keySelector, comparer)handed a null comparer built its lookup in .NET and called the comparer as an element selector in the browser. No entry is new.ToDictionarywith a null comparer refused (it is now compared with the call written without it),IList<T>.Addas the array'spush(it now reaches$eq.collections.add, which pushes onto an array), and GroupBy's site in the diagnostics baseline under EQ2008.TestRuntime2,323 tests in 170 files. Every exit code 0, no run aborted.dotnet build samples/DefaultUIDashboard: 0 warnings, 0 errors.Checklist
dotnet teston the affected test projects, anddotnet build src/eQuantic.UI.Runtime -t:TestRuntimeif TypeScript changed (the embedded Bun runstsc, thenvitest run)build-packages,test (ubuntu-latest),test (windows-latest),openspec,session-start,wiki-checkout). A workflow whose expression does not parse creates zero jobs, and the ruleset's required checks then keep the PR blocked. GitHub lists the workflow by its file path instead ofCI. While GitHub Actions has no credits, the same run is on eQuantic Space (eqs runs ls)./scripts/check-openspec.shpasses: the OpenSpec change validates strictly, and it is archived before the mergedotnet build samples/DefaultUIDashboard(andPhotonDesktop/WalletMobileif the native track changed). The native track did not changeDistinctorToLookup(EQ2007, where it was dropped or called as a selector), and a range handed to an indexer overRange(EQ2004). IneQuantic.UI.Compiler's public surface,RangeIndexerStrategy.Convertgives way toConvertIr, andEqgains four constants.locale/pt-BR/<Page>-pt-BR.mdthere)docs/LEDGER.mdline for this event, citing the issue/code-review high), every Copilot thread answered and resolved, a new round asked for (gh pr edit <n> --add-reviewer @copilot) only after a defect, and three rounds at most