Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
78 commits
Select commit Hold shift + click to select a range
ce9cd08
📝 docs: propose a nested class as a module of its own, named by its o…
equantictech Oct 6, 2026
474ac1c
🐛 fix: a nested class is a module of its own, named by its owner
equantictech Oct 6, 2026
8db20a1
🐛 fix: the server names a nested page by its twin, and a reference im…
equantictech Oct 6, 2026
ad5ded6
🔀 merge: main into fix/a-nested-class-is-its-owners
equantictech Oct 7, 2026
37ffbbd
🐛 fix: a list built with an argument and an initializer holds both
equantictech Oct 7, 2026
c33f00c
🐛 fix: a LINQ operator takes a comparer the collection fence passes
equantictech Oct 7, 2026
8f4d97c
🐛 fix: ICollection's Add and Clear answer for the collection behind it
equantictech Oct 7, 2026
514d173
🐛 fix: a list's face reads and writes the list behind it
equantictech Oct 7, 2026
a014cf4
🐛 fix: a range over a type with Slice calls it with a length
equantictech Oct 7, 2026
3c6d933
✅ test: a LINQ operator's comparer is refused or passed by the fence
equantictech Oct 7, 2026
c6e7a6a
📝 docs: propose the collection and indexer family, and its diagnostics
equantictech Oct 7, 2026
1ef97bb
🐛 fix: a key comparer after a null-conditional is dropped by its posi…
equantictech Oct 7, 2026
68da5db
🐛 fix: the runtime's twins read a list through its face as the list a…
equantictech Oct 7, 2026
6372343
🐛 fix: a nested twin is imported wherever it is written, and a nested…
equantictech Oct 7, 2026
d54df5a
🔀 merge: main into fix/a-nested-class-is-its-owners
equantictech Oct 7, 2026
10e0273
🐛 fix: a nested type whose owner never crosses is refused, and an ann…
equantictech Oct 7, 2026
8e61bad
🐛 fix: a list face's read is typed by its element in the runtime's twins
equantictech Oct 7, 2026
8d27a3a
🔀 merge: main into fix/a-collection-and-an-indexer-answer-as-dotnet-does
equantictech Oct 8, 2026
799717f
🔀 merge: main into fix/a-collection-and-an-indexer-answer-as-dotnet-does
equantictech Oct 8, 2026
374213e
🐛 fix: a nested type whose owner never crosses is refused however cod…
equantictech Oct 8, 2026
31460d2
✅ test: a range at an indexer over Range is refused in the strategy's…
equantictech Oct 8, 2026
049fb5f
🔧 chore: regenerate the runtime's twins over main's exception messages
equantictech Oct 8, 2026
f9abeec
🐛 fix: a list's face counts a twin by its own Count
equantictech Oct 8, 2026
6068857
📝 docs: the proposal counts a twin by its own Count, and names its wi…
equantictech Oct 8, 2026
48ccf17
🔧 chore: the served runtime's budget records the reads through a list…
equantictech Oct 8, 2026
3381621
🔀 merge: main into fix/a-nested-class-is-its-owners
equantictech Oct 8, 2026
5b025b0
📝 docs: the ledger records the collection and indexer family
equantictech Oct 8, 2026
f830ba2
📝 docs: archive the collection and indexer change into the transpiler…
equantictech Oct 8, 2026
b8f59e8
✅ test: the compiler's pins follow a comparer the fence passes and IC…
equantictech Oct 8, 2026
28ddcdb
🔧 chore: EQ2010's reporting sites and the served runtime's budget aft…
equantictech Oct 8, 2026
18a92bf
🐛 fix: a list initializer's complex element is the value its Add take…
equantictech Oct 8, 2026
b9c3229
✅ test: a null argument is measured on both sides over the translated…
equantictech Oct 8, 2026
b12c521
🐛 fix: a twin refuses a null argument by its parameter's name, as .NE…
equantictech Oct 8, 2026
b5d8f0c
📝 docs: the null-argument measurement in the ledger and in OpenSpec (…
equantictech Oct 8, 2026
ecc818c
✅ test: the null-argument control hands a culture at the call, and ea…
equantictech Oct 8, 2026
d1f9b55
📝 docs: the null-argument change archived into transpiler-exceptions …
equantictech Oct 8, 2026
ad443f1
🐛 fix: a read through a null list's face throws as .NET's does
equantictech Oct 8, 2026
ffc3fc8
🐛 fix: GroupBy's named arguments land in their own parameters
equantictech Oct 8, 2026
c8d6f62
🔧 chore: the served runtime's budget at this branch's head
equantictech Oct 8, 2026
68a2970
📝 docs: the ledger counts the final cases and names the review's find…
equantictech Oct 8, 2026
b964ea4
📝 docs: the specs say a null face throws and GroupBy's arguments go b…
equantictech Oct 8, 2026
07094db
🔧 chore: the runtime's budget and the ledger count the null refusals
equantictech Oct 8, 2026
6f3e07e
🔀 merge: main into fix/a-null-argument-is-refused-as-dotnet-refuses-it
equantictech Oct 10, 2026
b99d828
🔀 merge: main into fix/a-collection-and-an-indexer-answer-as-dotnet-does
equantictech Oct 10, 2026
6b5fa10
🔀 merge: main into fix/a-nested-class-is-its-owners
equantictech Oct 10, 2026
ea23e2f
🐛 fix: a list's capacity is evaluated before its elements and refused…
equantictech Oct 10, 2026
149c031
📝 docs: the spec says a list's capacity is evaluated first and refuse…
equantictech Oct 10, 2026
70b2dd9
🔀 merge: main into fix/a-nested-class-is-its-owners
equantictech Oct 10, 2026
d958806
🔀 merge: main into fix/a-null-argument-is-refused-as-dotnet-refuses-it
equantictech Oct 10, 2026
05f6f3c
🔧 chore: regenerate the runtime's twins over main and the capacity's …
equantictech Oct 10, 2026
41bfb0b
🔀 merge: main into fix/a-nested-class-is-its-owners
equantictech Oct 10, 2026
9a00b91
🔀 merge: main into fix/a-collection-and-an-indexer-answer-as-dotnet-does
equantictech Oct 10, 2026
d03dd26
🔀 merge: main into fix/a-nested-class-is-its-owners
equantictech Oct 10, 2026
9d95e4c
🔧 chore: the served runtime's budget records the branch over main at …
equantictech Oct 10, 2026
262b49b
📝 docs: the ledger counts the cases over main at 1ff5ceb2 and names t…
equantictech Oct 10, 2026
b12b813
🐛 fix: a copy of a generic record or struct keeps its closed type (#751)
equantictech Oct 10, 2026
79ecf68
🔀 merge: main into fix/a-null-argument-is-refused-as-dotnet-refuses-it
equantictech Oct 10, 2026
a9c57f1
🐛 fix: a copy of a generic record or struct keeps its closed type (#751)
equantictech Oct 10, 2026
4acca7b
🔀 merge: main into fix/a-copied-value-keeps-its-closed-type
equantictech Oct 10, 2026
5d6cda0
🐛 fix: the receiver of CompareTo(object) runs before its null is read
equantictech Oct 10, 2026
15244bf
✅ test: the null-argument measurement compares an array's answer, ove…
equantictech Oct 10, 2026
9e5a828
🐛 fix: a copy is of its closed type from its allocation
equantictech Oct 10, 2026
00302e8
🔧 chore: take the copy fix from its own pull request
equantictech Oct 10, 2026
eae3ecf
🔀 merge: the copy fix and main into fix/a-null-argument-is-refused-as…
equantictech Oct 10, 2026
56b6dde
🐛 fix: CompareTo(object) reads its receiver twice, where it wrote an …
equantictech Oct 10, 2026
6e00c81
🔧 chore: the served runtime's record over the copy fix
equantictech Oct 10, 2026
edf747a
🔀 merge: main and the two pull requests ahead into fix/a-nested-class…
equantictech Oct 10, 2026
3efb678
🔀 merge: the queue's head into fix/a-collection-and-an-indexer-answer…
equantictech Oct 11, 2026
62b8496
✅ test: the null-argument baseline loses ToLookup's null comparer
equantictech Oct 11, 2026
5f0e48b
🔧 chore: the served runtime's record over the queue's head
equantictech Oct 11, 2026
f03bcb6
📝 docs: the ledger counts over the queue's head, and the null-argumen…
equantictech Oct 11, 2026
a1b8fe2
🔀 merge: main into fix/a-nested-class-is-its-owners
equantictech Oct 11, 2026
1ad315e
🔀 merge: main into fix/a-collection-and-an-indexer-answer-as-dotnet-does
equantictech Oct 11, 2026
d9c3e37
🐛 fix: the refusal of a page inside a type with no module names a run…
equantictech Oct 11, 2026
a7c4635
🔀 merge: the head of #654 into fix/a-collection-and-an-indexer-answer…
equantictech Oct 11, 2026
5e5b5ca
📝 docs: the closure spec says what an unmarked copy compares as
equantictech Oct 11, 2026
4aead97
🔀 merge: main into fix/a-collection-and-an-indexer-answer-as-dotnet-does
equantictech Oct 11, 2026
1f4f96d
🔀 merge: main into fix/a-collection-and-an-indexer-answer-as-dotnet-does
equantictech Oct 11, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions docs/DIAGNOSTICS.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,10 +52,10 @@ not shrink with compiler work — they are the shape of the target.
| `EQ2001` | A C# construct with no runtime equivalent in the browser (typed-reference intrinsics: `__makeref`, `__reftype`, `__refvalue`, `stackalloc`, pointers). | Restructure without it. |
| `EQ2002` | `goto`. | Restructure with loops and conditionals, or a labelled `break`/`continue` (those DO translate). |
| `EQ2003` | `new T()` on a type parameter — generic arguments are erased at runtime in JavaScript, so the concrete type is unknown. | Pass a factory (`Func<T>`) or the constructed value in. |
| `EQ2004` | A member declared outside the compilation that no strategy translates, so nothing emits it: a BCL member with no JavaScript form (`Convert.FromBase64String`, `DateOnly.ParseExact`), written qualified or reached bare through `using static` (`WriteLine` under `using static System.Console`), an extension method whose declaring class is not part of this compilation, or a `System.Range` value stored rather than indexed with. | Use a member that translates (index with the range directly, `text[a..b]`), move the call behind a `[ServerAction]`, or bring the declaring source into the compilation. |
| `EQ2004` | A member declared outside the compilation that no strategy translates, so nothing emits it: a BCL member with no JavaScript form (`Convert.FromBase64String`, `DateOnly.ParseExact`), written qualified or reached bare through `using static` (`WriteLine` under `using static System.Console`), an extension method whose declaring class is not part of this compilation, or a `System.Range` value stored rather than indexed with, or handed to an indexer that takes the `Range` itself (`this[Range r]`). | Use a member that translates (index with the range directly, `text[a..b]`, or give the type a `Length` and a `Slice(int start, int length)`, which a range calls), move the call behind a `[ServerAction]`, or bring the declaring source into the compilation. |
| `EQ2005` | An infinite iterator. Iterators are MATERIALISED into an array, so the loop would run forever instead of yielding lazily. | Give the loop an end (a bound, a `yield break`), or take what you need inside the method. |
| `EQ2006` | The member does not bind in the semantic model, so any translation would be a guess. | Either the code does not compile, or eqc is missing references / generated sources. Never guessed — see [Compiler](https://github.com/equantic/equantic-ui/wiki/Compiler). |
| `EQ2007` | A comparer that changes what a collection considers equal or in order: a collection expression's `with(…)` argument beyond a capacity hint, a comparer handed to a collection's constructor (`new Dictionary<string, T>(StringComparer.OrdinalIgnoreCase)`) or to `ToHashSet`. The runtime's collections find an element by its type's default equality, and take no other comparer. The default comparers and `StringComparer.Ordinal` ask for exactly that, and pass. A sort or a binary search (`List<T>.Sort`, `Array.Sort`, `BinarySearch`) takes more: the type's default comparer, a `Comparison<T>`, a `StringComparer`'s six, `Comparer<T>.Create` and a comparer the app writes cross, and only a comparer no twin carries (a `StringComparer` made for a culture, a comparer a library compiled) is refused. | Drop the comparer, or normalize the keys yourself (lower-case them on the way in and on every lookup). For a sort, pass a `Comparison<T>`. |
| `EQ2007` | A comparer that changes what a collection considers equal or in order: a collection expression's `with(…)` argument beyond a capacity hint, a comparer handed to a collection's constructor (`new Dictionary<string, T>(StringComparer.OrdinalIgnoreCase)`) or to a LINQ operator that finds its keys or its elements by it (`ToHashSet`, `ToDictionary`, `ToLookup`, `GroupBy`, `Distinct`). The runtime's collections find an element by its type's default equality, and take no other comparer. The default comparers and `StringComparer.Ordinal` ask for exactly that, and pass. A sort or a binary search (`List<T>.Sort`, `Array.Sort`, `BinarySearch`) takes more: the type's default comparer, a `Comparison<T>`, a `StringComparer`'s six, `Comparer<T>.Create` and a comparer the app writes cross, and only a comparer no twin carries (a `StringComparer` made for a culture, a comparer a library compiled) is refused. | Drop the comparer, or normalize the keys yourself (lower-case them on the way in and on every lookup). For a sort, pass a `Comparison<T>`. |
| `EQ2008` | Query syntax using `join`, `let`, a second `from`, or `into` — its C# translation runs through compiler-generated transparent identifiers. Also the fenced initializer forms. | Rewrite in method syntax, where every operator is supported. |
| `EQ2009` | A component is declared more than once in one file (partial declarations). eqc emits one module per declaration and cannot merge them. | Combine the members into a single declaration. |
| `EQ2011` | A member that would take a name the runtime's component already uses. C# keeps a primary-constructor parameter, a field and a property apart, and spells them differently from the runtime's camelCase; JavaScript folds all of it onto `this.<name>`, so the assignment replaces the runtime's member and the page fails only in the browser. The list is read from the live runtime (`core/runtime-members.spec.ts` → `Resources/runtime-members.txt`), never typed. | Rename the member. An `override` is exempt — a component's `Build` is meant to replace the runtime's `build`, and replacing a base member is what one is for — but a plain method is not: `public void Mount() { }` overrides nothing and still emits `mount()` over the runtime's own, so the component never mounts. |
Expand Down
30 changes: 30 additions & 0 deletions docs/LEDGER.md
Original file line number Diff line number Diff line change
Expand Up @@ -2217,6 +2217,36 @@ record of a release, the wiki's Upgrading page is the distillate.
class asked the browser for a module named by its simple name, which nothing writes any more, and
one inside an owner that never crosses has no route and cannot be mapped. Proposed and archived through OpenSpec
(`openspec/specs/transpiler-classes`).
- **2026-10-07 · A collection and an indexer answer as .NET's do**: a list built with an argument and
an initializer took one or the other, `new List<int>(10) { 1, 2 }` empty and
`new List<int>(source) { 3 }` a second declarator
([#564](https://github.com/eQuantic/equantic-ui/issues/564));
`ToDictionary` refused every comparer, `StringComparer.Ordinal` included, `GroupBy` too, `ToLookup`
called one as an element selector and `Distinct` dropped whatever it was handed
([#578](https://github.com/eQuantic/equantic-ui/issues/578)); a range over a type with a
`Slice(int start, int length)` called JavaScript's `slice(start, end)` on the twin, three elements
where .NET slices two ([#585](https://github.com/eQuantic/equantic-ui/issues/585)); a twin read
through `IReadOnlyList<T>` or `IList<T>` read a subscript and a `length`, null where .NET says 19
([#586](https://github.com/eQuantic/equantic-ui/issues/586)); and `ICollection<T>`'s `Add` and
`Clear` were an array's `push` and `splice`, which a set, a linked list and a dictionary's pairs
lack ([#593](https://github.com/eQuantic/equantic-ui/issues/593)). A list is one array, what its
constructor copies and then its elements; a LINQ operator's comparer passes the collection fence
(EQ2007), dropped where it asks for the default; a range over a twin calls its `Slice` with a
length, in the order C# reads its parts, and one handed to an indexer over `Range` fails the build
(EQ2004); a list's face reads, writes and counts through the runtime's `item`, `setItem` and
`count`, which answer an array and a twin alike, a twin counted by its own `Count` before a `Length`
beside it; and `ICollection<T>`'s `Add` and `Clear` reach the runtime's `add` and `clear`. The
review before the pull request opened found a complex element initializer written as an empty object
and an indexer over `Range?` taking the slice path, Copilot's first round a read through a null face
answering 0 and GroupBy's named arguments read by their position, and its second a list's capacity
dropped unread, which never ran its call and built a list where a negative one throws, all fixed; a
`T[]` behind the faces grows where .NET refuses, as it did before,
[#711](https://github.com/eQuantic/equantic-ui/issues/711). 97 of the 115 new conformance cases fail
on main, and 14 of the 16 new Compiler cases; all of them pass here. One gap of the null-argument
baseline closes with it, ToLookup's null comparer, 402 entries to 401. The served runtime grows 686
gzipped bytes over what main serves, the twins' reads through a list's face and the runtime's
helpers behind it. Proposed and archived through OpenSpec (`openspec/specs/transpiler-bcl`,
`transpiler-sequences` and `transpiler-expressions`).

## Retired documents

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
schema: spec-driven
created: 2026-10-07
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
# Proposal

Closes #564, #578, #585, #586 and #593, sub-issues of #565 (the transpiler's fences hold on every
path, continued). Each is a collection or an indexer that answers in the browser differently from
.NET, with a green build.

## Why

Measured on main (fc8f0fdc), both sides executed by the conformance suite:

- A list built with a constructor argument and a collection initializer takes one or the other
(#564): `new List<int>(10) { 1, 2 }` is `[]`, and `new List<int>(source) { 3 }` is
`source, [3]`, a second declarator in a declaration and a second argument in a call. The
target-typed form keeps the elements and drops the source, and `List<int> c = new(source)` is empty.
- `ToDictionary` refuses every comparer, `StringComparer.Ordinal` included, which the comparer fence
every collection's construction passes lets through (#578). `GroupBy` refuses every one too,
`ToLookup(k, comparer)` calls the comparer as an element selector, and `Distinct` drops whatever
comparer it is handed, so `Distinct(StringComparer.OrdinalIgnoreCase)` keeps "a" and "A".
- A range over a type with a `Length` and a `Slice(int start, int length)` calls JavaScript's
`slice(start, end)` on the twin, whose `slice` is that `Slice` (#585): `new Strip()[1..3]` is two
elements in .NET and three in the browser, and `[3..1]` slices where .NET's `Slice(3, -2)` throws.
- A twin read through `IReadOnlyList<T>` or `IList<T>` reads a JavaScript subscript and a `length`,
which a twin does not answer (#586): `r[0] + r[2] + r.Count` over a twin is 19 in .NET and null in
the browser.
- `ICollection<T>`'s `Add` and `Clear` are an array's `push` and `splice`, which a `HashSet`, a
`LinkedList`, a `SortedSet` and a dictionary's pairs behind the interface lack (#593): `c.Add(2)`
throws `c.push is not a function`.

## What Changes

- A `List<T>` construction is one array, as C# builds it: what its constructor copies, spread, then
each element of its collection initializer in order; an object initializer is applied to it once
built. The explicit and the target-typed forms share it.
- `ToDictionary`, `ToLookup`, `GroupBy` and `Distinct` hand their comparer to the fence a 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 fails the build.
- A range over a type eqc writes calls the member the bound tree names: its `Slice` with the start and
the length C# computes, its receiver evaluated once, then its endpoints in order, then its `Length` or
`Count`, read only where an endpoint counts from the end or the end is open. A range handed to an
indexer that takes the `Range` itself fails the build (EQ2004), since a System.Range value has no
translation. A range over a string, an array or a list keeps JavaScript's `slice`.
- An access through a list's face (`IList<T>`, `IReadOnlyList<T>`, `IList`) reads and writes through
the runtime's `$eq.collections.item` and `setItem`, which answer an array's subscript and a twin's
`item` and `setItem` alike, from the end too, and its `Count` through `$eq.collections.count`, which
reads a twin's own `count` before a `size` or a `length` it may also declare. A receiver typed as an
array or a list keeps its subscript.
- `ICollection<T>`'s own `Add` and `Clear` go through the runtime's `$eq.collections.add` and `clear`,
which add and empty as the collection behind the interface does, a member's collection initializer
included.
- No break for an app: its C# compiles to the new forms. The public surface of `eQuantic.UI.Compiler`
gains four `Eq` constants and `RangeIndexerStrategy` moves to the IR (`ConvertIr` in place of
`Convert`); the developer surface does not move.

## Capabilities

### New Capabilities

None.

### Modified Capabilities

- `transpiler-bcl`: a list built with an argument and an initializer, a list's face, and
`ICollection<T>`'s `Add` and `Clear`.
- `transpiler-sequences`: a LINQ operator handed a comparer.
- `transpiler-expressions`: a range over a type with a `Slice`.

## Impact

- eqc: `ObjectCreationStrategy` (the list), `LinqTableStrategy`, `GroupByStrategy`, `DistinctStrategy`
and the collection fence (comparers), `RangeIndexerStrategy` and `RangeExpressionStrategy` (ranges),
`Indexer`, `Place`, `CountSpelling` and `IndexFromEndStrategy` (the list's face), and
`ListMethodStrategy` and `ObjectInitializer` (`ICollection<T>`).
- The runtime: `add`, `clear`, `item` and `setItem` in `utils/collections.ts`, exposed on
`$eq.collections`, and `count` reading a twin's `count`. The runtime's transpiled components
regenerate where they read a list through its face.
- Tests: conformance cases for each issue, both sides executed, the twins through the module graph;
Compiler tests for the comparers EQ2007 refuses and the range EQ2004 refuses; runtime specs.
- Docs: docs/DIAGNOSTICS.md for EQ2004 and EQ2007, and one docs/LEDGER.md line.
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
## ADDED Requirements

### Requirement: A list built with an argument and an initializer holds both

A `List<T>` constructed with an argument and a collection initializer SHALL hold what .NET's holds:
a copy of the source, never the source itself, or nothing for a capacity, then each element of the
initializer in order, in the explicit and the target-typed forms alike, the source evaluated before the
elements. A target-typed list built from a source with no initializer SHALL be a copy of the source.

#### Scenario: A capacity and an initializer

- **WHEN** browser-side code builds `new List<int>(10) { 1, 2 }` and joins it
- **THEN** the answer is `1,2|2` with its count, as in .NET

#### Scenario: A source and an initializer

- **WHEN** browser-side code builds `var b = new List<int>(source) { 3 }` over `source = new List<int> { 1 }`, then runs `b.Add(4)`
- **THEN** `b` holds `1,3,4` and `source` still holds `1`, as in .NET

#### Scenario: A target-typed copy

- **WHEN** browser-side code builds `List<int> c = new(source)` over `source = new List<int> { 1 }` and runs `c.Add(9)`
- **THEN** `c` holds `1,9` and `source` one element, as in .NET

#### Scenario: A capacity is evaluated first

- **WHEN** browser-side code builds `new List<int>(Cap()) { At("a", 1), At("b", 2) }`, each call logging its step, and `new List<int>(-1)`
- **THEN** the log is `cab` before the list holds `1,2`, and the second throws an ArgumentOutOfRangeException, "Non-negative number required. (Parameter 'capacity')", as in .NET

### Requirement: A list's face reads and writes the list behind it

An element read or written through the indexer of `IList<T>`, `IReadOnlyList<T>` or `IList`, and a
`Count` read through such a face, SHALL answer for whatever the face holds when it runs: an array's or
a list's elements, and a twin's own indexer and count. A read, a write, a compound, a step, a
null-conditional read, an object initializer's entry and a key from the end SHALL each reach it, the
receiver and the key evaluated once and in C#'s order, and a write SHALL answer the value it wrote.

#### Scenario: A twin behind IReadOnlyList

- **WHEN** browser-side code computes `r[0] + r[2] + r.Count` over `IReadOnlyList<int> r` holding a twin of `{ 7, 8, 9 }`
- **THEN** the answer is `19`, as in .NET

#### Scenario: A twin behind IList

- **WHEN** browser-side code runs `l[0] = 5; l[1] += 2; l[2]++;` over `IList<int> l` holding a twin of `{ 1, 2, 3 }`
- **THEN** `l[0]`, `l[1]` and `l[2]` are `5`, `4` and `4`, as in .NET

#### Scenario: A twin with a Length beside its Count

- **WHEN** browser-side code reads `p.Count` and `p[^1]` over `IReadOnlyList<int> p` holding a twin of `{ 3, 4 }` that also declares `Length => 12.5` and `Size => 7`
- **THEN** the answers are `2` and `4`, as in .NET

#### Scenario: A face that holds null

- **WHEN** browser-side code reads `r.Count`, `r[0]` and `r?.Count` over `IReadOnlyList<int> r = null`
- **THEN** the first two throw a NullReferenceException, "Object reference not set to an instance of an object.", and the null-conditional answers null, as in .NET

### Requirement: ICollection's Add and Clear answer for the collection behind it

`ICollection<T>.Add` and `ICollection<T>.Clear` SHALL add and empty as the collection the interface
holds when the call runs does in .NET: a list appends, a set adds only a value it does not hold, a
linked list adds last, a dictionary adds the pair and refuses a key already there with .NET's message,
and a twin of the app's own runs its own `Add` and `Clear`. A collection initializer that adds to a
member typed as the interface SHALL add the same way.

#### Scenario: A set behind ICollection

- **WHEN** browser-side code runs `c.Add(2); c.Add(1);` over `ICollection<int> c = new HashSet<int> { 1 }` and reads `c.Count`
- **THEN** the count is `2`, as in .NET, where the call threw `c.push is not a function`

#### Scenario: A dictionary's pairs behind ICollection

- **WHEN** browser-side code adds the pair `("a", 2)` to `ICollection<KeyValuePair<string, int>>` holding a dictionary with the key `a`
- **THEN** it throws `An item with the same key has already been added. Key: a`, as in .NET
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
## ADDED Requirements

### Requirement: A range over a type with a Slice calls its Slice

A range used as an index over a type eqc writes, with a `Length` or a `Count` and a
`Slice(int start, int length)`, SHALL call that `Slice` as C# lowers the range: with the start and the
length computed from the endpoints and the count, the receiver evaluated 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 left open. A range handed to an indexer that takes the `Range` itself SHALL fail the build with
EQ2004, since a System.Range value has no translation. A range over a string, an array or a list SHALL
keep answering as it does.

#### Scenario: A range over a type with Slice

- **WHEN** browser-side code computes `new Strip()[1..3]` over a type whose `Slice(start, length)` copies five elements
- **THEN** the slice holds 2 elements, `2,3`, as in .NET, where `slice(1, 3)` held three

#### Scenario: The order a range is read in

- **WHEN** browser-side code computes `Strip.R(s)[^Strip.At("A", 3)..^Strip.At("B", 1)]`, each call logging its step and `Length` logging `L`
- **THEN** the log is `RABL` before the slice, as in .NET

#### Scenario: An indexer over Range

- **WHEN** a component computes `new Ranged()[1..3]` over a type that declares `this[Range r]`
- **THEN** the build fails with EQ2004 at the range
Loading
Loading