Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
56 changes: 40 additions & 16 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -253,28 +253,52 @@ numeric types, which rejects an exponent.

Unit conversion factors and metric magnitudes reach a storage type the same way constants do.
`ConversionsGenerator` and `MagnitudesGenerator` still emit `double` constants, which back the
public `IUnit.ToBaseFactor` and `ToBaseOffset` properties, and alongside them a `Values<T>` holder
that calls `StorageLiteral.Parse<T>` or `StorageLiteral.Divide<T>` once per closed generic type. The
generated `From{Unit}` factories multiply by `Values<T>`, and each generated unit implements
`IUnit.ToBaseFactorAs<T>()` and `ToBaseOffsetAs<T>()` explicitly from it, which is what
`ToBase`/`FromBase` and every `In(unit)` read. Before this, every factor went through
`T.CreateChecked(double)`, so a `decimal` quantity converted with the 15 significant digits that
conversion keeps.

- A value in `conversions.json` is a decimal literal or an exact fraction of two, `"5/9"`. Write a
repeating ratio as the fraction, not as its rounded decimal, and anything built on π as a long
literal. SEM009 reports a value that is neither.
- An integer storage type, or one that cannot parse the literal, falls back to the old
`T.CreateChecked(double)`, so `int` quantities convert exactly as they did.
public `IUnit.ToBaseFactor` and `ToBaseOffset` properties, and alongside them a `Values<T>` holder.
Each value in the holder is a property over a private nullable field that `StorageLiteral.Parse<T>`
or `StorageLiteral.Divide<T>` fills once per closed generic type, and the property converts the
`double` constant with `T.CreateChecked` when that field is null. The generated `From{Unit}` factories
multiply by these properties, and each generated unit implements `IUnit.ToBaseFactorAs<T>()` and
`ToBaseOffsetAs<T>()` explicitly from them, which is what `ToBase`/`FromBase` and every `In(unit)`
read. Before 5.2.0 every factor went through `T.CreateChecked(double)`, so a `decimal` quantity
converted with the 15 significant digits that conversion keeps.

- A value in `conversions.json` is a decimal literal or an exact fraction of two, `"5/9"`, that a
`double` can hold. Write a repeating ratio as the fraction, not as its rounded decimal. SEM009
reports a value that is neither form, is beyond the range of `double`, or is non-zero and rounds to
zero in it.
- Write anything built on π as a long literal computed from π itself and correctly rounded to 150
significant digits, never derived from another literal. `DegreeToRadians` once carried an error from
its 98th digit into every factor taken from it. `PiLiteralTests` checks each literal built on π in
`conversions.json` and `domains.json` against π computed there by Machin's formula.
- An integer storage type, or one that cannot parse the literal, gets `null` from `StorageLiteral` and
converts the `double` at each read, so integer quantities convert exactly as they did before 5.2.0.
A factor too large for the type (`CurieToBecquerels` for `int`, `Yotta` for `long`) throws
`OverflowException` from the factory that uses it, and every other factory keeps working. 5.2.0
converted every value for a type in one static initializer, so that single overflow made every
conversion for `int` throw `TypeInitializationException`. Keep conversions out of the initializer.
`IntegerStorageConversionTests` pins the behavior against the 5.1 expressions.
- A parse that throws `NotSupportedException` or `ArgumentException` for `NumberStyles.Float`, as a
numeric type outside the base library may, counts as one that cannot parse the literal. Nothing else
is caught.
- Writing factors as their exact definitions in 5.2.0 moved two `double` constants to the adjacent
representable value, each closer to the true value than before: `PsiToPascals` from
6894.757293168361 to 6894.757293168362, and `RevolutionPerMinuteToRadianPerSecond` from
0.10471975511965977 to 0.10471975511965978. The public `IUnit.ToBaseFactor` of `Psi` and
`RevolutionPerMinute` moved with them. Every other constant kept its value. Each constant is written
with a `d` suffix, because a literal such as `100000000000000000000` is otherwise an integer literal
the compiler rejects.
- `ToBaseFactorAs<T>()` and `ToBaseOffsetAs<T>()` are default-implemented on `IUnit`, so a unit
written outside the library needs nothing new. They are not named `Get…`, because CA1721 rejects
a `GetToBaseFactor` method next to the `ToBaseFactor` property.

Vector `Length()` and `Distance()` call `StorageMath.Sqrt`. The binary floating point and integer
primitives take the `Math.Sqrt` round trip the generated code always inlined, so their results are
unchanged. Any other type is seeded from that root and refined with Newton steps in its own
arithmetic, so a `decimal` length has 28 significant digits. The logarithmic scales and the
hand-written audio types still compute through `double`.
arithmetic, so a `decimal` length has 28 significant digits. A value a `double` cannot hold, such as a
`BigInteger` of 2^2048, is scaled by powers of four into [1, 4) for the seed, and the root is scaled
back by the matching power of two. A root that does not settle throws `ArithmeticException` rather
than returning an estimate. The logarithmic scales and the hand-written audio types still compute
through `double`.

`StorageConversionTests<T>` runs the same conversions, relationships and vector lengths over
`double` and `decimal`, exactly where the answer terminates and to a relative tolerance where it
Expand Down Expand Up @@ -368,7 +392,7 @@ var converted = sourceString.As<SourceType, TargetType>();
- **SEM006** — a metadata file a generator declared in `MetadataFileNames` was not supplied as an `AdditionalFile`. Previously this produced no output and no explanation, which is indistinguishable from a generator that simply had nothing to emit.
- **SEM007** — a metadata file could not be parsed. Replaces the base generator's `CONV001` in category `SourceGenerator`, and covers the path that used to swallow the exception, where a malformed `units.json` silently produced factories with no scale factor.
- **SEM008** — a relationship's declared result does not follow from the dimensions of its operands, or its value is signed and the declared result is a magnitude. The check comes from `Semantics.Vocabulary`, shared with the C++ projection; before that this side checked the names (SEM001) and the forms (SEM003) and then emitted the operator, so `Sensitivity * Pressure -> ElectricPotential` shipped as a working C# operator computing the wrong physics — which is what found that bug, and it is now fixed. **No operator is generated** for a refused relationship, in any of the directions C# spells a product in — that followed from making the vocabulary drive emission rather than only check it, and the removal is documented in `docs/migration-guide-5.0.md`. Suppressed in `Semantics.Quantities.csproj` because ktsu.Sdk builds warnings as errors and the four below are outstanding; `UnkeepableRelationshipTests` pins the set, and asserts that none of them is in the compiled surface, so a fifth fails there rather than disappearing into the suppression.
- **SEM009**: a factor's `value` in `conversions.json` is neither a decimal literal nor a fraction of two with a non-zero denominator. An error, and no constant is generated for it, because every unit using the factor would otherwise fail to compile far from the metadata line that caused it.
- **SEM009**: a factor's `value` in `conversions.json` is neither a decimal literal nor a fraction of two with a non-zero denominator, or a `double` cannot hold it (a literal, operand, or quotient beyond its range, or a non-zero value that rounds to zero). An error, and no constant is generated for it, because every unit using the factor would otherwise fail to compile far from the metadata line that caused it, or convert with a wrong factor.
- Descriptors are allocated from `SemanticsDiagnostics`, which is the one place to add a new one. `AnalyzerReleaseTrackingTests` fails if the identifier is missing from `AnalyzerReleases.Unshipped.md`, so RS2008 no longer surfaces only after a push.
- See `docs/physics-generator.md` for the full schema and an end-to-end "add a dimension" walk-through.

Expand Down
Loading
Loading