Generate the reflection table beside the C++ headers - #173
Conversation
A generated struct carries a name, a type and an order. Everything else the schema says about a member - its unit, its range, whether it wraps, whether two states of it can be blended, how it is quantised on the wire, what an editor should draw - the header can only put in a comment, which is to say it cannot say it at all. CppGeneratorOptions.Reflection is where those facts become data. Two files. `reflect` is the vocabulary and `reflection` is the table, and the option is off by default because they are two files a target that does not read them did not ask for. The offsets are the compiler's. The table says offsetof(Class, member) and sizeof(Class::member), so the numbers are whatever the compiler chose for that target, that ABI, those packing rules - and reflection therefore cannot drift from the layout it describes, because it is derived from it by the only thing that knows. A generator that worked offsets out itself would be a second implementation of the C++ ABI and would be wrong somewhere eventually. One of the tests asserts that no literal offset appears at all. A member's kind and its representation are both carried, and one field could not have done both. `kind` is what the schema declares - Semantic for a member typed Kilograms - and `representation` is what the bytes are, with a semantic type followed down its chain of refinement to Float. Reading a value out of a save file needs the second; showing the member to a person wants the first, because "Kilograms" is the answer and "Float" is not. The dimension comes from the unit rather than beside it. The member's unit text resolves through UnitRegistry and the unit's own DimensionInfo supplies the eight exponents, so the numbers cannot disagree with the unit written next to them. `reflect` is shipped rather than generated, for the same reason ktsu.Semantics.Cpp ships its prelude: `template <typename T> struct Describe;` declares a type parameter and `concept Reflected` is a concept, and the AST models neither - a generator names a generic type, it never declares one. Two lists in it are substituted, though, and that is the point of substituting rather than copying: TypeKind is every [JsonDerivedType] on BaseType and Interpolation is the enum of that name, so a type added to the schema appears in C++ with no edit in either repository. Each is written twice, as the enumeration and as the names beside it, from one list - because the alternative is a switch, which the AST cannot say and which nothing would then keep in step. What reads the table is written once, by hand. A validator, a serialiser, a network codec and an editor all walk it rather than each being a generator with its own copy of the same facts, which is also what lets them work on a schema loaded at run time. The rule that suggests: generate what has to be a type, and write by hand what only needs to read a type's description. Thirteen tests. The one that matters is that the table compiles and holds: a program of static_asserts over a plain-types schema, checked with -fsyntax-only so it needs no run, asserting the two things text assertions cannot - that an offset in the table equals offsetof of the same member, and that describe<T>() finds the right descriptor by type. Requires ktsu.Coder 3.7.0 for ClassDeclaration.SpecialisationArguments, which is what makes template<> struct Describe<Class> expressible. Without it the table would have to be a DescribeRigidBody every consumer spells for itself, which is what a lookup by type exists to avoid. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UGHDsYaaTQdzVR4XBR6miu
|
The failure is 3.7.0 is the release carrying ktsu-dev/Coder#53, which this PR needs for It has indexed now. Re-running the failed jobs is refused while the other platforms in this run are still going ( Generated by Claude Code |
A member that names no enum contributes nothing to the values tables, and saying that as an empty sequence rather than as an if inside the loop leaves the loop doing one thing to everything it is handed. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UGHDsYaaTQdzVR4XBR6miu
The vocabulary's TypeKind is every [JsonDerivedType] on BaseType, read off the attribute itself. On net10.0 that type is the shared framework's and needs no reference; on net9.0 it arrives transitively, and KTSU0006 is right that a project using a package directly should say so. Only the net9.0 leg failed, and only in CI, because the local verification built one framework rather than both. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UGHDsYaaTQdzVR4XBR6miu
|



A generated struct carries a name, a type and an order. Everything else the schema says about a member — its unit, its range, whether it wraps, whether two states of it can be blended, how it is quantised on the wire, what an editor should draw — the header can only put in a comment, which is to say it cannot say it at all.
CppGeneratorOptions.Reflectionis where those facts become data. Two files:reflect(the vocabulary) andreflection(the table). Off by default, because they are two files a target that does not read them did not ask for.The offsets are the compiler's
offsetof(Class, member)andsizeof(Class::member), so the numbers are whatever the compiler chose for that target, that ABI, those packing rules. Reflection therefore cannot drift from the layout it describes — it is derived from it, by the only thing that knows. A generator that worked offsets out itself would be a second implementation of the C++ ABI and would be wrong somewhere eventually.AsksTheCompilerForTheLayoutasserts both halves: thatoffsetofis there, and that no literal offset is.kindandrepresentationare both carriedOne field could not do both jobs.
kindis what the schema declares —Semanticfor a member typedKilograms.representationis what the bytes are, with a semantic type followed down its chain of refinement toFloat.Reading a value out of a save file needs the second. Showing the member to a person wants the first, because "Kilograms" is the answer and "Float" is not.
The dimension comes from the unit
The member's unit text resolves through
UnitRegistry, and the unit's ownDimensionInfosupplies the eight exponents — so the numbers cannot disagree with the unit written next to them.m/sgives{ 1, 0, -1, 0, 0, 0, 0, 0 }; a member that measures nothing is dimensionless, which is the same shape rather than a missing one.reflectis shipped, and two of its lists are notSame reason
ktsu.Semantics.Cppships its prelude:template <typename T> struct Describe;declares a type parameter andconcept Reflectedis a concept, and the AST models neither — a generator names a generic type, it never declares one.Two lists in it are substituted, and that is the point of substituting rather than copying:
TypeKindis every[JsonDerivedType]onBaseType— 24 enumerators today.Interpolationis the enum of that name.So a type added to the schema appears in C++ with no edit in either repository. Each is written twice — as the enumeration and as the names beside it — from one list, because the alternative is a
switch, which the AST cannot say and which nothing would then keep in step. A test counts both against the attribute count.What reads the table is written once, by hand
A validator, a serialiser, a network codec and an editor all walk it rather than each being a generator with its own copy of the same facts — which is also what lets them work on a schema loaded at run time. The rule that suggests: generate what has to be a type, and write by hand what only needs to read a type's description.
Tests
Thirteen new, 67 in the project. The one that matters is
TheTableCompilesAndSaysWhatItWasGeneratedToSay— a program ofstatic_asserts over a plain-types schema, checked with-fsyntax-onlyso it needs no run, asserting the two things text assertions cannot reach:That the offset in the table equals
offsetofof the same member, and thatdescribe<T>()finds the right descriptor by type, are exactly what asserting on generated text cannot check.Schema.Test: 413 passed, unchanged. Verified locally against a project reference to the merged
ktsu.Coderbefore the pin was set.Radiancomes out dimensionless in the table, because this repo pinsktsu.Semantics.Quantities 4.0.0, which predates theangleaxis added in ktsu-dev/Semantics#214. The table is correct for the version in use; the angle exponent arrives when that pin moves, which is separate work.Requires
ktsu.Coder 3.7.0For
ClassDeclaration.SpecialisationArguments, which is what makestemplate<> struct Describe<Class>expressible. Without it the table would have to be aDescribeRigidBodyevery consumer spells for itself — which is what a lookup by type exists to avoid.🤖 Generated with Claude Code
https://claude.ai/code/session_01UGHDsYaaTQdzVR4XBR6miu
Generated by Claude Code