Skip to content

Add Semantics.Cpp: the C++ projection of the quantity vocabulary - #214

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

Add Semantics.Cpp: the C++ projection of the quantity vocabulary#214
matt-edmondson merged 2 commits into
mainfrom
claude/blissful-euler-fzuap2

Conversation

@matt-edmondson

Copy link
Copy Markdown
Contributor

The .NET side of this library has always been generated from dimensions.json. Holotype needs the same vocabulary in C++, and the alternative to generating it is that someone writes 162 classes by hand and they drift — which is the failure this repository exists to avoid, in a second language.

Two layers, because neither can do the other's job

StructuralQuantity<D> over a Dimension of eight integer exponents. Shipped as a prelude rather than generated: no part of it is derived from the metadata. What it buys is that a product nobody declared still has a type.

Nominal — one class per dimension and per named overload. This is what the exponents cannot do: measured against the real metadata, 72 dimensions share 63 exponent vectors, so Area/NuclearCrossSection, Torque/Energy and AbsorbedDose/EquivalentDose are each one vector between two names.

The angle exponent

dimensionalFormula gains angle, carried by AngularDisplacement, AngularVelocity, AngularAcceleration and AngularJerk — and nothing else. Without it an angle is the same type as a ratio and an angular speed the same type as a frequency; with it, 61 distinct exponent vectors become 63.

Nothing on the .NET side depends on it yet; it travels through DimensionInfo, which is why the generated C# moves (4 lines).

A relationship is checked before it is emitted

The operator is written as Result{ lhs.value() * rhs.value() }, so the exponents must agree with the declared result or it doesn't compile. That turns every claim in integrals and derivatives into something checkable rather than asserted. Four are refused, each by name with both dimensions written out:

Torque * AngularDisplacement -> Energy: is not dimensionally true: L² M T⁻² * A is L² M T⁻² A, and Energy is L² M T⁻².
MomentOfInertia * AngularVelocity -> AngularMomentum: ...
MomentOfInertia * AngularAcceleration -> Torque: ...
Sensitivity * Pressure -> ElectricPotential: is not dimensionally true: L⁻¹ M⁻¹ T² I * L⁻¹ M T⁻² is L⁻² I, and ElectricPotential is L² M T⁻³ I⁻¹.

The first three are not fixable by choosing different angle exponents, and that's provable: Torque × AngularDisplacement → Energy forces torque's angle exponent to −1, and Force × Length → Torque forces it to 0. It's the r×F versus τ·θ contradiction, and it's why SI keeps the radian dimensionless.

⚠️ The fourth is a pre-existing metadata bug with nothing to do with angle. Sensitivity is declared as A/Pa (M⁻¹L⁻¹T²I) while the relationship treats it as V/Pa. One of the two is wrong; which is a physics call, so it's reported rather than guessed at. Nothing had ever multiplied the exponents out before, so nothing had noticed.

How the generated code is written is measured, not chosen

The header of CppQuantityGenerator carries the four rules and why. The same vocabulary written two ways measured 0.9896 and 1.4004 against bare floats on MSVC while GCC and clang folded both away — the wrong formulation passes on three compilers of four.

Tests

16, and two of them are the ones that matter:

  • TheWholeVocabularyCompiles — all 153 headers through g++ -std=c++20 -Wall -Wextra, clean.
  • AProductWithTheWrongDimensionDoesNotCompile — a product whose exponents don't match its result type must be rejected. Without this, the first test would only prove the headers parse, and the "checked" relationships would be checked against nothing.

Existing suite: 1,125 passed, so the metadata change doesn't move the C# side.

Deliberately not here

  • The vector forms. 122 dimension-and-form entries against the 72 magnitudes projected here. They're distinct classes too and need componentwise operations, which have a rule of their own — half-doing them would be worse than not starting.
  • A runtime guard in the constructor body. It rides in the member initialiser instead, because ktsu.Coder has no expression-statement node — a call made for its effect isn't something the AST can currently say. A comma expression in a constexpr constructor is a legitimate spelling, but the reason it was chosen is that the alternative couldn't be written.

🤖 Generated with Claude Code

https://claude.ai/code/session_01UGHDsYaaTQdzVR4XBR6miu


Generated by Claude Code

The .NET side of this library has always been generated from dimensions.json.
Holotype needs the same vocabulary in C++, and the alternative to generating it
is that someone writes 162 classes by hand and they drift - which is the failure
this repository exists to avoid, in a second language.

