Skip to content

Units are constexpr, convert by one compile-time map, and reject mixed dimensions by concept - #592

Merged
RyeMutt merged 1 commit into
developfrom
rye/llunits-constexpr
Oct 27, 2026
Merged

RyeMutt merged 1 commit into
developfrom
rye/llunits-constexpr

Conversation

@RyeMutt

@RyeMutt RyeMutt commented Oct 27, 2026

Copy link
Copy Markdown
Member

LLUnit and LLUnitImplicit are constexpr throughout, convert between units through one map worked out at compile time, and reject what does not mix by concept, with a deleted overload for each rejection.

Conversions

A unit's conversion from its base unit is an ALUnitConversion (alunitconversion.h), (x * m + c) / d, built from the text the macros already took: ALUnitConversion() * 9 / 5 + 32. A conversion between two units is one such map through their root unit, kept in lowest terms and, for an integer, worked in 64 bits with one division. It replaces the recursive ll_convert_units chain, the two overloads each LL_DECLARE_DERIVED_UNIT generated, LLUnitLinearOps/LLUnitInverseLinearOps and sLevel. The macros stay as wrappers, and a unit can also be written as a type (ALBaseUnit, ALDerivedUnit).

Fixed on the way, each with a test that fails against the old header:

  • A second offset went in without the first step's divisor: 212 °F came to 130.35 K.
  • A divisor wider than 32 bits overflowed: U32_MAX microseconds came to 8 days.
  • A multiplication after a division dropped the divisor: / 4 * 2 doubled where it should halve.
  • S32QuatloosImplicit(1) + F32Latinum(0.375f) truncated the explicit operand into the implicit one's storage first (2, now 2.5), unlike every other pairing of the two kinds.

A floating point conversion across levels now multiplies once (3600 for hours to seconds) where it multiplied per level, so its last bit can round differently.

What does not mix

Operators between units take any mix of LLUnit and LLUnitImplicit and require ALCompatibleUnits, units of one root. A rejection is a deleted overload rather than an omitted one: a unit converts to its storage type, so an omitted operator would be taken by float arithmetic and drop the units silently. Rejected: units of different dimensions (arithmetic, comparison, construction, +=), an explicit unit plus or minus a plain value, a product of units, and *= or /= by a unit. The static-assert overloads go, and each rejection can now be asked of a concept: std::is_constructible_v<F32Seconds, F32Bytes> is false, where it was true.

The rest

  • Comparisons convert the right operand into the left one's units, and != is rewritten from ==. The relational operators stay written out rather than rewritten from <=>: MSVC /O2 compiles a floating point <=> followed by < 0 into a chain of branches.
  • Compound assignments return the unit; accessors, conversions and operators are [[nodiscard]].
  • LLUnitImplicit != LLUnit and the valueInUnits<U>(v) setter, which was const, now compile.
  • fmt::formatter for both kinds: the value by its storage type's spec, then the label, so "{:.2f}" of F32Seconds(1.5f) is 1.50s. It lives in llunittype.h on fmt/base.h: fmt 12 refuses a type with only an implicit conversion, so a formatter in another header could differ between translation units, and llunits.h is not in the PCH. Formatting a unit needs fmt/format.h where it happens, as any fmt call does.
  • The Microseconds label was a tab and <s; it is µs.

Testing

  • Windows, vs2026-os RelWithDebInfo with tests off: the whole tree built, 1581 files, no warnings.
  • llunits_test and the new alunitconversion_test (30 cases, 220 assertions) pass built outside the tree with the tree's MSVC flags, and with clang-cl at C++20 and C++23; lltrace_test compiles against the new header.
  • The viewer was run in world.

Not built here: macOS, Linux (GCC), and the llcommon test binary in a tests-on tree.

🤖 Generated with Claude Code

…d dimensions by concept

Every LLUnit and LLUnitImplicit operation is constexpr, so a conversion,
or a constant built from units, folds at compile time. A unit's conversion
from its base unit is an ALUnitConversion, (x * m + c) / d, built from the
text the macros already took (ALUnitConversion() * 9 / 5 + 32). Between two
units it is one such map through their root unit, kept in lowest terms
and, for an integer, worked in 64 bits with one division. That replaces the
recursive ll_convert_units chain, the two overloads each macro generated
and the linear-ops helpers, and fixes what the chain got wrong: a second
offset went in without the first step's divisor (212 F came to 130.35 K),
a divisor wider than 32 bits overflowed (U32_MAX microseconds came to 8
days), and a multiplication after a division dropped the divisor (/ 4 * 2
doubled where it should halve). A floating point conversion across levels
now multiplies once (3600 for hours to seconds) where it multiplied per
level.

Operators between units take any mix of LLUnit and LLUnitImplicit and
require ALCompatibleUnits, units of one root. What does not mix is a
deleted overload rather than one left out, since a unit converts to its
storage type and an operator left out would fall to float arithmetic:
units of different dimensions, an explicit unit plus or minus a plain
value, a product of units, and *= or /= by a unit. The static-assert
overloads go with it, and each rejection can now be asked of a concept.
Adding an explicit unit to an implicit one no longer truncates it into the
implicit one's storage first.

Comparisons convert the right operand into the left one's units, with !=
rewritten from ==. The relational operators stay written out: MSVC
compiles a floating point <=> followed by < 0 into a chain of branches.
LLUnitImplicit != LLUnit and the valueInUnits<U>(v) setter, which was
const, now compile, and compound assignments return the unit.

fmt::formatter formats a unit's value by its storage type's spec, then its
label. It lives in llunittype.h, on fmt/base.h, because fmt 12 refuses a
type with only an implicit conversion, so a formatter in another header
could differ between translation units. The Microseconds label was a tab
and "<s"; it is "µs".

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Oct 27, 2026

Copy link
Copy Markdown
Contributor

Warning

Review limit reached

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

Next included review available in 22 minutes.

Check out review usage here.

View limit details

Limit details: You’ve used the included review currently available.

Learn how review limits work.

Review configuration:

Run configuration
  • Source mode: Auto
  • Repository: Unavailable
  • Branch: Unavailable
  • File: No configuration file from this repository applied
  • Commit: Unavailable
  • Layers: Repository UI
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: d0f38ab7-3f67-45c7-bdac-fb8a506527a5
Commits

Reviewing files that changed from the base of the PR and between c32de66 and 732fe62.

Files selected for processing: 6
  • indra/llcommon/CMakeLists.txt
  • indra/llcommon/alunitconversion.h
  • indra/llcommon/llunits.h
  • indra/llcommon/llunittype.h
  • indra/llcommon/tests/alunitconversion_test.cpp
  • indra/llcommon/tests/llunits_test.cpp
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@RyeMutt
RyeMutt merged commit 98c9bc2 into develop Oct 27, 2026
23 checks passed
@RyeMutt
RyeMutt deleted the rye/llunits-constexpr branch October 27, 2026 19:52
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant