Skip to content

Commit d7a8c31

Browse files
🐛 fix: a nested class is a module of its own, named by its owner (#654)
Closes #584 ## What A class, a record or a struct declared inside another type is a module of its own in the browser, named by the types that contain it and its own name joined by `$` (`Cart$Item`, `A$B$C`), which no C# type can take, so it never meets a top-level type of its name. - Every reference to it, inside its owner or out, names that twin and imports it from there: a construction, a type test, a static member, an operator, a conversion, a zero and an annotation alike, inside an array or a generic too, through one name every strategy and emitter reads. - A nested static class is a module like any other, a component's included, where it was written into its owner's module unexported. - A nested type of an owner that never crosses (`[ServerOnly]`, an exception, an attribute) has no twin, as its owner has none. Client code that reaches its twin is refused with EQ2010 where it names the type, however it names it: a construction, a type test, a static (by its name, an alias or `using static`), a zero, an operator or an implicit conversion its value brings. Under those fences sits a net, the import every module makes: a twin the build writes no module for is reported there rather than dropped. The server publishes no route to a page declared in such an owner, and `MapPage` refuses one at startup. - A nested type of an owner the runtime provides is the runtime's, imported from `@equantic/runtime` under its twin name, as the runtime carries `CodeBlock$CodeMetrics`. - A record's text keeps its C# name (`Line { Qty = 2 }`), as .NET's does. - The server names the page it serves by the same rule, one file the compiler and the server share (`TwinName.OfType`), so a page declared inside a class loads the module the build wrote for it. The runtime's one nested type is `CodeBlock$CodeMetrics`. The developer surface does not move. `eQuantic.UI.Compiler`'s does: `UserDefinedOperators.Unary` and `Binary` take the conversion context, so the twin an operator names is imported, and `TypeSymbolExtensions.IntroduceTwin` is new. The same diagnostic at the same place is now reported once. ## Why Measured through the module graph an app's build writes, both sides executed: `Roster.First()` building a private nested `Row` answered `Ada` in .NET and threw in the browser, `new Cart().Size()` over a private `Cart.Item` with `Qty = 9`, beside a top-level `Item` with `Qty = 3`, was 9 in .NET and 3 in the browser, and a nested record wrote the module a top-level record of its name writes, one file over the other. ## OpenSpec `a-nested-class-is-its-owners`, archived in this pull request. It modifies `transpiler-classes` (a nested type is a module of its own, named by its owner, and a page declared inside a class is served under that name). ## Proof A/B against main, first at fc8f0fd with the 6 cases the pull request opened with (three shapes, each with and without type annotations), both sides executed through the module graph an app's build writes, and again against main as it was with #752 (7636da4), with the 10 cases `NestedTypeConformanceTests` holds now. Every one fails on main and passes here: a nested record beside a top-level one of its name stopped the build with EQ2009 (both were `Line`), a component's nested class threw, and a nested class beside a top-level class of its name answered the top-level class's 3 where .NET says 9. Copilot's first round found nine more paths a nested type took by its simple name or without its import (an operator, a conversion and a zero reached by inference, a nested component's type test and construction, and a component beside a nested class of its name): the four cases that cover them fail on the head before that round (ad5ded6) and pass here. Its second round found three more: a reference to a nested type of an owner that never crosses, the route to a page declared in one, and a nested type inside an array or a generic, which the annotation named by its simple name. The three tests that cover them fail on the head before that round (d54df5a) and pass here. Its third round, the last, found seven more. A nested type of an owner that never crosses was still reached through an alias, a static or a call through `using static`, an operator or an implicit conversion a value brings, and a zero (`default`, an array's fill, a field's or a record member's), each writing a twin nothing declares or a zero that was undefined. A nested type of an owner the runtime provides named a sibling module where the runtime carries it. And `MapPage` mapped a page declared inside an owner that never crosses. The 11 tests that cover them fail on the head before that round (10e0273) and pass here, and the 4 that came before pass on both. The suites on this branch's head, a tree the merge of main 7d3018c (#715) left as it was, each alone and read by its exit code and its total: Compiler 1855, Web 1021 against this branch's wiki pages, Server 372, the runtime's 2316 in 170 files (`tsc` and vitest through the embedded bun), and Conformance 4741 with 1 skipped. No run aborted. The served runtime is 218,555 gzipped bytes, 28 fewer than the 218,583 main serves, measured the same way, so this pull request costs it nothing. EQ2010's reporting sites include the record emitter's net. And it runs: the dashboard sample built from this branch's head serves a runtime that exports `CodeBlock$CodeMetrics`, the runtime's one nested type, and no `CodeMetrics`. `/markdown` rendered its code block and hydrated with no error in the console, and so did a client navigation to `/code` and back. ## 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`) - [x] 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), the other hosts are yours - [x] A broken contract has a line in the migration notes (we are in preview: break freely, hide nothing) - [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, and 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>
1 parent 7d3018c commit d7a8c31

56 files changed

Lines changed: 1427 additions & 202 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎docs/DIAGNOSTICS.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ not shrink with compiler work — they are the shape of the target.
5959
| `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. |
6060
| `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. |
6161
| `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. |
62-
| `EQ2010` | A symbol the framework keeps on the HOST — a type or a single member marked `[ServerOnly]` — named from client code. The runtime ships no twin, so the reference would compile, emit, and fail at hydration on "does not provide an export named" while SSR kept answering 200. | Call it from server code: a `[ServerAction]`, a `[ServerOnly]` class, or a realizer. Never from a component's `Build`. Unlike `EQ2004` this is a DECISION, not a gap — adding a strategy is not the answer. |
62+
| `EQ2010` | A symbol the framework keeps on the HOST — a type or a single member marked `[ServerOnly]` — named from client code. The runtime ships no twin, so the reference would compile, emit, and fail at hydration on "does not provide an export named" while SSR kept answering 200. A type declared inside one that never crosses (a `[ServerOnly]` class, an exception or an attribute) has no twin either, as its owner has none, so client code that reaches it is refused the same way, however it reaches it: its name, an alias, `using static`, an operator or a conversion its value brings, or a zero. A module that would import its twin reports it at the declaration that holds the reference (#584). | Call it from server code: a `[ServerAction]`, a `[ServerOnly]` class, or a realizer. Never from a component's `Build`. Unlike `EQ2004` this is a DECISION, not a gap — adding a strategy is not the answer. |
6363
| `EQ2012` | A `[ServerAction]` on an ABSTRACT component. The server registers an action under the name of the CONCRETE component carrying it — `ServerActionRegistry.ScanAssembly` skips abstract types and walks inherited methods — so it serves `Tile/Load` while a stub emitted on the base would invoke `CardBase/Load`. Nothing serves that id and the page gets "action not found", on both sides silently. | Declare the action on the concrete component. An alias is not the answer: the descriptor carries the component TYPE to invoke on, so two children inheriting one action give the base's id two answers — the owner of an inherited action is a decision, not a patch. |
6464
| `EQ2013` | A vocabulary type whose browser half revives the values of a type argument (`ServerTopic<T>`, its parameter marked `[HydratesTypeArgument]`) is built where that argument is, or holds, a type parameter: `static ServerTopic<T> Topic<T>(string name) => new(name)`. JavaScript erases the type there, so the topic would carry no spec and every payload would reach its subscribers unrevived, a decimal or a long as the text the wire writes. | Build the topic where its payload type is concrete, as a static member of the type that declares the app's topics: `static ServerTopic<RoomEvent> Room(string id) => new($"room:{id}")`. |
6565

‎docs/LEDGER.md‎

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2199,6 +2199,24 @@ record of a release, the wiki's Upgrading page is the distillate.
21992199
reason, most of them members lowered to JavaScript's own method, where refusing a null by name is a
22002200
guard at every call, a decision. Proposed and archived through OpenSpec
22012201
(`openspec/specs/transpiler-exceptions`).
2202+
- **2026-10-06 · A nested class is a module of its own, named by its owner**: a class declared inside
2203+
another one had no module, so `Roster.First()` building a private `Row` threw, and where a top-level
2204+
class had its name the nested one resolved to it, so `new Cart().Size()` read the top-level `Item`'s
2205+
3 for its own 9 ([#584](https://github.com/eQuantic/equantic-ui/issues/584)); a nested record wrote
2206+
the module a top-level record of its name writes, and the two were refused (EQ2009); a nested static
2207+
class outside a component had no module. Every class, static class, record and struct declared
2208+
inside another type is a module named by the types that contain it and its own name joined by `$`
2209+
(`Cart$Item`), which no C# type can take, and every reference to it, a construction, a type test, a
2210+
static member, an operator, a zero and an annotation alike, names that twin and imports it, through
2211+
one name the parser, the resolver and the emitters read; one inside a server-only class, an
2212+
exception or an attribute has none, and client code that reaches it, by a name, an alias, `using
2213+
static`, an operator or a zero, is refused with EQ2010, the import every module makes being the net
2214+
under the fences; one inside a type the runtime provides is the runtime's. A record's text keeps its
2215+
C# name. The runtime's one nested type is `CodeBlock$CodeMetrics`. The server names the page it
2216+
serves by the same rule, one file linked into both (`TwinName.OfType`): a page declared inside a
2217+
class asked the browser for a module named by its simple name, which nothing writes any more, and
2218+
one inside an owner that never crosses has no route and cannot be mapped. Proposed and archived through OpenSpec
2219+
(`openspec/specs/transpiler-classes`).
22022220

22032221
## Retired documents
22042222

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
schema: spec-driven
2+
created: 2026-10-06
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
# Design
2+
3+
## Context
4+
5+
eqc writes one module per type, named by the type, and a module that names another imports it as
6+
`import { X } from "./X"`. The name was the type's simple C# name everywhere: the parser's
7+
definitions, the dependency resolver's set of modules, the emitters' class declarations, and every
8+
strategy that writes a type into code (`new`, `instanceof`, a static member, an operator, a zero, an
9+
annotation, a hydration map). A nested type broke the scheme three ways: a plain class or a static
10+
class nested outside a component had no module, a component's nested static classes were written into
11+
the component's module unexported, and a nested record or struct had a module named like a top-level
12+
type, which could write the same file.
13+
14+
## Goals / Non-Goals
15+
16+
**Goals:**
17+
18+
- One name for a type's twin, read by everything that writes or resolves one.
19+
- A nested type is a module like any other, so the import machinery that exists serves it unchanged.
20+
21+
**Non-Goals:**
22+
23+
- A nested enum or interface: an enum lowers to its values and an interface to nothing.
24+
- Reflection's names (`GetType().Name`, `nameof`), which stay the C# names.
25+
26+
## Decisions
27+
28+
### `Owner$Nested`, a module of its own
29+
30+
The twin of a type declared inside another is named by the chain of the types that contain it and its
31+
own name joined by `$`: `Cart$Item`, `A$B$C`, generic arguments erased (`Box<T>.Node` is `Box$Node`).
32+
C# allows no `$` in a name, so the twin can never meet a top-level type's, and its module is named
33+
the same. Every reference writes that name and the module that holds the reference imports it from
34+
`./Cart$Item`, through the machinery a top-level type already takes.
35+
36+
Alternative: write the nested types into their owner's module, as a component's nested static
37+
classes were. Rejected: every emitter would need to merge another type's code and imports into its
38+
own, a reference from outside the owner would need the owner's module to re-export it, and the four
39+
emitters would each carry a second shape of module. Alternative: the owner's static property
40+
(`Cart.Item`). Rejected: TypeScript cannot read a value as a type, so every annotation of a nested
41+
type would be `any`, and the owner's module would have to be loaded before its nested type's.
42+
43+
### One name, read everywhere
44+
45+
A type symbol's twin name (`TwinName`) and a declaration's (the same walk over its parents, for a
46+
host with no model) replace the simple name in the parser, the dependency resolver's scan, the
47+
emitters' class declarations and the strategies that write a type. A record's text keeps the C# name
48+
it prints, as .NET's `PrintMembers` writes it, and so do the diagnostics, which speak C#.
49+
50+
How Flutter answers it: Dart has no nested classes, so a library writes each class at its top level
51+
under a name of its own, and a private one takes a leading `_`. The `$` is the same idea in a name C#
52+
cannot spell.
53+
54+
### Who has a twin
55+
56+
A nested type has a twin where a top-level type of its kind would, and where its owner crosses: a
57+
nested type of a `[ServerOnly]` owner, an exception or an attribute has none, as its owner has none,
58+
whatever it is itself. A component's nested static class is a static helper's module like any other.
59+
60+
## Risks / Trade-offs
61+
62+
- [A module file whose name holds a `$`] → the bundler, the server's module route and every host's
63+
file system take it; the conformance suite runs the bundled graph, and the dashboard sample serves
64+
it.
65+
- [A hand-written runtime file naming `CodeMetrics`] → none: only the transpiled `CodeBlock` reads it,
66+
and it regenerates.
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
# Proposal
2+
3+
Closes #584, a sub-issue of #565 (the transpiler's fences hold on every path, continued). It builds on
4+
the class construction of #583, whose branch this one follows.
5+
6+
## Why
7+
8+
A type declared inside another one is its owner's scope in C#, and eqc had no single answer for it,
9+
measured through the module graph an app's build writes, both sides executed:
10+
11+
- A nested class that is not a component's static helper got no module, so constructing it named a
12+
class the browser does not have: `Roster.First()` building a private `Row` answered "Ada" in .NET
13+
and threw in the browser.
14+
- Where a top-level class has the nested one's name, the nested one resolved to it:
15+
`new Cart().Size()` over a private `Cart.Item` with `Qty = 9`, beside a top-level `Item` with
16+
`Qty = 3`, was 9 in .NET and 3 in the browser.
17+
- A nested record or struct got a module named by its simple name, which a top-level type of that
18+
name writes too, one file over the other.
19+
- A nested static class outside a component got no module, and one inside a component was written
20+
into the component's module under its simple name.
21+
22+
## What Changes
23+
24+
- Every class, record and struct declared inside another type has a twin of its own, named by the
25+
chain of the types that contain it and its own name, joined by `$` (`Cart$Item`, `A$B$C`): a name no
26+
C# type can take, so it never meets a top-level type's. Its module is named so, and every reference
27+
to it, inside its owner or out, a construction, a type test, a static member, a default and an
28+
annotation alike, names it so, importing it from there.
29+
- A nested type of an owner that never crosses (`[ServerOnly]`, an exception, an attribute) has none,
30+
as its owner has none: client code that names it is refused with EQ2010, and the server publishes no
31+
route to a page declared in it.
32+
- A record's text still prints its C# name (`Line { Qty = 2 }`), as .NET's does.
33+
- The runtime's one nested type, `CodeBlock.CodeMetrics`, is written as `CodeBlock$CodeMetrics`. The
34+
developer surface does not move. `eQuantic.UI.Compiler`'s does: `UserDefinedOperators.Unary` and
35+
`Binary` take the conversion context, so the twin an operator names is imported, and
36+
`TypeSymbolExtensions.IntroduceTwin` is new.
37+
38+
## Capabilities
39+
40+
### New Capabilities
41+
42+
None.
43+
44+
### Modified Capabilities
45+
46+
- `transpiler-classes`: a nested class, record or struct is a module of its own, named by its owner.
47+
48+
## Impact
49+
50+
- eqc: one name for a type's twin (its containing types and its own name), which the parser, the
51+
dependency resolver, every emitter and every strategy that names a type in the code it writes read;
52+
the parser discovers nested types as modules; a component no longer writes its nested static
53+
classes into its own module.
54+
- The runtime: `CodeMetrics` regenerates as `CodeBlock$CodeMetrics`.
55+
- Tests: conformance cases through the module graph for each shape, both sides executed.
56+
- Docs: the wiki's SupportedFeatures page (English and Portuguese) and one docs/LEDGER.md line.
Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
## ADDED Requirements
2+
3+
### Requirement: A nested type is a module of its own, named by its owner
4+
5+
Every class, record and struct declared inside another type SHALL have a twin of its own, in a module
6+
of its own, named by the chain of the types that contain it and its own name joined by `$`
7+
(`Cart$Item`), which no C# type can take. Every reference to it, inside its owner or out, SHALL name
8+
that twin: a construction, a type test, a static member, a default and an annotation alike. A nested
9+
type of an owner that never crosses (`[ServerOnly]`, an exception, an attribute) SHALL have none, and
10+
client code that reaches its twin SHALL be refused: one that builds it, tests for it, reads its statics,
11+
takes its zero or calls an operator or a conversion its value brings, however the code names the type
12+
(an alias, `using static`, inference). A module that would import such a twin SHALL report it rather
13+
than import it. A nested type of an owner the runtime provides SHALL be the runtime's,
14+
imported from it under its twin name. A record's text SHALL print its C# name, as .NET's does. The server SHALL name the page it serves by the
15+
same rule, so a page declared inside a class loads the module the build wrote for it, and SHALL refuse to
16+
map a page declared inside an owner that never crosses.
17+
18+
#### Scenario: A nested class beside a top-level class of its name
19+
20+
- **WHEN** `class Item { public int Qty = 3; }` and `class Cart { private class Item { public int Qty = 9; } public int Size() => new Item().Qty; }`
21+
- **THEN** `new Cart().Size()` is `9`, as in .NET
22+
23+
#### Scenario: A private nested class built by its owner
24+
25+
- **WHEN** `class Roster { private sealed class Row { public string Name = ""; } public static string First() { var r = new Row { Name = "Ada" }; return r.Name; } }`
26+
- **THEN** `Roster.First()` is `Ada`, as in .NET
27+
28+
#### Scenario: A nested record beside a top-level record of its name
29+
30+
- **WHEN** `class Shop { public record Line(int Qty); }` and `record Line(string Text);`
31+
- **THEN** `new Shop.Line(2).Qty` is `2`, `new Line("x").Text` is `x`, and `new Shop.Line(2)` prints `Line { Qty = 2 }`, as in .NET
32+
33+
#### Scenario: A nested static class of a plain class
34+
35+
- **WHEN** `class Calc { static class Ops { public static int Twice(int x) => x * 2; } public int Run(int x) => Ops.Twice(x); }`
36+
- **THEN** `new Calc().Run(4)` is `8`, as in .NET
37+
38+
#### Scenario: A public nested type from outside its owner
39+
40+
- **WHEN** `class Outer { public class Inner { public int N = 4; } }` and `object o = new Outer.Inner();`
41+
- **THEN** `((Outer.Inner)o).N` is `4` and `o is Outer.Inner` is true, as in .NET
42+
43+
#### Scenario: Two levels of nesting
44+
45+
- **WHEN** `class A { public class B { public class C { public int V = 1; } } }`
46+
- **THEN** `new A.B.C().V` is `1`, as in .NET
47+
48+
#### Scenario: A nested type of a server-only owner
49+
50+
- **WHEN** `[ServerOnly] class Vault { public class Key { } }`
51+
- **THEN** the build writes no module for `Key`, as it writes none for `Vault`, client code that builds a
52+
`Vault.Key` or tests for one is refused with EQ2010, and a page declared inside `Vault` has no route,
53+
and mapping it with `MapPage` fails at startup
54+
55+
#### Scenario: A kept-out nested type the syntax never names
56+
57+
- **WHEN** `[ServerOnly] class Vault` holds `Key`, `Pair` and `Amount`, and client code reads `K.Count` through
58+
`using K = Vault.Key;`, reads `Count` or calls `Make()` through `using static Vault.Key;`, asks for
59+
`default(Vault.Pair)`, declares a field of type `Vault.Pair`, or adds or converts a `Vault.Amount` that
60+
another class's method returns
61+
- **THEN** each is refused with EQ2010, and no module imports a twin of `Vault`'s
62+
63+
#### Scenario: A nested type of a runtime-provided owner
64+
65+
- **WHEN** `[RuntimeProvided] class Kit { public class Part { } }` and client code builds a `Kit.Part`
66+
- **THEN** the module imports `Kit$Part` from the runtime, which carries it as it carries
67+
`CodeBlock$CodeMetrics`, and the build writes no module for it
68+
69+
#### Scenario: A page declared inside a class
70+
71+
- **WHEN** `public static class Admin { [Page("/admin/users")] public class Users : StatelessComponent { … } }` is served
72+
- **THEN** the page's configuration and its route name `Admin$Users`, the module the build wrote
73+
74+
#### Scenario: A nested component, tested and built by its twin
75+
76+
- **WHEN** `class Host { public class Page : StatelessComponent { … } }`, `object o = new Host.Page();` and `Host.Page p = new();`
77+
- **THEN** `o is Host.Page` tests `Host$Page`, and both constructions build `Host$Page`, imported where it is used
78+
79+
#### Scenario: A component beside a nested class of its name
80+
81+
- **WHEN** `class Item : StatelessComponent { … }` and `class Cart { public class Item { } }`
82+
- **THEN** `Item` is the component's module and `Cart$Item` a plain class's, neither taken for the other
83+
84+
#### Scenario: A nested type the syntax never names
85+
86+
- **WHEN** `Factory.Make() + Factory.Make()`, `-Factory.Make()` and `total += Factory.Make()` bind the operators of a
87+
nested `Outer.Amount` that no expression names, and `default(Alias)` is the zero of a nested struct through
88+
`using Alias = Outer.Pair;`
89+
- **THEN** each module imports `Outer$Amount` and `Outer$Pair`, as it imports a twin the syntax names
90+
91+
#### Scenario: A nested type inside an array or a generic
92+
93+
- **WHEN** a member is typed `A.Inner[]`, `List<A.Inner>` or `Func<A.Inner, B.Inner>`, in a class or a record
94+
- **THEN** its annotation names `A$Inner` and `B$Inner`, each where its type stands, and a top-level `Inner`
95+
beside them keeps its own name
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
# Tasks
2+
3+
## 1. One name for a twin
4+
5+
- [x] 1.1 A type's twin name, from its symbol and from its declaration: the chain of its containing types and its own name joined by `$`, generic arguments erased. Verified by Compiler tests for a top-level, a nested, a doubly nested and a generic owner's type
6+
- [x] 1.2 Every strategy and emitter that writes a type into code names its twin: a construction, a type test, a static member, an operator, a conversion, a zero, a `with`, an annotation, a hydration map and a class declaration. Verified by the conformance cases of group 2 and the Compiler suite
7+
8+
## 2. A nested type is a module
9+
10+
- [x] 2.1 The parser and the dependency resolver take a nested class, static class, record and struct as a module named by its twin, and none for one whose owner never crosses; a component's nested static classes are modules like any other. Verified by conformance cases through the module graph (the two of #584, a nested record beside a top-level one, a nested static class, a public nested type from outside, two levels), failing before and green here, and a Compiler test for the server-only owner
11+
12+
## 3. The runtime and the real thing
13+
14+
- [x] 3.1 Regenerate the runtime's transpiled classes (`CodeBlock$CodeMetrics`) and run `dotnet build src/eQuantic.UI.Runtime -t:TestRuntime`, the Server suite and the Web suite
15+
- [x] 3.2 The dashboard sample in a browser: the code screens, whose `CodeBlock` builds a nested record, render with no console error
16+
17+
## 4. Documentation and archive
18+
19+
- [x] 4.1 The wiki's SupportedFeatures page in English and Portuguese, on the wiki branch named like this change's branch, and one docs/LEDGER.md line citing #584
20+
- [x] 4.2 `./scripts/check-openspec.sh` green, then `openspec archive a-nested-class-is-its-owners --yes` before the merge

0 commit comments

Comments
 (0)