Two layers, because neither can do the other's job.

The structural layer is Quantity<D> over a Dimension of eight integer exponents.
It is shipped as a prelude rather than generated: no part of it is derived from
the metadata, and eight integers and the four ways to combine them are the same
whatever dimensions.json says. What it buys is that a product nobody declared
still has a type.

The nominal layer is one class per dimension and per named overload, and it is
what the exponents cannot do. Measured against the real metadata, 72 dimensions
share 63 exponent vectors: Area and NuclearCrossSection, Torque and Energy,
AbsorbedDose and EquivalentDose are each one vector between two names, and only
naming them separates them.

The metadata gains an angle exponent, carried by AngularDisplacement,
AngularVelocity, AngularAcceleration and AngularJerk. Without it an angle is the
same type as a ratio and an angular speed the same type as a frequency; with it,
61 distinct exponent vectors become 63. Nothing on the .NET side depends on it
yet; it travels through DimensionInfo, which is why the generated C# moves.

A relationship is checked before it is emitted. The operator is written as
Result{ lhs.value() * rhs.value() }, so the exponents have to agree with the
declared result or it does not compile - which turns every claim in `integrals`
and `derivatives` into something checkable rather than something asserted. Four
are refused, each by name with both dimensions written out:

  Torque * AngularDisplacement -> Energy
  MomentOfInertia * AngularVelocity -> AngularMomentum
  MomentOfInertia * AngularAcceleration -> Torque
  Sensitivity * Pressure -> ElectricPotential

The first three are not fixable by choosing different angle exponents, and that
is provable rather than a matter of taste: Torque * AngularDisplacement -> Energy
forces torque's angle exponent to -1, and Force x Length -> Torque forces it to
0. It is the r x F versus tau . theta contradiction, and it is why SI keeps the
radian dimensionless.

The fourth has nothing to do with angle and was already wrong: Sensitivity is
declared as A/Pa while the relationship treats it as V/Pa. One of the two is a
mistake and which one is a physics call, so it is reported rather than guessed
at. Nothing had ever multiplied the exponents out before, so nothing had noticed.

How the generated code is written is measured rather than chosen, and the header
of CppQuantityGenerator says so: the same vocabulary written two ways measured
0.9896 and 1.4004 against bare floats on MSVC while GCC and clang folded both
away, so the wrong formulation passes on three compilers of four.

Sixteen tests, two of which are the ones that matter: the whole vocabulary is
compiled with g++ -std=c++20 -Wall -Wextra, and a product whose exponents do not
match its result type is required to be rejected by the compiler. Without the
second, the first would only prove the headers parse.

Two things are deliberately not here. The vector forms are distinct classes too -
122 dimension-and-form entries against the 72 magnitudes projected here - and
they need componentwise operations, which have a rule of their own; half-doing
them would be worse than not starting. And the magnitude guard rides in the
member initialiser rather than the constructor body, because ktsu.Coder has no
expression-statement node, so a call made for its effect is not something the AST
can currently say.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UGHDsYaaTQdzVR4XBR6miu
Comment thread Semantics.Cpp.Test/GeneratedCppCompilesTests.cs Fixed
Comment thread Semantics.Cpp/QuantityVocabulary.cs Fixed
Comment thread Semantics.Cpp.Test/CppQuantityGeneratorTests.cs Fixed
Comment thread Semantics.Cpp.Test/GeneratedCppCompilesTests.cs Fixed
Comment thread Semantics.Cpp.Test/GeneratedCppCompilesTests.cs Fixed
Comment thread Semantics.Cpp.Test/GeneratedCppCompilesTests.cs Fixed
Comment thread Semantics.Cpp.Test/GeneratedCppCompilesTests.cs Fixed
Comment thread Semantics.Cpp.Test/GeneratedCppCompilesTests.cs Fixed
Comment thread Semantics.Cpp.Test/GeneratedCppCompilesTests.cs Fixed
The `Path.Combine` calls are the substantive one: `Combine` silently discards
every argument before a rooted one, so a name that turned out to be rooted --
a generated file name, a PATH entry -- would write or read somewhere other
than where the call reads as writing. `Path.Join` concatenates instead, which
is what all of these actually mean. `Find` takes the executable's file name
for the same reason, since a PATH entry is the directory.

`ResolveRelationships` filters with `OfType` rather than an `if` inside the
loop, which drops the null and the null-forgiving both.

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

Copy link
Copy Markdown

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