Skip to content

Say what a constant table is, so a reflection table can be one - #50

Merged
matt-edmondson merged 1 commit into
mainfrom
claude/holotype-cpp-schema-integration-8nzvut
Sep 11, 2026
Merged

Say what a constant table is, so a reflection table can be one#50
matt-edmondson merged 1 commit into
mainfrom
claude/holotype-cpp-schema-integration-8nzvut

Conversation

@matt-edmondson

Copy link
Copy Markdown
Contributor

Holotype's docs/generated-cpp-target.md closes by saying the other backends "become the same exercise once the vocabulary exists". A spike that tried to build the reflection table generate_reflection.cpp actually emits says otherwise — a header of structs and a table of rows need different things, and four of them were missing.

What was missing

Needed for Was
FieldInfo kFields[] no way to say a declaration is an array
inline constexpr no storage or constness on FieldDeclaration
= { … } a construction always wrote its type name first
.name = "mass" MemberInitialiser threw NotSupportedException outside a constructor's initialiser list

What this adds

TypeReference.IsArray — only that it is an array, with no bound. A bound would have to be an expression rather than a number and nothing asks for one; and where the brackets go is the generator's business, since C++ is the one language here that puts them on the declarator (T name[]) rather than on the type (T[] name). Parse/ToString read and write [] so the round trip still holds.

FieldDeclaration.IsStatic and .IsConstant — the intent rather than the keyword, because no two of these languages spell it the same way and one of them spells it differently depending on where the field sits:

namespace scope inside a type
C++ inline constexpr static constexpr
C# static readonly static readonly
Python, JavaScript omitted omitted

inline is what makes a namespace-scope constant safe to define in a header, which is the only place a generated one appears; a static data member is already implicitly inline, so saying it inside a type would be noise. The generator tracks that with a depth rather than a flag so a type nested in a type stays balanced.

A braced list. A ConstructionExpression with no type is the list alone — what initialises a declaration that has already named its type, an array of rows most of all, where naming the array's type again would be wrong rather than merely redundant. A MemberInitialiser among its arguments is an element that names its member: .name = v in C++, Name = v in C#, name=v in Python, { name: v } in JavaScript. The last of those is the options-object convention rather than a translation — it is the only shape in that language where the names survive at all, and the remark on the method says so.

Stacking. A list whose elements are themselves lists is a table, and a table written on one line is a diff nobody can read, so it goes one element per line with a trailing comma. A list of plain values is a value and stays where it is. The test is the shape of the data rather than a column count, because a generated file has no idea how wide anyone's editor is.

Result

inline constexpr holo::reflect::FieldInfo kRigidBodyFields[] = {
    holo::reflect::FieldInfo{ .name = "velocity", .offset = offsetof(holo::components::RigidBody, velocity), .lerp = true },
    holo::reflect::FieldInfo{ .name = "mass", .offset = offsetof(holo::components::RigidBody, mass), .lerp = false },
};

ExemplarReflectionTableTests builds that and asserts it, alongside ExemplarHeaderTests — written against the real table rather than a toy, so it fails when a generated table would.

What is still not expressible

The template<> struct Describe<T> specialisation below that table. An explicit specialisation is C++ and only C++, and whether this AST should learn it or the generator should write it is the same question static_assert raised, with the same answer available. Recorded in the new test's remarks and in the Holotype plan rather than decided here.

One behaviour change worth naming

IsPunctuation now stops a name at [ or ], or int[] would parse as a type named int[] rather than an array of int. Text the grammar still cannot read whole — int[3] — falls back to a verbatim name exactly as before, so nothing becomes lossy.

Testing

dotnet build -c Release clean, 0 warnings. Full suite 496/496 on net10.0 (this container has no .NET 9 runtime, so that leg cannot launch here — CI has both). AstFields gained the two flags so the editor's inspector can still edit every property a field has, and both round-trip through YAML.

🤖 Generated with Claude Code

https://claude.ai/code/session_0165XQS7s11ssp36AEY8tvCF


Generated by Claude Code

[minor]

The target document says the other backends "become the same exercise once the
vocabulary exists". They are not. A header of structs and a table of rows need
different things, and a spike that tried to build the reflection table
generate_reflection.cpp emits found four of them missing: there was no way to
say a declaration was an array, no way to say its value was fixed where it was
written, no way to write a braced list without naming a type in front of it,
and no way for an element to say which member it was for.

So TypeReference gains IsArray - only that it is one, with no bound, because
where the brackets go is the generator's business and C++ is the one language
here that puts them on the declarator rather than the type. FieldDeclaration
gains IsStatic and IsConstant, the second of which is the intent rather than
the keyword, since C++ says inline constexpr at namespace scope and static
constexpr inside a type while C# says static readonly and Python says nothing
at all. A ConstructionExpression with no type is the braced list alone, and a
MemberInitialiser among its arguments is an element that names its member:
a designated initialiser in C++, an object initialiser in C#, a keyword
argument in Python, an object literal in JavaScript.

A list whose elements are themselves lists is a table rather than a value, so
it is written one element per line with a trailing comma. A table on one line
is a diff nobody can read, and a generated file is read.

What is still not expressible is the explicit template specialisation that
sits below that table. That is C++ and only C++, and whether the AST should
learn it or the generator should write it is the same question static_assert
raised, with the same answer available.

Also: a name may no longer swallow a bracket, or int[] would parse as a type
called "int[]" rather than an array of int.

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

Copy link
Copy Markdown

@matt-edmondson
matt-edmondson merged commit 6e7711d into main Sep 11, 2026
12 checks passed
@matt-edmondson
matt-edmondson deleted the claude/holotype-cpp-schema-integration-8nzvut branch September 11, 2026 08:43
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.

2 participants