diff --git a/CLAUDE.md b/CLAUDE.md index 753e64a8..7a12dd74 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -44,7 +44,7 @@ The opt-in lives in `.sonarlint/sonar-local.props` (analyzer package) and `.sona | `Semantics.Color` | Physically-grounded color types. Canonical linear-RGB `Color` hub plus color-space satellites (`Srgb`, `Hsl`, `Hsv`, `Oklab`, `Oklch`); every type converts to and from every other, routed through the nearest shared hub (`Srgb` within the sRGB family, `Oklab` within the perceptual family, linear `Color` across families) so no conversion takes a redundant gamma round-trip. Also WCAG accessibility tooling, HSL/perceptual adjustment operations (lighten/saturate/hue/invert), and `NamedColors`. Targets `net8.0`–`net10.0` + `netstandard2.0`/`netstandard2.1`. | | `Semantics.Quantities` | Hand-written runtime types (`IPhysicalQuantity`, `PhysicalQuantityCore`, `IVector0`..`IVector4`, `UnitSystem`) plus generator output under `Generated/`. Every generated quantity is a `readonly record struct`. | | `Semantics.SourceGenerators` | Roslyn incremental generators that emit quantity types, units, conversions, magnitudes, physical constants, and storage-type helpers from metadata. Only the physics-specific half lives here — `Models/`, `Metadata/`, `Generators/`, and the bindings in `SemanticsGenerator`/`SemanticsDiagnostics`/`Emit`. The C# syntax templates come from `ktsu.CodeBlocker.Templates`; the metadata-driven generator base, metadata loading and the diagnostic catalogue come from `ktsu.SourceGeneratorToolkit` (#181, #192). | -| `Semantics.Quantities.{Double,Float,Decimal}` | Props-only satellite packages. Each ships a `buildTransitive` props file (generated by `scripts/Generate-AliasProps.ps1`) that injects global-using aliases binding every quantity to one storage type, so consumers write `Mass` instead of `Mass`. | +| `Semantics.Quantities.{Double,Float,Decimal,Precise}` | Props-only satellite packages. Each ships a `buildTransitive` props file (generated by `scripts/Generate-AliasProps.ps1`) that injects global-using aliases binding every quantity to one storage type, so consumers write `Mass` instead of `Mass`. `Precise` binds to `ktsu.PreciseNumber.PreciseNumber` and is the one whose storage type comes from a package rather than being a C# keyword, so it carries a `PackageReference` the others do not; the core `Semantics.Quantities` still has no PreciseNumber dependency. | | `Semantics.Vocabulary` | **Shared source, not a project.** Resolves `dimensions.json` into the quantities and operators it describes and separates out what cannot be honoured. Compiled into both `Semantics.SourceGenerators` and `Semantics.Cpp` via `Compile Include`; see its README for why source rather than an assembly, and what that costs. | | `Semantics.Cpp` | The C++ projection of the quantity vocabulary, in its own project because `ktsu.Coder` ships no `net8.0`. Reads `dimensions.json` and emits one C++ class per dimension, per vector form and per named overload, plus the declared relationships as operators. | | `Semantics.Cpp.Test` | Its tests, including ones that compile the whole generated vocabulary with `g++`/`clang++`, assert what it means through `static_assert`, and check that a dimensionally wrong product — scalar or componentwise — is refused by the compiler. Also `SevenTargetProjectionTests`, which is not about C++ at all: see below. | @@ -381,7 +381,7 @@ var converted = sourceString.As(); - Edit `Semantics.SourceGenerators/Metadata/dimensions.json` to add a dimension, vector form, semantic overload, or relationship. - Rebuild `Semantics.SourceGenerators` and the consuming `Semantics.Quantities` project; emitted files appear in `Semantics.Quantities/Generated/Semantics.SourceGenerators//`. - Treat generator output as committed source. Diff it before commit so accidental regressions are visible. -- After adding or renaming a quantity, regenerate the storage-type alias props with `pwsh scripts/Generate-AliasProps.ps1` (it reads the generated catalogue and rewrites `Semantics.Quantities.{Double,Float,Decimal}/buildTransitive/*.props`) and commit them. The `verify-generated` workflow rebuilds, regenerates, and fails the PR if either the generated sources or the alias props drift. +- After adding or renaming a quantity, regenerate the storage-type alias props with `pwsh scripts/Generate-AliasProps.ps1` (it reads the generated catalogue and rewrites `Semantics.Quantities.{Double,Float,Decimal,Precise}/buildTransitive/*.props`) and commit them. The `verify-generated` workflow rebuilds, regenerates, and fails the PR if either the generated sources or the alias props drift. - Factory names are the **singular lemma** (#49). The generator emits `From{name}` using each unit's `name` from `units.json` verbatim (e.g. `Length.FromMeter`, `Mass.FromKilogram`, `Speed.FromMeterPerSecond`, `Length.FromFoot`, `Frequency.FromHertz`). The rule is purely mechanical, so `name` must itself be the singular lemma — including compounds, whose leading noun is singular too (`MeterPerSecond`, `RevolutionPerMinute`, `PartPerMillion`, not `MeterPerSecond`/`RevolutionPerMinute`/`PartPerMillion`). There is no `factoryName` field and no pluralisation step; the generator never has to know English pluralisation. - Generator diagnostics: - **SEM001** — a relationship in `dimensions.json` references a dimension that does not exist (typo or rename). The operator is silently dropped. diff --git a/Directory.Packages.props b/Directory.Packages.props index 82a0bd57..2ba64ec3 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -10,6 +10,7 @@ + diff --git a/README.md b/README.md index 1788c7c3..597b0746 100644 --- a/README.md +++ b/README.md @@ -29,6 +29,7 @@ Each package has its own README with the full API surface, examples, and referen | [`ktsu.Semantics.Quantities.Double`](Semantics.Quantities.Double/README.md) | `double` storage-type aliases | [README](Semantics.Quantities.Double/README.md) | | [`ktsu.Semantics.Quantities.Float`](Semantics.Quantities.Float/README.md) | `float` storage-type aliases | [README](Semantics.Quantities.Float/README.md) | | [`ktsu.Semantics.Quantities.Decimal`](Semantics.Quantities.Decimal/README.md) | `decimal` storage-type aliases | [README](Semantics.Quantities.Decimal/README.md) | +| [`ktsu.Semantics.Quantities.Precise`](Semantics.Quantities.Precise/README.md) | `ktsu.PreciseNumber` storage-type aliases | [README](Semantics.Quantities.Precise/README.md) | | [`ktsu.Semantics.Music`](Semantics.Music/README.md) | Musical value types and harmonic analysis | [README](Semantics.Music/README.md) | | [`ktsu.Semantics.Color`](Semantics.Color/README.md) | Linear/perceptual color with accessibility tooling | [README](Semantics.Color/README.md) | diff --git a/Semantics.Quantities.Decimal/README.md b/Semantics.Quantities.Decimal/README.md index c96a6976..3a7ca32f 100644 --- a/Semantics.Quantities.Decimal/README.md +++ b/Semantics.Quantities.Decimal/README.md @@ -66,6 +66,7 @@ The aliases are project-wide global usings keyed on the bare type name (`Mass`, - [`ktsu.Semantics.Quantities.Double`](../Semantics.Quantities.Double/README.md) - [`ktsu.Semantics.Quantities.Float`](../Semantics.Quantities.Float/README.md) - `ktsu.Semantics.Quantities.Decimal` (this package) +- [`ktsu.Semantics.Quantities.Precise`](../Semantics.Quantities.Precise/README.md) A project that genuinely needs mixed storage types should skip the alias packages and reference `ktsu.Semantics.Quantities` directly, writing the closed generic (`Mass`) explicitly. diff --git a/Semantics.Quantities.Double/README.md b/Semantics.Quantities.Double/README.md index 73e76f2d..2ea123cb 100644 --- a/Semantics.Quantities.Double/README.md +++ b/Semantics.Quantities.Double/README.md @@ -66,6 +66,7 @@ The aliases are project-wide global usings keyed on the bare type name (`Mass`, - `ktsu.Semantics.Quantities.Double` (this package) - [`ktsu.Semantics.Quantities.Float`](../Semantics.Quantities.Float/README.md) - [`ktsu.Semantics.Quantities.Decimal`](../Semantics.Quantities.Decimal/README.md) +- [`ktsu.Semantics.Quantities.Precise`](../Semantics.Quantities.Precise/README.md) A project that genuinely needs mixed storage types should skip the alias packages and reference `ktsu.Semantics.Quantities` directly, writing the closed generic (`Mass`) explicitly. diff --git a/Semantics.Quantities.Float/README.md b/Semantics.Quantities.Float/README.md index a90fdfc3..007714ad 100644 --- a/Semantics.Quantities.Float/README.md +++ b/Semantics.Quantities.Float/README.md @@ -66,6 +66,7 @@ The aliases are project-wide global usings keyed on the bare type name (`Mass`, - [`ktsu.Semantics.Quantities.Double`](../Semantics.Quantities.Double/README.md) - `ktsu.Semantics.Quantities.Float` (this package) - [`ktsu.Semantics.Quantities.Decimal`](../Semantics.Quantities.Decimal/README.md) +- [`ktsu.Semantics.Quantities.Precise`](../Semantics.Quantities.Precise/README.md) A project that genuinely needs mixed storage types should skip the alias packages and reference `ktsu.Semantics.Quantities` directly, writing the closed generic (`Mass`) explicitly. diff --git a/Semantics.Quantities.Precise/README.md b/Semantics.Quantities.Precise/README.md new file mode 100644 index 00000000..dcf61a96 --- /dev/null +++ b/Semantics.Quantities.Precise/README.md @@ -0,0 +1,101 @@ +# ktsu.Semantics.Quantities.Precise + +> Storage-type aliases that bind every `ktsu.Semantics.Quantities` type to `ktsu.PreciseNumber.PreciseNumber`, so you write `Mass` instead of `Mass`, project-wide. + +[![License](https://img.shields.io/github/license/ktsu-dev/Semantics.svg?label=License&logo=nuget)](../LICENSE.md) +[![NuGet Version](https://img.shields.io/nuget/v/ktsu.Semantics.Quantities.Precise?label=Stable&logo=nuget)](https://nuget.org/packages/ktsu.Semantics.Quantities.Precise) +[![NuGet Version](https://img.shields.io/nuget/vpre/ktsu.Semantics.Quantities.Precise?label=Latest&logo=nuget)](https://nuget.org/packages/ktsu.Semantics.Quantities.Precise) +[![NuGet Downloads](https://img.shields.io/nuget/dt/ktsu.Semantics.Quantities.Precise?label=Downloads&logo=nuget)](https://nuget.org/packages/ktsu.Semantics.Quantities.Precise) +[![GitHub commit activity](https://img.shields.io/github/commit-activity/m/ktsu-dev/Semantics?label=Commits&logo=github)](https://github.com/ktsu-dev/Semantics/commits/main) +[![GitHub contributors](https://img.shields.io/github/contributors/ktsu-dev/Semantics?label=Contributors&logo=github)](https://github.com/ktsu-dev/Semantics/graphs/contributors) +[![GitHub Actions Workflow Status](https://img.shields.io/github/actions/workflow/status/ktsu-dev/Semantics/dotnet.yml?label=Build&logo=github)](https://github.com/ktsu-dev/Semantics/actions) + +`ktsu.Semantics.Quantities.Precise` is a satellite of [`ktsu.Semantics.Quantities`](../Semantics.Quantities/README.md) in the [ktsu.Semantics](../README.md) family. Read the core package README first for the quantity API itself. + +## Introduction + +Every quantity in `ktsu.Semantics.Quantities` is generic over its numeric storage type, so you normally write `Mass`, `Speed`, and so on. If a project uses one storage type throughout, that generic argument is noise. + +This package is props-only. It ships no assembly, just a `buildTransitive` props file that injects one C# global-using alias per quantity, binding each open generic type to [`ktsu.PreciseNumber.PreciseNumber`](https://nuget.org/packages/ktsu.PreciseNumber). Reference it and you can write `Mass`, `Speed`, `Force3D` with no generic argument, and every quantity resolves to its `PreciseNumber` form. The aliases are real `Mass` (and so on), so they interoperate with the entire API with no conversion. + +Installing this package also pulls in the matching version of `ktsu.Semantics.Quantities` and `ktsu.PreciseNumber` as dependencies, so it is the only reference you need. The core quantities package does not depend on `ktsu.PreciseNumber`; that dependency arrives only with this package. + +## Why this storage type + +`PreciseNumber` is an arbitrary-precision type — a `BigInteger` significand with a decimal exponent — so a unit factor is applied at the precision the value carries rather than at the 15 to 17 significant digits a `double` holds. Conversions that terminate come out exact: + +```csharp +Length.FromFoot(PreciseNumber.One).In(Units.Inch) // exactly 12 +1.0 * 0.3048 / 0.0254 // 12.000000000000002 in double +``` + +Roots are computed in the storage type's own arithmetic rather than through a `double` round trip, so a vector length keeps its digits too: + +```csharp +Velocity3D.One.Length() // 1.73205080756887729352744634150587236694280525381038 +``` + +This costs speed and allocation against `double`, so it suits work where the error budget matters more than throughput — long-horizon integration, unit-conversion chains, and reference values to check a faster pipeline against. + +Two limits worth knowing. The logarithmic scales (`Decibels`, `Cents`, `PH`, and the rest) still compute through `double` whatever the storage type. And a quotient that does not terminate is rounded to a precision derived from its operands, not carried forever. + +## Installation + +### Package Manager Console + +```powershell +Install-Package ktsu.Semantics.Quantities.Precise +``` + +### .NET CLI + +```bash +dotnet add package ktsu.Semantics.Quantities.Precise +``` + +### Package Reference + +```xml + +``` + +## Usage Example + +```csharp +using ktsu.Semantics.Quantities; // types resolve to their PreciseNumber form via the injected aliases +using ktsu.PreciseNumber; + +Mass mass = Mass.FromKilogram(10.ToPreciseNumber()); +Speed speed = Speed.FromMeterPerSecond(15.ToPreciseNumber()); +Mass total = mass + Mass.FromKilogram(2.ToPreciseNumber()); // still a Mass, full identity + +Force3D f = new() { X = 3.ToPreciseNumber(), Y = 4.ToPreciseNumber(), Z = PreciseNumber.Zero }; +ForceMagnitude mag = f.Magnitude(); // exactly 5 +``` + +Under the hood the injected alias looks like this (one line per quantity, roughly 220 in total): + +```xml + +``` + +## Use exactly one storage-type alias package per project + +The aliases are project-wide global usings keyed on the bare type name (`Mass`, `Speed`, ...). Referencing two flavor packages in the same project would define the same alias name twice (one bound to `PreciseNumber`, one to another type), which is a compile error. So reference exactly one of: + +- [`ktsu.Semantics.Quantities.Double`](../Semantics.Quantities.Double/README.md) +- [`ktsu.Semantics.Quantities.Float`](../Semantics.Quantities.Float/README.md) +- [`ktsu.Semantics.Quantities.Decimal`](../Semantics.Quantities.Decimal/README.md) +- `ktsu.Semantics.Quantities.Precise` (this package) + +A project that genuinely needs mixed storage types should skip the alias packages and reference `ktsu.Semantics.Quantities` directly, writing the closed generic (`Mass`) explicitly. + +The alias lists are generated from the quantity catalogue by `scripts/Generate-AliasProps.ps1` and validated in CI, so they stay in lockstep with the quantities the core package emits. + +## Contributing + +Contributions are welcome! Feel free to open issues or submit pull requests. + +## License + +This project is licensed under the MIT License. See the [LICENSE.md](../LICENSE.md) file for details. diff --git a/Semantics.Quantities.Precise/Semantics.Quantities.Precise.csproj b/Semantics.Quantities.Precise/Semantics.Quantities.Precise.csproj new file mode 100644 index 00000000..78ad5c60 --- /dev/null +++ b/Semantics.Quantities.Precise/Semantics.Quantities.Precise.csproj @@ -0,0 +1,33 @@ + + + + + + + net10.0;net9.0;net8.0 + + false + + false + false + Storage-type aliases for ktsu.Semantics.Quantities. Reference this package to write `Mass` instead of `Mass<PreciseNumber>` (and every other quantity) bound to ktsu.PreciseNumber, project-wide. Unit factors and vector lengths are then exact to the precision the value carries rather than to a double. Use one storage-type alias package per project. + + $(NoWarn);NU5128 + + + + + + + + + + + + + + diff --git a/Semantics.Quantities.Precise/buildTransitive/ktsu.Semantics.Quantities.Precise.props b/Semantics.Quantities.Precise/buildTransitive/ktsu.Semantics.Quantities.Precise.props new file mode 100644 index 00000000..5769af15 --- /dev/null +++ b/Semantics.Quantities.Precise/buildTransitive/ktsu.Semantics.Quantities.Precise.props @@ -0,0 +1,226 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Semantics.Test/Quantities/StorageConversionTests.cs b/Semantics.Test/Quantities/StorageConversionTests.cs index b398c3cd..38a4bfca 100644 --- a/Semantics.Test/Quantities/StorageConversionTests.cs +++ b/Semantics.Test/Quantities/StorageConversionTests.cs @@ -7,6 +7,7 @@ namespace ktsu.Semantics.Test.Quantities; using System.Linq; using System.Numerics; using System.Reflection; +using ktsu.PreciseNumber; using ktsu.Semantics.Quantities; using ktsu.Semantics.Quantities.Units; using Microsoft.VisualStudio.TestTools.UnitTesting; @@ -23,6 +24,22 @@ public sealed class DoubleStorageConversionTests() : StorageConversionTests(tolerance: "1e-25", decimalExact: true); +/// +/// Runs over , the storage type the +/// ktsu.Semantics.Quantities.Precise alias package binds to. +/// +/// +/// The tolerance is ten orders of magnitude tighter than 's, so a factor that +/// reached the type through a — wrong from about the sixteenth digit — fails here +/// by an enormous margin. It is not tighter still because the limit is the expected literals rather +/// than the storage type: the knot is a repeating fraction written to 38 decimal places, and +/// PreciseNumber answers it with 50 correct digits, so the residual being measured is the truncation +/// of the reference value. Lengthening that literal is what would buy a tighter bound. +/// +[TestClass] +public sealed class PreciseNumberStorageConversionTests() + : StorageConversionTests(tolerance: "1e-35", decimalExact: true); + /// /// The same unit conversions, physics relationships and vector lengths, run over each storage type. /// diff --git a/Semantics.Test/Semantics.Test.csproj b/Semantics.Test/Semantics.Test.csproj index 73ea1410..01b93430 100644 --- a/Semantics.Test/Semantics.Test.csproj +++ b/Semantics.Test/Semantics.Test.csproj @@ -43,6 +43,10 @@ + + diff --git a/Semantics.sln b/Semantics.sln index 3fe8046c..b51638b3 100644 --- a/Semantics.sln +++ b/Semantics.sln @@ -29,6 +29,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Semantics.Cpp", "Semantics. EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Semantics.Cpp.Test", "Semantics.Cpp.Test\Semantics.Cpp.Test.csproj", "{2E8EAA2E-59FB-410D-9DB1-C9850311B029}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Semantics.Quantities.Precise", "Semantics.Quantities.Precise\Semantics.Quantities.Precise.csproj", "{5A67DFC3-E2D2-41B9-9E42-0B4727AF0C7D}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -195,6 +197,18 @@ Global {2E8EAA2E-59FB-410D-9DB1-C9850311B029}.Release|x64.Build.0 = Release|Any CPU {2E8EAA2E-59FB-410D-9DB1-C9850311B029}.Release|x86.ActiveCfg = Release|Any CPU {2E8EAA2E-59FB-410D-9DB1-C9850311B029}.Release|x86.Build.0 = Release|Any CPU + {5A67DFC3-E2D2-41B9-9E42-0B4727AF0C7D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {5A67DFC3-E2D2-41B9-9E42-0B4727AF0C7D}.Debug|Any CPU.Build.0 = Debug|Any CPU + {5A67DFC3-E2D2-41B9-9E42-0B4727AF0C7D}.Debug|x64.ActiveCfg = Debug|Any CPU + {5A67DFC3-E2D2-41B9-9E42-0B4727AF0C7D}.Debug|x64.Build.0 = Debug|Any CPU + {5A67DFC3-E2D2-41B9-9E42-0B4727AF0C7D}.Debug|x86.ActiveCfg = Debug|Any CPU + {5A67DFC3-E2D2-41B9-9E42-0B4727AF0C7D}.Debug|x86.Build.0 = Debug|Any CPU + {5A67DFC3-E2D2-41B9-9E42-0B4727AF0C7D}.Release|Any CPU.ActiveCfg = Release|Any CPU + {5A67DFC3-E2D2-41B9-9E42-0B4727AF0C7D}.Release|Any CPU.Build.0 = Release|Any CPU + {5A67DFC3-E2D2-41B9-9E42-0B4727AF0C7D}.Release|x64.ActiveCfg = Release|Any CPU + {5A67DFC3-E2D2-41B9-9E42-0B4727AF0C7D}.Release|x64.Build.0 = Release|Any CPU + {5A67DFC3-E2D2-41B9-9E42-0B4727AF0C7D}.Release|x86.ActiveCfg = Release|Any CPU + {5A67DFC3-E2D2-41B9-9E42-0B4727AF0C7D}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/scripts/Generate-AliasProps.ps1 b/scripts/Generate-AliasProps.ps1 index f5e55c7f..6a21f5c2 100644 --- a/scripts/Generate-AliasProps.ps1 +++ b/scripts/Generate-AliasProps.ps1 @@ -43,11 +43,14 @@ if ($typeNames.Count -eq 0) { throw "No quantity types found under $generatedRoot. Build Semantics.Quantities first so the generated files exist." } -# PascalCase project/package suffix => C# storage keyword. +# PascalCase project/package suffix => the storage type as it is written in the alias. +# A keyword for the built-in types; a fully qualified name for one that comes from a package, +# since the alias is expanded in projects that have no using directives of ours. $storageTypes = [ordered]@{ 'Double' = 'double' 'Float' = 'float' 'Decimal' = 'decimal' + 'Precise' = 'ktsu.PreciseNumber.PreciseNumber' } foreach ($entry in $storageTypes.GetEnumerator()) {