Name the quantity, not the unit - #189
Merged
Merged
Conversation
A schema that wanted a mass had to declare one. It wrote a semantic type called Kilograms, put "kg" on it as metadata, and every member of it said the unit twice - once as a name nothing reads and once as text something does. The kilograms are how the stored number is read, which is a fact about the field; the mass is what the value is. ktsu.Semantics already names all of it. 212 physical quantities in C#, the same 212 as C++ classes from ktsu.Semantics.Cpp, and both are things a target already has. So Quantity names one rather than asking for a copy of it - Quantity(Mass), Quantity(Velocity3D), Quantity(Ratio) - and nothing is generated for it. That is the whole difference from Semantic, and both are wanted: a semantic type is the schema's own, a quantity is everybody's. QuantityRegistry reads the vocabulary out of the assembly, the same arrangement UnitRegistry has. A quantity's dimension takes three routes, in order: a magnitude declares one through IPhysicalQuantity; a vector form does not, so its dimension is what Magnitude() answers with; and a named overload of a vector form answers neither, so what is followed is the implicit widening onto what it is an overload of. Six of the 212 are reachable only by that third route, and 212 is not a number chosen here - it is what the C++ projection emits, so the two counts agreeing is what says a schema can name every quantity a C++ target has. The ten it leaves out are the logarithmic scales and two hand-written audio types: a decibel does not add and a pH does not scale, so neither has a dimensional formula for a reflection table to carry. Three things follow from a quantity knowing its own dimension. A unit on one has to agree with it - the check the semantic type could never make, since a unit is text and Kilograms was a name. The exponents are compared and not the names, because 72 of the vocabulary's dimensions share 63 exponent vectors: a joule and a newton metre are the same eight numbers, and refusing what the physics allows is worse than not checking. The reflection table asks the type first. Its eight exponents used to come from the member's unit text and nowhere else, so a member that measured something but named no unit was written down as dimensionless, indistinguishable from a flag. Storage is the rest of the type, since every quantity is generic over one. It defaults to Float and is omitted from the file when it is, exactly as a vector's ElementType is, and it is held to a bare numeric: a generated semantic type is a record struct over a float implementing no INumber<T>, so Mass<Kilograms> is not a type anything could write. C# spells it ktsu.Semantics.Quantities.Mass<float> and ClrTypeImporter reads it straight back off the closed type - the one thing generated C# carries that needs no attribute recording what it is. Everything else does, because a sequential struct or a record struct over a float is a shape a hand-written type may have for its own reasons; a Mass<float> is not something a target happened to write, it is the quantity. C++ spells it wherever the target's vocabulary went, which CppGeneratorOptions.Quantities says in one entry for all 212 - the target named none of them, it ran the generator. That entry also says what the vocabulary is stored in, because a C++ quantity is a class rather than a template and the storage was fixed when it was generated: a member the schema keeps in a double is refused on a float target rather than emitted as two languages quietly disagreeing about the bytes. Format version 7. Unlike every step since version 1 this is not something an older reader drops - it fails to deserialize the member at all, on a discriminator it has never heard of - so the version moves to say so instead. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UGHDsYaaTQdzVR4XBR6miu
The sample declared Kilograms, Metres, MetresPerSecond and MetresPerSecondSquared: four semantic types named after units, each also carrying the unit as metadata. That states the presentation twice and makes the wrong copy load-bearing. A weight is a mass and the kilograms are how its number is read, so the member now says Quantity(Mass) with "kg" beside it - each thing once, and validation notices when the two disagree. Coin stays a semantic type, and that is the point of keeping it. A price is this schema's own idea and nothing outside it has heard of a coin, so the schema declares one and a generator emits it; a mass is everybody's, so the schema names it and nothing is emitted at all. The two ways of saying "this number is not just a number" now sit beside each other in one file, which is what a sample is for. lightRadius, hitboxRadius and influenceRadius are Radius rather than Length, and gravity is Acceleration1D rather than a magnitude, because its default is -9.81 and a magnitude form refuses a negative value at construction. The vocabulary has names for both, which is the argument for using it rather than restating it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UGHDsYaaTQdzVR4XBR6miu
`property?.PropertyType == typeof(DimensionInfo)` is false when the property is absent, so the conditional never dereferenced a null. Neither the compiler's null-state analysis nor CodeQL can follow that, though, and a warning that has to be reasoned about every time it is read is worth two lines to remove. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UGHDsYaaTQdzVR4XBR6miu
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.



A schema that wanted a mass had to declare one. It wrote a semantic type called
Kilograms, put"kg"on it as metadata, and every member of it said the unit twice — once as a name nothing reads and once as text something does. The kilograms are how the stored number is read, which is a fact about the field; the mass is what the value is.ktsu.Semanticsalready names all of it: 212 physical quantities in C#, the same 212 as C++ classes fromktsu.Semantics.Cpp, and both are things a target already has. So this adds aQuantitytype that names one rather than asking for a copy of it —Quantity(Mass),Quantity(Velocity3D),Quantity(Ratio)— and nothing is generated for it.That is the whole difference from
Semantic, and both are wanted. A semantic type is the schema's own — an entity id is not a texture id, and nothing outside the schema has heard of either. A quantity is everybody's.The registry
QuantityRegistryreads the vocabulary out of the assembly, the same arrangementUnitRegistryhas and for the same reason: keeping a list here would fall behind it.A quantity's dimension takes three routes, in order:
IPhysicalQuantity.IVectorNabove zero declares components and no dimension — so its dimension is whatMagnitude()answers with.Position3Dis aDisplacement3Dunder another name and the vocabulary gives it noMagnitude()of its own, so what is followed is the implicit widening onto what it is an overload of.Six of the 212 are reachable only by that third route, and 212 is not a number chosen here — it is what the C++ projection emits (148 magnitudes, 27 signed scalars, 37 vectors). The two counts agreeing is what says a schema can name every quantity a C++ target has; it was 206 before the widening was followed.
The ten it leaves out are the logarithmic scales and two hand-written audio types. A decibel does not add and a pH does not scale, which is why
ktsu.Semanticsemits them fromlogarithmic.jsonrather than as dimensions — they have no dimensional formula and no vector form, so accepting one would put a type in the schema whose arithmetic means nothing and whose eight exponents a reflection table would have to invent.Three things follow from a quantity knowing its own dimension
A unit on one has to agree with it. The check the semantic type could never make: a unit is text resolved through
UnitRegistryandKilogramswas a name nothing reads, so the two had no way to disagree. The exponents are compared and not the names, because 72 of the vocabulary's dimensions share 63 exponent vectors — a joule and a newton metre are the same eight numbers, and refusing what the physics allows is worse than not checking.The reflection table asks the type first. Its eight exponents used to come from the member's unit text and nowhere else, so a member that measured something but named no unit was written down as dimensionless — indistinguishable from a flag. A
Velocity3Dis a length over a time because of what it is.Storageis the rest of the type, since every quantity is generic over one. It defaults toFloatand is omitted from the file when it is, exactly as a vector'sElementTypeis, and it is held to a bare numeric: a generated semantic type is a record struct over a float implementing noINumber<T>, soMass<Kilograms>is not a type anything could write.The two generators
C# spells it
ktsu.Semantics.Quantities.Mass<float>, andClrTypeImporterreads it straight back off the closed type — the one thing generated C# carries that needs no attribute recording what it is. Everything else does, because a sequential struct or a record struct over a float is a shape a hand-written type may have for its own reasons; aMass<float>is not something a target happened to write, it is the quantity.C++ spells it wherever the target's vocabulary went, which
CppGeneratorOptions.Quantitiessays in one entry for all 212 — the target named none of them, it ran the generator. That entry also says what the vocabulary is stored in, because a C++ quantity is a class rather than a template and the storage was fixed when it was generated:A member the schema keeps in a double is refused on a float target rather than emitted as two languages quietly disagreeing about the bytes — the same class of bug the enum width and the bool marshalling were already spelled out to prevent.
Format version 7
Unlike every step since version 1 this is not something an older reader drops. It fails to deserialize the member at all, on a discriminator it has never heard of, so the version moves to say so instead.
The modernised sample says it the new way
Second commit.
samples/modernised.schema.jsondeclaredKilograms,Metres,MetresPerSecondandMetresPerSecondSquared— four semantic types named after units, each also carrying the unit as metadata. Those becomeQuantity(Mass),Quantity(Radius),Quantity(Speed)andQuantity(Acceleration1D)with the unit on the member.Coinstays a semantic type, and that is the point of keeping it: a price is this schema's own idea and nothing outside it has heard of a coin. The two ways of saying "this number is not just a number" now sit beside each other in one file, which is what a sample is for.gravityisAcceleration1Drather than a magnitude because its default is-9.81, and a magnitude form refuses a negative value at construction.Tests
772 → 795, all passing.
QuantityTypeTests(15) — the vocabulary count, component arities, the three dimension routes including the widening, the log-scale exclusion, the unknown-name and non-numeric-storage refusals, the unit agreement check and the shared-exponent case that must not be refused,travelsAsBytes, the omit-when-default storage, the file round trip, the C# spelling, and generate-compile-reimport.QuantityCppTests(8) — the spelling and its include, the refusal for a target with no vocabulary, the storage mismatch both ways, the options-file checks, a promising class of quantities compiled under-std=c++20 -Wall -Wextrawith itsstatic_assert(std::is_trivially_copyable_v<T>), and the reflection table's exponents coming from the quantity with no unit written.TheModernisedSampleCompilesexercises the new mapping over real types.What this sets up
Holotype's
rigid_body.schema.jsondeclaresMass,Heading,RadiusandRestitutionas semantic types and names them inholotype.cppgen.json'sexistingTypes. Those becomeQuantity(Mass),Quantity(Heading),Quantity(Radius)andQuantity(Ratio), and theexistingTypesentries go away — which is the change this was built for.🤖 Generated with Claude Code
https://claude.ai/code/session_01UGHDsYaaTQdzVR4XBR6miu
Generated by Claude Code