Skip to content

Let a class declaration be for a type rather than of one - #53

Merged
matt-edmondson merged 2 commits into
mainfrom
claude/blissful-euler-fzuap2
Sep 12, 2026
Merged

Let a class declaration be for a type rather than of one#53
matt-edmondson merged 2 commits into
mainfrom
claude/blissful-euler-fzuap2

Conversation

@matt-edmondson

Copy link
Copy Markdown
Contributor

A generated reflection table has to be reachable from the type it describes, and C++ does that by specialising a template on it:

template <>
struct Describe<holo::components::RigidBody>
{
    static constexpr ComponentInfo info{ /* ... */ };
};

The AST could already say everything such a table contains — namespace-scope inline constexpr arrays of braced lists, designated initialisers, nested lists one per line — and could not say the declaration that anchors them. That made the whole artefact unreachable from the thing it is about.

The alternative was naming. A DescribeRigidBody beside RigidBody works, and it is exactly what a lookup by type exists to avoid: every consumer has to spell the convention for itself, and nothing checks that it got it right.

The change

ClassDeclaration gains SpecialisationArguments, an ordered list that is empty on an ordinary declaration:

ClassDeclaration describe = new("Describe")
{
    Kind = TypeDeclarationKind.Struct,
    SpecialisationArguments = { new TypeReference("holo::components::RigidBody") },
};

Empty is what makes this safe to add. IsSpecialisation is false for every type this library has ever emitted, so nothing moves.

Why the arguments are types and not text

This is the one place it departs from CompileTimeAssertion's precedent. An assertion's condition is opaque to everything but the compiler, so carrying it as a string costs nothing. A specialisation argument is a type — the AST already knows how to be one, and the comma in Result<Handle, Error> belongs to that type rather than separating two arguments. Text would have to guess; a list does not, and there's a test for exactly that case.

One generator honours it, three say what it was

Language Output
C++ template <> then struct Describe<holo::components::RigidBody>
C# // specialised for holo::components::RigidBody above an ordinary struct Describe
Python # specialised for holo::components::RigidBody
JavaScript // specialised for holo::components::RigidBody

A generated file that quietly drops what it was for looks like one that still means it. That is now the second node with this shape, and the reason is the same both times.

Tests

Seven, and the one that matters as much as the positive cases is Cpp_WritesAnOrdinaryDeclarationWithNone — a declaration with no arguments still writes no template. Unconditional would have broken every type this library emits. Also covered: argument order, a nested generic argument kept whole, the comment in all three other languages, YAML round-trip (nested argument included), clone independence, and that an ordinary declaration serialises no key at all.

Full suite: 503 passed.

🤖 Generated with Claude Code

https://claude.ai/code/session_01UGHDsYaaTQdzVR4XBR6miu


Generated by Claude Code

A generated reflection table has to be reachable from the type it describes, and
C++ does that by specialising a template on it: template<> struct
Describe<RigidBody>. The AST could say everything such a table contains -
namespace-scope inline constexpr arrays of braced lists, designated initialisers,
nested lists - and could not say the declaration that anchors them, which made
the whole artefact unreachable from the thing it is about.

The alternative was naming. A DescribeRigidBody beside RigidBody works, and it
is exactly what a lookup by type exists to avoid: every consumer has to spell the
convention for itself, and nothing checks that it got it right.

ClassDeclaration gains SpecialisationArguments, an ordered list that is empty on
an ordinary declaration. Empty is what makes this safe to add: IsSpecialisation
is false for every type this library has ever emitted, so nothing moves.

The arguments are TypeReference rather than text, which is the one place this
departs from CompileTimeAssertion's precedent. An assertion's condition is opaque
to everything but the compiler, so a string costs nothing; a specialisation
argument is a type, the AST already knows how to be a type, and the comma in
Result<Handle, Error> belongs to that type rather than separating two arguments -
which text would have to guess at, and a list does not.

Only C++ honours it. The other three write the type it was specialised for as a
comment above an ordinary declaration, because a generated file that quietly
drops what it was for looks like one that still means it. That is now the second
node with this shape and the reason is the same both times.

Seven tests, and the one that matters as much as the positive cases is that a
declaration with no arguments still writes no template: unconditional would have
broken every type this library emits.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UGHDsYaaTQdzVR4XBR6miu
Comment thread Coder/Serialization/YamlDeserializer.cs Fixed
A null or empty entry in the document is not an argument, and saying so in a
Where rather than in an if inside the loop leaves the loop doing one thing.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UGHDsYaaTQdzVR4XBR6miu
@sonarqubecloud

Copy link
Copy Markdown

@matt-edmondson
matt-edmondson merged commit 23ca8d5 into main Sep 12, 2026
12 checks passed
@matt-edmondson
matt-edmondson deleted the claude/blissful-euler-fzuap2 branch September 12, 2026 03:49
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant