feat(quantities): per-overload physicalConstraints + EnsurePositive guard (closes #51)#75
Merged
matt-edmondson merged 1 commit intovectorsfrom May 10, 2026
Merged
Conversation
…uard (closes #51) Adds an optional `physicalConstraints` block to OverloadDefinition in dimensions.json. When a V0 overload declares `minExclusive: "0"`, its generated From{Unit} factories use the new Vector0Guards.EnsurePositive (which rejects zero and negative inputs) instead of the default EnsureNonNegative (which only rejects negative). The guard runs after the unit conversion to the SI base unit, mirroring the existing #50 invariant — so e.g. Wavelength.FromNanometers(0) correctly throws even though the SI value is exactly zero. Applied today to: - Wavelength (Length V0 overload) — no zero-wavelength wave - Period (Time V0 overload) — no zero-period oscillation - HalfLife (Time V0 overload) — no zero half-life The base types (Length, Duration) and other zero-allowing overloads (Distance, Latency, etc.) keep the V0 default and continue to allow zero. Verified by spot-checking the generated output: public static Wavelength<T> FromMeters(T value) => Create(Vector0Guards.EnsurePositive(value, nameof(value))); public static Length<T> FromMeters(T value) => Create(Vector0Guards.EnsureNonNegative(value, nameof(value))); Tests added in Vector0InvariantTests.cs cover zero/positive/negative across Wavelength/Period/HalfLife and the unconstrained Length/Duration/ Distance/Latency baselines, plus direct EnsurePositive helper coverage. CLAUDE.md "Resolved design decisions" §4 updated to mention the overload-level opt-in. Other dimensions/overloads that need stricter bounds in the future can extend the PhysicalConstraints class — the generator currently honours minExclusive == "0"; future fields (maxInclusive, dimension-level constraints, etc.) would need additional generator support.
3 tasks
|
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 #51.
The V0 non-negativity invariant from #50 already covers most of the constraints listed in the original issue (Temperature ≥ 0 K, Frequency ≥ 0, Pressure absolute ≥ 0). What was still open is the strict-positive case — quantities where zero is unphysical, distinct from zero-allowing magnitudes.
This PR adds the missing schema bit and applies it to the three overloads that need it.
What changed
OverloadDefinitionindimensions.jsongains an optionalphysicalConstraintsblock:{ "name": "Wavelength", "description": "...", "physicalConstraints": { "minExclusive": "0" } }When a V0 overload declares
minExclusive: "0", the generator emits itsFrom{Unit}factories with the newVector0Guards.EnsurePositivehelper (rejects zero and negative) instead of the defaultEnsureNonNegative(rejects negative only). The guard still runs after the unit conversion, soWavelength.FromNanometers(0)andWavelength.FromAngstroms(0)both throw — same flow as the #50 /Temperature.FromCelsius(-300)story.Applied today:
Wavelength(Length V0 overload)Period(Time V0 overload)HalfLife(Time V0 overload)Base types (
Length,Duration) and other zero-allowing overloads (Distance,Latency, etc.) keep the V0 default — zero stays valid for those.Spot-checked the regenerated output:
Schema scope
PhysicalConstraintsonly honoursminExclusive == "0"today. Future bounds (maxInclusive, non-zero floors, upper bounds, dimension-level constraints) are deliberately not implemented — the issue's open questions called for the strict-positive case specifically. The class shape leaves room to grow (MinExclusiveis astringto accommodate future literals) without breaking metadata that opts in today.Test plan
dotnet build Semantics.SourceGeneratorsanddotnet build Semantics.Quantitiesclean.EnsurePositiveforWavelength/Period/HalfLifeandEnsureNonNegativeeverywhere else (spot-checked).SEM00xwarnings on current metadata.dotnet test—Semantics.Testcannot restore in this sandbox (Microsoft.NETCore.App.Host.ubuntu.24.04-x64); CI is the validator. Tests inVector0InvariantTests.cscover:Wavelength.FromMeters(0.0)/FromNanometers(0.0)throwWavelength.FromMeters(550e-9)succeeds (visible-light wavelength)Period.FromSeconds(0.0)/HalfLife.FromSeconds(0.0)throwLength.FromMeters(0.0)/Duration.FromSeconds(0.0)/Distance.FromMeters(0.0)/Latency.FromSeconds(0.0)still succeedVector0Guards.EnsurePositivezero/negative/positive coverage withparamNamepropagation.Doc update
CLAUDE.md"Resolved design decisions" §4 now mentions the overload-level opt-in.https://claude.ai/code/session_01Tj63Rddvs9frqLUgsjNEP5
Generated by Claude Code