Skip to content

feat(quantities): enforce V0 non-negativity and absolute V0-V0 subtraction (closes #50, #52) - #68

Merged
matt-edmondson merged 1 commit into
vectorsfrom
work/issue-50-52
May 9, 2026
Merged

matt-edmondson merged 1 commit into
vectorsfrom
work/issue-50-52

Conversation

@matt-edmondson

Copy link
Copy Markdown
Contributor

Summary

Closes #50 and #52.

Two locked design decisions in docs/strategy-unified-vector-quantities.md weren't honoured by the generator:

#50 — Vector0 non-negativity invariant

Speed.FromMetersPerSecond(-1) silently produced Speed(-1). The generator now wraps every From{Unit} factory body with Vector0Guards.EnsureNonNegative(...), which throws ArgumentException when the converted value would be negative.

public static Speed<T> FromMetersPerSecond(T value)
    => Create(Vector0Guards.EnsureNonNegative(value, nameof(value)));

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 => V1 when a V1 form existed (the original "option 4" from the strategy doc), and fell through to PhysicalQuantity's plain subtraction otherwise — both wrong against the locked decision.

// Now emitted on every V0 base type and V0 overload:
public static Speed<T> operator -(Speed<T> left, Speed<T> right)
    => Create(T.Abs(left.Quantity - right.Quantity));

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. Previously Weight - Weight returned Force1D; it now returns Weight.

Implementation

  • New Semantics.Quantities/Vector0Guards.cs with EnsureNonNegative<T>(T value, string paramName). Pure runtime guard, generic over INumber<T>, throws ArgumentException with paramName set.
  • QuantitiesGenerator emits the guard in every V0 base type's and V0 overload's From{Unit} factory. V1 base types and V1 overloads are unchanged.
  • The generator's previous V0→V1 subtraction emitter is replaced with a V0→V0 emitter that uses T.Abs.

Test plan

Semantics.Test/Quantities/Vector0InvariantTests.cs covers:

  • Vector0 quantities do not enforce the non-negativity invariant #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.
  • Resolve open design decision for Vector0 subtraction #52 V0 - V0 returns same V0 of |a - b| for bases (Mass, Speed, Length) and overloads (Weight, Distance); type identity preserved (Weight - Weight is still Weight<T>); works across storage types.
  • Direct unit tests for Vector0Guards.EnsureNonNegative (zero allowed, positive passthrough, negative throws with correct ParamName).

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.

⚠️ Note about committed Generated/ files

As with #67, the on-disk Semantics.Quantities/Generated/*.g.cs files are not refreshed in this commit (no dotnet available 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 / EmitOverloadType as #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

…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.
@sonarqubecloud

sonarqubecloud Bot commented May 9, 2026

Copy link
Copy Markdown

Quality Gate Failed Quality Gate failed

Failed conditions
1 Security Hotspot
0.0% Coverage on New Code (required ≥ 80%)

See analysis details on SonarQube Cloud

@matt-edmondson
matt-edmondson merged commit 0ee52bd into vectors May 9, 2026
4 of 5 checks passed
@matt-edmondson
matt-edmondson deleted the work/issue-50-52 branch May 9, 2026 11:49
Damon3000s pushed a commit to Damon3000s/Semantics that referenced this pull request Jul 10, 2026
…tor output

- Adds DimensionsMetadata.Validate() and a SEM002 Roslyn diagnostic so
  malformed entries in dimensions.json (missing name/symbol, empty
  availableUnits, duplicate type names, no vector forms declared)
  surface in the build log instead of crashing the emit pass or dropping
  output silently. Pairs with the existing SEM001 (closes ktsu-dev#60).
- Adds AnalyzerReleases.Shipped/Unshipped.md so SEM001/SEM002 satisfy
  RS2008 analyzer release tracking; the generator project now builds
  clean.
- Re-runs QuantitiesGenerator to refresh 132 committed .g.cs files that
  were stale relative to the V0 non-negativity / absolute V0-V0
  subtraction work merged in ktsu-dev#66/ktsu-dev#68. Diff is purely the documented
  ktsu-dev#50/ktsu-dev#52 changes (Vector0Guards.EnsureNonNegative in factories;
  same-type T.Abs subtraction replacing the old V1-typed result).
- CLAUDE.md: corrects the V0 invariant entry (constraints are enforced
  structurally, not yet declared in metadata), fixes the
  PhysicalConstants surface to match what's actually emitted (no
  Conversion sub-class), and lists the SEM00x diagnostics.
Damon3000s pushed a commit to Damon3000s/Semantics that referenced this pull request Jul 10, 2026
…0 non-negativity

Resolves the conflict between ktsu-dev#48 (multi-unit From{Unit} factories) and ktsu-dev#68
(V0 non-negativity guard, V0-V0 absolute subtraction).

QuantitiesGenerator.AddUnitFactories now takes an applyV0Guard parameter
and wraps the per-unit conversion expression with
Vector0Guards.EnsureNonNegative when emitting V0 base types and V0
overloads. The guard runs *after* the conversion to base SI units, so
inputs that are non-negative in the source unit but negative in the
target unit (Temperature.FromCelsius(-300) -> -26.85 K) still throw
ArgumentException.

V1 base types and V1 overloads continue to accept any sign — same call
site, applyV0Guard: false.

The V0-V0 absolute subtraction operator from ktsu-dev#52 is preserved on every
V0 base and V0 overload (Create(T.Abs(left.Quantity - right.Quantity))),
hiding the inherited PhysicalQuantity subtraction so the magnitude
invariant holds.

Also pulls in the AnalyzerReleases.Shipped.md / .Unshipped.md files
from ktsu-dev#70 so the SEM001 diagnostic introduced on this branch satisfies
RS2008 and the source-generator project builds clean.

Regenerates Semantics.Quantities/Generated/ with the merged generator.
Spot-checked Mass (Gram/Ton/Pound/Ounce factories all guarded),
Temperature (FromKelvin/FromCelsius/FromFahrenheit all wrap the
post-conversion value), Length (FromMile/FromFoot/FromYard with guard),
Velocity1D (FromMilesPerHour/FromKilometersPerHour without guard).
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