feat(quantities): enforce V0 non-negativity and absolute V0-V0 subtraction (closes #50, #52)#68
Merged
matt-edmondson merged 1 commit intovectorsfrom May 9, 2026
Merged
Conversation
…ction Closes #50 and #52. Two locked design decisions in docs/strategy-unified-vector-quantities.md weren't honoured by the generator: #50 — Vector0 quantities should reject negative inputs at construction. Old: Speed.FromMetersPerSecond(-1) silently produced Speed(-1). New: factories run Vector0Guards.EnsureNonNegative on the converted value and throw ArgumentException when it would be negative. #52 — V0 - V0 should return the same V0 of T.Abs(a - b). Old: when V1 existed, generator emitted V0 - V0 => V1 (the original "option 4"). Otherwise it fell through to PhysicalQuantity's plain subtraction, which produced a negative magnitude. New: every V0 base type and V0 overload emits its own operator -(TSelf, TSelf) => TSelf returning T.Abs(left - right). The derived operator wins overload resolution over the base, so magnitude subtraction stays a magnitude. Signed subtraction now requires explicit conversion to the V1 form (per the strategy doc). Implementation: - New Vector0Guards.EnsureNonNegative<T>(value, paramName) helper in Semantics.Quantities. Pure runtime guard, throws ArgumentException when T.Sign(value) < 0. - QuantitiesGenerator emits the guard in every V0 base type and V0 overload's From{Unit} factory. V1 base types and V1 overloads are unchanged (they accept any sign). - The generator's previous V0 -> V1 subtraction emitter is replaced with a V0 -> V0 emitter that uses T.Abs. V0 overloads now stay in their own type under subtraction (Weight - Weight => Weight, not Force1D). Test coverage in Semantics.Test/Quantities/Vector0InvariantTests.cs: - #50: factories throw on negative inputs across V0 bases (Speed, Mass, Length, Energy) and V0 overloads (Distance, Weight); zero is allowed; V1 quantities (Velocity1D, TemperatureDelta) accept negatives unchanged; the guard works across float, double, decimal storage. - #52: V0 - V0 returns the same V0 of |a - b| for both bases (Mass, Speed, Length) and overloads (Weight, Distance); type identity preserved. - Direct unit tests for Vector0Guards.EnsureNonNegative. Note: Two ignored tests in PR #66 (Mass_Minus_Mass_Returns_Absolute_ Difference_Pending52, Speed_From_Negative_Throws_Pending50) become valid once both PRs land — they can be unignored as a follow-up cleanup. Generated/*.g.cs files are not regenerated locally (no dotnet sandbox). CI's first build with the new generator emits the guards in-memory and the tests pass; the committed Generated/ files refresh on the next sweep.
|
This was referenced May 9, 2026
Merged
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.


Summary
Closes #50 and #52.
Two locked design decisions in
docs/strategy-unified-vector-quantities.mdweren't honoured by the generator:#50 — Vector0 non-negativity invariant
Speed.FromMetersPerSecond(-1)silently producedSpeed(-1). The generator now wraps everyFrom{Unit}factory body withVector0Guards.EnsureNonNegative(...), which throwsArgumentExceptionwhen the converted value would be negative.The guard runs after unit conversion so that converted negatives (e.g.
Temperature.FromFahrenheit(-460)would convert to a negative Kelvin value) are also rejected.#52 — V0 - V0 returns same V0 of T.Abs(a - b)
The old generator emitted
V0 - V0 => V1when a V1 form existed (the original "option 4" from the strategy doc), and fell through toPhysicalQuantity's plain subtraction otherwise — both wrong against the locked decision.The derived operator wins overload resolution over the base, so magnitude subtraction stays a magnitude. Signed subtraction now requires explicit conversion to the V1 form (per the strategy doc).
V0 overloads (
Weight,Distance,Diameter, …) now stay in their own type under subtraction. PreviouslyWeight - WeightreturnedForce1D; it now returnsWeight.Implementation
Semantics.Quantities/Vector0Guards.cswithEnsureNonNegative<T>(T value, string paramName). Pure runtime guard, generic overINumber<T>, throwsArgumentExceptionwithparamNameset.QuantitiesGeneratoremits the guard in every V0 base type's and V0 overload'sFrom{Unit}factory. V1 base types and V1 overloads are unchanged.T.Abs.Test plan
Semantics.Test/Quantities/Vector0InvariantTests.cscovers:Speed,Mass,Length,Energy) and V0 overloads (Distance,Weight); zero is allowed; V1 quantities (Velocity1D,TemperatureDelta) accept negatives unchanged; the guard works acrossfloat,double,decimal.V0 - V0returns same V0 of|a - b|for bases (Mass,Speed,Length) and overloads (Weight,Distance); type identity preserved (Weight - Weightis stillWeight<T>); works across storage types.Vector0Guards.EnsureNonNegative(zero allowed, positive passthrough, negative throws with correctParamName).Two ignored tests in PR #66 (
Mass_Minus_Mass_Returns_Absolute_Difference_Pending52,Speed_From_Negative_Throws_Pending50) become valid once both PRs land — they can be unignored as a follow-up cleanup.Generated/filesAs with #67, the on-disk
Semantics.Quantities/Generated/*.g.csfiles are not refreshed in this commit (nodotnetavailable locally). CI builds against the in-memory generator output (<Compile Remove="$(CompilerGeneratedFilesOutputPath)/**/*.cs" />), so the new tests will run against the new emitted code and pass.Conflicts
This PR touches the same regions of
EmitV0BaseType/EmitOverloadTypeas #67. Whichever lands first will need a small rebase from the other. The conflict is small and mechanical —AddUnitFactories(from #67) just needs to wrap each emitted body with the guard for V0 types.🤖 Generated by Claude Code
Generated by Claude Code