
Yumemi
PHP ordinarily treats meters, feet, and seconds as interchangeable numbers, allowing incorrect arguments and arithmetic to pass unnoticed. Yumemi provides static dimensional analysis for PHPStan and exact runtime unit conversion for PHP.
The PHPStan extension tracks units on ordinary int and float values. It can reject incompatible arguments and
arithmetic without requiring runtime wrapper objects. The runtime library uses the same parser, unit catalog, and
normalization engine for exact quantity arithmetic, temperature scales and other coordinate points, and conversion.
Start Here
<?php
use function jbboehr\Yumemi\unit;
/** @param unit_float<'meter'> $height */
function setDoorHeight(float $height): void {}
// @akashi-phpstan-error argument.type: unit_float<'meter'>, 6.0&unit_float<'international_foot'> given
setDoorHeight(unit(6.0, 'foot'));
PHPStan reports the unit mismatch while PHP still receives an ordinary float. In tested examples, an
@akashi-phpstan-error comment records the stable PHPStan diagnostic identifier and a distinctive fragment of the
expected message on the following statement. It is documentation-test notation, not Yumemi syntax. foot is an alias of
the catalog’s canonical international_foot unit, and diagnostics use the canonical name after resolving aliases.
- I want PHPStan to catch unit mistakes in native numbers. Start with Static Analysis.
- I need exact runtime conversion, quantity arithmetic, or values on temperature scales such as Celsius. Start with Runtime Conversion.
A branded native value is still an ordinary PHP int or float. Types such as unit_float<'meter'> add a unit
only inside PHPStan; they do not create runtime wrappers.
Browse Documentation
- Getting Started provides a complete installation and verification path.
- Core Concepts helps choose among branded native values, exact quantities, and coordinate points.
- Recipes shows common integration, conversion, custom-unit, and display tasks.
- PHPStan defines branded native types, operator inference, helpers, generic quantities, configuration, and diagnostics.
- Unit Syntax defines the expression language shared by PHPStan and the runtime.
- Runtime API documents quantities, coordinate points, conversion, numeric output, dimensions, and formatting.
- Built-in and Custom Units documents UDUNITS2 behavior, custom registries, and introspection.
Yumemi 0.1 is an initial public development release. Patch releases within the 0.1 line preserve the documented contract, while later 0.x minor releases may deliberately introduce documented breaking changes before 1.0. Architecture, implementation status, and deferred work are tracked in the repository planning document.
Getting Started
Upon the day of ashes, draw a narrow door of salt upon the chapel floor. Let the penitent cross it barefoot, naming the restitution already made, and suffer none to sweep behind them. At evening, if the door remaineth whole, their sorrow lacked weight; if their feet have broken it, admit them to the choir, and let the first hymn be for those they harmed.
— Ordinances of the Synthetic Dawn 34:72
Yumemi requires PHP 8.2 or later and the GMP extension, which provides the arbitrary-precision integers used for exact rational arithmetic and conversion.
Installation
Most applications call Yumemi at runtime and also use its PHPStan extension. Install it as a normal application dependency:
composer require jbboehr/yumemi:^0.1
Yumemi does not install PHPStan automatically in consuming projects. Install PHPStan and the extension installer as development dependencies to enable automatic registration:
composer require --dev phpstan/phpstan:^2.2.5 phpstan/extension-installer
Projects that do not use phpstan/extension-installer should install
PHPStan by itself and include Yumemi explicitly from phpstan.neon:
includes:
- vendor/jbboehr/yumemi/extension.neon
Keep jbboehr/yumemi as a normal dependency whenever application code calls functions such as unit() or unit_to(),
or uses runtime classes such as Units and Quantity. A project using Yumemi only during static analysis, with no
runtime calls or classes, may install it as a development dependency instead.
Verify Static Analysis
Configure at least one source path for PHPStan. For an application whose PHP code lives under src/, a minimal
phpstan.neon is:
parameters:
level: 8
paths:
- src
When automatic extension registration is unavailable, add the includes entry shown in Installation to
the same file.
Use unit() to brand an ordinary native value at a system boundary. PHPStan then carries the unit through arithmetic
and rejects a deliberately incorrect result. Place this example under a configured path, such as src/YumemiCheck.php:
<?php
require 'vendor/autoload.php';
use function jbboehr\Yumemi\unit;
/** @param unit_float<'meter / second'> $speed */
function saveJourneySpeed(float $speed): void {}
$distance = unit(100.0, 'meter');
$duration = unit(10.0, 'second');
$speed = $distance / $duration;
saveJourneySpeed($speed);
assert($speed === 10.0);
// @akashi-phpstan-error argument.type: unit_float<'meter / second'>, 1000.0&unit_float<'meter * second'> given
saveJourneySpeed($distance * $duration);
Run the PHPStan command used by your project, or the default executable directly:
vendor/bin/phpstan analyse
PHPStan should accept $speed and report the expected unit mismatch for the final call. The @akashi-phpstan-error
line records the diagnostic identifier and a distinctive fragment of the expected message; it is an ordinary comment,
not a required annotation. Remove the incorrect call once the extension is working.
If PHPStan instead reports unknown unit_int or unit_float PHPDoc types, the extension is not registered. Install
phpstan/extension-installer or add Yumemi’s extension.neon include explicitly.
If the deliberately incorrect call produces no diagnostic, confirm that the example file is under one of the configured
paths, that the invalid call remains in the file, and that the command is loading the phpstan.neon where Yumemi is
registered.
The runtime values remain ordinary floats. The additional unit information exists only in PHPStan’s type system.
Most applications should use Yumemi’s PHPDoc types directly. Libraries that cannot require Yumemi from every consumer
can instead use the deliberately opt-in
@yumemi-* annotation integration.
Runtime Conversion
The runtime unit engine can be used independently of PHPStan brands and extension registration. Use Units and
Quantity when the application must perform a conversion or retain exact rational values:
<?php
require 'vendor/autoload.php';
use jbboehr\Yumemi\Units;
$length = Units::default()->quantity(1, 'mile')->to('kilometer');
assert($length->exactDecimalValueIn('kilometer') === '1.609344');
assert($length->unitToString() === 'kilometer');
For more runtime-only examples, see Preserve Exact Conversion and Convert Temperatures. Continue with Core Concepts, then use the PHPStan, unit syntax, and runtime API references as needed.
Core Concepts
In the chamber above the polar night, the Angel of Boundaries unfolded a black fan, and from each rib issued a road of fire. The roads crossed without mingling, and upon them nations unlike in tongue carried one white stone toward a city not yet built. When the first foundation shone below, a voice forbade the roads to surrender their names.
— Revelation of the Artificial Sun 3:52
Yumemi exposes native values and exact runtime objects over the same unit engine. Use native values when existing PHP
numbers primarily need PHPStan protection. Use Quantity when the program must retain a multiplicative unit, perform
conversions, or preserve exact fractions. Use PointQuantity for a position on a coordinate scale, such as a
temperature in Celsius.
The examples below assume the Composer autoloader has already been loaded as shown in Getting Started.
Choose An API
| Need | Use |
|---|---|
| Add unit checking to existing PHP numbers and operators | unit_int<'...'> / unit_float<'...'> |
| Perform exact conversion or unit-aware runtime arithmetic | Quantity<'...'> |
| Represent an exact temperature or other coordinate point | PointQuantity<'...'> |
| Convert at an application boundary and return a native number | unit_to() |
A branded native value is an ordinary PHP int or float; the unit exists only in PHPStan. Branded values therefore
fit naturally into existing signatures, arrays, frameworks, serialization, and numerical code, with normal scalar
precision. Conversion is explicit through unit_to().
Quantity is the ergonomic precision path. Use it when results such as 1/3 must remain exact, rounding must be
deferred, or compatible-unit operations should be expressed through one runtime object.
PointQuantity separates coordinate points from multiplicative differences. For example, celsius identifies an
absolute temperature while delta_celsius identifies a temperature interval. Points can be converted and compared;
subtracting two points returns a Quantity, and adding or subtracting a compatible Quantity translates a point.
Points themselves do not support multiplication, division, powers, or point-plus-point arithmetic.
All three surfaces use the same parser, catalog, dimensions, and conversion semantics, but they are not equivalent
interfaces. PHP cannot make native 1 meter + 1 foot produce a correct number without converting one operand. Yumemi
therefore rejects that branded-native addition. Quantity::add() performs the conversion at runtime and accepts the
same pair.
<?php
use jbboehr\Yumemi\Quantity;
use jbboehr\Yumemi\Units;
use function jbboehr\Yumemi\unit;
use function jbboehr\Yumemi\unit_to;
/** @param unit_float<'kilometer / hour'> $speed */
function sendVehicleSpeed(float $speed): void {}
$nativeSpeed = unit(100.0, 'meter') / unit(10.0, 'second');
$nativeKilometersPerHour = unit_to($nativeSpeed, 'meter / second', 'kilometer / hour');
sendVehicleSpeed($nativeKilometersPerHour);
assert(abs($nativeKilometersPerHour - 36.0) < 1e-12);
$units = Units::default();
/** @var Quantity<'meter / second'> $exactSpeed */
$exactSpeed = $units->quantity(100, 'meter')->div($units->quantity(10, 'second'));
assert($exactSpeed->valueIn('kilometer / hour')->toString() === '36');
PHPStan knows that $nativeSpeed is branded as unit_float<'meter / second'>, but that brand does not exist at
runtime: PHP receives only an ordinary float. Runtime code therefore cannot recover the source unit, so unit_to()
requires both 'meter / second' and 'kilometer / hour' explicitly.
See the PHPStan reference for branded-native behavior and the runtime reference for exact quantities.
Native Values At Trusted Boundaries
Once a native value has a unit brand, PHP still executes ordinary int or float arithmetic. The brand adds no runtime
wrapper, method dispatch, Rational allocation, or unit metadata to those calculations. This makes branded values the
natural path when a unit contract can be established at an application boundary and the remaining work should use normal
scalar operations.
Use unit($value, 'meter') when runtime validation of the unit expression against the active catalog is useful. The
function returns $value unchanged after parsing the expression; it does not prove that the incoming magnitude was
physically measured in meters. A trusted parameter, property, return type, third-party stub, or local @var declaration
can establish the same static brand without runtime parsing:
<?php
/** @var unit_float<'meter'> $warehouseAisleLength */
$warehouseAisleLength = (float) 18;
/** @param unit_float<'meter'> $length */
function storeWarehouseAisleLength(float $length): void {}
storeWarehouseAisleLength($warehouseAisleLength);
Use declarations or stubs where the unit is already guaranteed, use unit() when catalog validation is desired, and
keep repeated arithmetic on branded native values. When exact fractions or runtime unit identity matter more, use
Quantity instead.
A trusted string-oriented contract may use unit_numeric_string<'...'> when it exposes a numeric magnitude as text. The
value remains a string until an explicit integer or float cast moves it into numerical code. See
Numeric Strings for the assignment, cast, and coercion rules.
Choose An Operation
| Goal | Operation |
|---|---|
| Brand an existing native magnitude | unit() or a unit_int / unit_float PHPDoc type |
| Label an exact magnitude without converting it | Units::quantity() |
| Parse one string containing a magnitude and unit | Units::parseQuantity() |
| Construct an exact coordinate point | Units::point() |
| Construct a multiplicative coordinate difference | Units::deltaQuantity() |
| Check whether two units share a dimension | Units::areCompatible() |
| Check whether two quantities share a context and dimension | Quantity::isCompatibleWith() |
| Check whether two points share a context and dimension | PointQuantity::isCompatibleWith() |
| Convert an exact magnitude | Units::convert() or Quantity::to() / valueIn() |
| Convert a native scalar | unit_to() or Units::convertFloat() |
| Obtain an exact multiplicative factor | Units::conversionFactor() |
| Obtain a native factor whose static units cancel | unit_factor() |
| Take an exact root without implicit unit substitution | Quantity::root() |
| Take an exact absolute value | Quantity::abs() |
| Test an exact magnitude for zero | Quantity::isZero() |
| Substitute definitions without changing stored magnitude | Quantity::normalize() |
| Substitute definitions and fold scale into magnitude | Quantity::simplify() |
| Request a particular target unit | Quantity::to() |
| Apply an application profile at an output boundary | Quantity::toPreferred() |
| Choose an engineering prefix within a named family | Quantity::toCompact() |
| Change names, symbols, typography, or division layout | Formatting APIs |
The important semantic boundaries are documented once in the references:
- Definitional equivalence and compatibility explains why native addition cannot convert its operands.
- Quantity arithmetic defines symbolic reduction and compatible-unit addition.
- Affine conversion distinguishes coordinate points from multiplicative differences.
- Normalization and simplification distinguishes definition substitution from magnitude changes.
- Preferred unit profiles apply explicit application policy without making formatting choose units.
- Compact unit selection chooses an engineering prefix without guessing a unit system.
- Native numeric output defines exact and approximate extraction.
- Custom registries defines catalog and context ownership.
When in doubt, keep conversion explicit. Symbolic algebra preserves the units chosen by the caller; catalog substitution and magnitude changes occur only through operations whose names communicate that intent.
Recipes
Write the covenant upon fresh clay and place it in the public kiln beside the vessels of common use. If the words blister while the cups endure, summon the oath-givers again; for no promise is strengthened by the fire it was fashioned to escape. But if the tablet darken without division, carry it warm between the households, and let neither claim the colder edge.
— Ordinances of the Synthetic Dawn 12:44
These short examples show common application tasks. They assume the Composer autoloader and PHPStan extension are already configured as described in Getting Started. Follow the links after each recipe for the complete semantics and limitations.
Protect An Existing API
Brand incoming data where its unit becomes known, then convert explicitly before calling an API that expects another unit:
<?php
use function jbboehr\Yumemi\unit;
use function jbboehr\Yumemi\unit_to;
/** @param unit_float<'meter'> $height */
function setRideHeight(float $height): void {}
$measuredHeight = unit(6.0, 'foot');
// @akashi-phpstan-error argument.type: unit_float<'meter'>, 6.0&unit_float<'international_foot'> given
setRideHeight($measuredHeight);
setRideHeight(unit_to($measuredHeight, 'foot', 'meter'));
See Branded Native Types and Boundary Helpers.
Keep Unit Setup Outside Hot Loops
When an external invariant already guarantees the input unit, declare that contract once and let repeated work remain ordinary native arithmetic. Compute a conversion factor before the loop so the loop itself performs only float multiplication:
<?php
use function jbboehr\Yumemi\unit_factor;
/** @var list<unit_float<'international_foot'>> $surveyLengths */
$surveyLengths = [1.0, 5.0, 10.0];
$footToMeter = unit_factor('international_foot', 'meter');
$metricLengths = [];
foreach ($surveyLengths as $surveyLength) {
$metricLengths[] = $surveyLength * $footToMeter;
}
/** @param list<unit_float<'meter'>> $lengths */
function saveMetricSurveyLengths(array $lengths): void {}
saveMetricSurveyLengths($metricLengths);
assert(abs($metricLengths[2] - 3.048) < 1e-12);
The @var declaration asserts that the source data is measured in feet; it does not validate its provenance. Use
unit() instead when parsing the unit expression against the runtime catalog is valuable. See
Native Values At Trusted Boundaries and
Constant Unit Expressions.
Preserve Exact Conversion
Use Quantity when conversion must retain an exact decimal or fraction rather than immediately becoming a float:
<?php
use jbboehr\Yumemi\Units;
$length = Units::default()->quantity(1, 'foot')->to('meter');
assert($length->valueToString() === '381/1250');
assert($length->exactDecimalValueIn('meter') === '0.3048');
assert($length->unitToString() === 'meter');
See Conversion and Comparison and Native Numeric Output.
Convert Temperatures
Temperature scales with different zero points require a full value conversion. Use PointQuantity when the coordinate
must remain attached to the value, and use a generated delta unit for temperature differences:
<?php
use jbboehr\Yumemi\Units;
use function jbboehr\Yumemi\unit_to;
$units = Units::default();
$freezing = $units->point(0, 'celsius');
$rise = $units->deltaQuantity(18, 'fahrenheit');
assert(abs(unit_to(98.6, 'fahrenheit', 'celsius') - 37.0) < 1e-12);
assert($freezing->valueIn('kelvin')->toString() === '5463/20');
assert($freezing->add($rise)->valueToString() === '10');
assert($units->point(100, 'celsius')->difference($freezing)->toString() === '100 * delta_celsius');
Do not use celsius itself in products or quotients. delta_celsius is multiplicative, and symbol formatting renders
it as Δ°C. See Affine Conversion.
Define Application Units
Put project-specific definitions in one factory, then use that factory for both PHPStan and the runtime context. This prevents one layer from accepting a unit that the other cannot resolve:
<?php
namespace App\Units;
use jbboehr\Yumemi\PHPStan\UnitRegistryFactory;
use jbboehr\Yumemi\Registry\UnitRegistry;
use jbboehr\Yumemi\Registry\UnitRegistryBuilder;
use jbboehr\Yumemi\Units;
use function jbboehr\Yumemi\unit;
use function jbboehr\Yumemi\unit_to;
final class ApplicationUnitRegistryFactory implements UnitRegistryFactory
{
public static function create(): UnitRegistry
{
return UnitRegistryBuilder::default()
->define('shipping_pallet = 48 * inch')
->alias('shipping_pallets', 'shipping_pallet')
->build();
}
}
$units = new Units(ApplicationUnitRegistryFactory::create());
$width = $units->quantity(2, 'shipping_pallets');
assert($width->exactDecimalValueIn('meter') === '2.4384');
$previous = Units::setDefault($units);
try {
$nativeWidth = unit(2, 'shipping_pallets');
assert(abs(unit_to($nativeWidth, 'shipping_pallets', 'meter') - 2.4384) < 1e-12);
} finally {
Units::setDefault($previous);
}
Select the same factory for PHPStan:
parameters:
yumemi:
registryFactory: App\Units\ApplicationUnitRegistryFactory
When an application unit is not derived from the seven SI axes, declare one canonical base with baseUnit() and derive
the remaining units through exact definitions. The custom-registry reference
shows this pattern for an application-owned currency-rate snapshot.
Instance methods use the registry attached to their Units context. Native helpers use the process-wide default
instead; an application may install that context once during bootstrap, while tests and scoped workers should restore
the previous context in finally. See Registry Configuration,
Custom Registries, and
Contexts And Construction for the complete lifecycle and overlay
rules.
Format Units For Display
Formatting changes spelling and typography without converting or normalizing the underlying unit:
<?php
use jbboehr\Yumemi\Formatter\FormatOptions;
use jbboehr\Yumemi\Formatter\Typography;
use jbboehr\Yumemi\Formatter\UnitNameStyle;
use jbboehr\Yumemi\Units;
$options = FormatOptions::create()
->withUnitNameStyle(UnitNameStyle::Symbol)
->withTypography(Typography::Unicode);
assert(Units::default()->format('kilogram * meter / second^2', $options) === 'kg · m / s²');
See Formatting for division styles, dimensionless output, and reusable formatters.
PHPStan Reference
Before judgment, suspend the bronze plummet above the council mosaic, and let neither advocate nor prince touch its cord. If it hangeth toward the floor, hear the cause; but if its weight rise toward the painted heavens, dismiss the court and uncover the dais, for authority hath seated itself where only witness was appointed.
— Ordinances of the Synthetic Dawn 57:34
Yumemi’s PHPStan extension attaches units to ordinary PHP int and float values and propagates them through supported
operations. It can also brand a numeric-string at a string-oriented boundary. The runtime values remain native
scalars; the additional unit identity exists only during static analysis.
The extension uses the same parser, catalog, reduction, normalization, and conversion semantics as the runtime API. See the unit syntax reference for accepted expressions and name resolution.
Most applications primarily need branded native types, operator inference, and boundary helpers. Quantity and
PointQuantity type inference becomes relevant when exact runtime objects cross analyzed code; registry configuration
and optional annotation integration are advanced topics for projects extending the catalog or integrating third-party
libraries.
| I need to… | Start with |
|---|---|
| Add a unit to an existing native number | unit() and branded types |
| Brand numeric text from a trusted API | Numeric Strings |
| Infer units through PHP operators | Native Operators |
| Convert a native magnitude | Boundary Helpers |
| Track an exact runtime quantity | Quantity Types |
| Track an exact coordinate point | Quantity Types |
| Add project-specific units | Registry Configuration |
| Suppress or baseline an error | Diagnostics |
Current boundaries: Genuinely dynamic unit strings cannot receive a precise static unit; native helpers diagnose them by default while runtime object APIs may parse them dynamically. Casts other than explicit integer/float casts and unsupported built-ins may erase a brand, and dimensional analysis cannot distinguish concepts with identical physical dimensions. See Limitations for the complete list.
Branded Native Types
unit_int<'unit'> and unit_float<'unit'> are PHPDoc types for native integers and floats with a statically known
unit. A branded value is not a wrapper or subclass: it remains an ordinary PHP number at runtime. The types work in
ordinary @param, @return, @var, generic, union, intersection, and nullable positions.
Feet are therefore distinct from meters even though both values are native floats:
<?php
/** @param unit_float<'meter'> $length */
function setPlatformHeight(float $length): void {}
/** @var unit_float<'foot'> $height */
$height = 6.0;
// @akashi-phpstan-error argument.type: unit_float<'meter'>, unit_float<'international_foot'> given
setPlatformHeight($height);
In tested examples, an @akashi-phpstan-error comment records the stable PHPStan diagnostic identifier and a
distinctive fragment of the expected message on the following statement. It is an ordinary comment used by the
documentation tests, not a Yumemi annotation.
The catalog canonicalizes aliases when it constructs a brand, which is why the diagnostic names international_foot.
The catalog reference describes canonical names, aliases, symbols, plurals, and prefixes.
Integer Constants And Ranges
Integer precision composes with a unit brand through PHPStan’s ordinary intersection syntax:
<?php
use function jbboehr\Yumemi\unit;
/** @param unit_int<'second'>&int<0, max> $delay */
function scheduleBoundedRetry(int $delay): void {}
scheduleBoundedRetry(unit(30, 'second'));
unit(30, 'second') is inferred as the branded constant 30&unit_int<'second'>. A native int<0, 100> passed to
unit($value, 'second') becomes unit_int<'second'>&int<0, 100>. Standard refinements such as positive-int and
non-negative-int may be intersected with unit_int in the same way. There is no separate unit_const_int syntax:
literal and range precision remain ordinary PHPStan types, while unit_int contributes only the unit identity.
Bounded targets enforce both parts. A bare int<0, 100> lacks the required unit, a branded value outside the range
violates the bound, and a value with another unit violates the brand. An unbounded unit_int<'second'> accepts bounded
and constant seconds.
Known float values use the same idea. unit(1.5, 'meter') is inferred as 1.5&unit_float<'meter'>: PHPStan’s ordinary
constant-float type supplies 1.5, while unit_float supplies the unit. There is no separate unit_const_float
syntax, and the intersection shown in a diagnostic is not a runtime wrapper.
Here, “known” means that PHPStan knows the actual PHP binary floating-point value. It does not make 1.5, a conversion
ratio, or a calculated result into an exact rational quantity. Use Rational or Quantity when the program must retain
exact decimal or fractional semantics at runtime.
Numeric Strings
unit_numeric_string<'unit'> brands a PHPStan numeric-string whose magnitude has a statically known unit. It is
useful when a trusted configuration or framework API represents a number as text while its contract defines the unit. At
runtime the value remains an ordinary string: Yumemi does not attach metadata, parse a combined value such as
"30 second", or validate where the magnitude came from.
A bare numeric-string does not satisfy a unit-bearing parameter, and strings branded with different units are not
interchangeable. An explicit integer or float cast preserves the brand on the resulting native number. Convert that
number through the normal unit boundary when a different unit is required. Implicit arithmetic, weak parameter coercion,
and other string-to-number conversions do not preserve the brand; cast first when entering numerical code:
<?php
interface RetryConfiguration
{
/** @return unit_numeric_string<'second'> */
public function retryDelay(): string;
}
/** @param unit_int<'second'> $delay */
function scheduleConfiguredRetry(int $delay): void {}
function applyRetryConfiguration(RetryConfiguration $configuration): void
{
scheduleConfiguredRetry((int) $configuration->retryDelay());
}
Use this type only when the external contract already guarantees both numeric syntax and the unit. It is a static declaration, not a runtime validation helper; use ordinary validation before branding data whose contents are not yet trusted.
Definitional Equivalence And Compatibility
Native arithmetic cannot change either operand’s magnitude to a different scale. Addition, subtraction, and assignment therefore require definitionally equivalent units: their normalized expressions, including scale, must match.
Units may instead be merely dimensionally compatible. meter and foot both describe length, but assigning or
adding their native magnitudes would be incorrect without conversion. Use unit_factor() or unit_to() at that
boundary. Runtime Quantity objects can perform the conversion themselves and consequently use dimensional
compatibility for add(), sub(), and comparisons.
Native Operators
Yumemi infers native unit types for unary + and - and for these binary operators:
| Operator | Static behavior |
|---|---|
+, - | Require two definitionally equivalent unit values and preserve their unit |
*, / | Multiply or divide unit expressions and reduce the result |
** | Raise the unit to a constant integer power |
% | Require two unit_int values with definitionally equivalent units |
| Comparisons | Require definitionally equivalent units and retain PHP’s native result |
Multiplication and division may combine a unit value with a bare numeric scalar. Division always produces a
unit_float; operations involving a float-like magnitude also produce a float brand. Yumemi preserves known integer
constants and signed ranges through addition, subtraction, multiplication, unary signs, and nonnegative powers. It also
preserves known float values through supported arithmetic when every required operand value is known. Exact integer
endpoint arithmetic determines the result kind:
| Mathematical result relative to PHP’s integer range | Inferred type |
|---|---|
| Entirely inside | Branded constant or bounded unit_int |
| Entirely outside | unit_float |
| Partly inside | Benevolent union of bounded unit_int and unit_float |
For a mixed result, the integer branch is clipped to values PHP can actually retain as integers. Unary negation
therefore isolates the PHP_INT_MIN case rather than treating every integer as equally likely to overflow. Modulo
preserves a branded constant when both operands are known and the divisor is nonzero; other modulo results remain an
unbounded unit_int. Finite operand unions are evaluated arm by arm, and Yumemi rejects the whole operation if any
possible pairing is invalid.
Applications that intentionally prefer PHPStan’s integer-preserving approximation for potentially overflowing arithmetic can disable float promotion:
parameters:
yumemi:
integerOverflowToFloat: false
This setting changes static inference only; it cannot alter PHP’s runtime overflow behavior. Proven-safe constants and
ranges remain precise; a potentially overflowing result widens to an unbounded unit_int because PHPStan cannot
represent an integer endpoint outside PHP’s platform range.
For example, distance divided by time is inferred as speed, while distance multiplied by time is rejected at a speed boundary:
<?php
/** @param unit_float<'meter / second'> $speed */
function saveSprintSpeed(float $speed): void {}
/** @var unit_float<'meter'> $distance */
$distance = 100.0;
/** @var unit_float<'second'> $elapsed */
$elapsed = 9.58;
saveSprintSpeed($distance / $elapsed);
// @akashi-phpstan-error argument.type: unit_float<'meter / second'>, unit_float<'meter * second'> given
saveSprintSpeed($distance * $elapsed);
Definitional equivalence understands catalog definitions such as newton = kilogram * meter / second^2. It does not
make compatible scales interchangeable: meter + foot remains an error because no runtime conversion occurs.
Equality, identity, ordering, and spaceship comparisons follow the same rule: native PHP compares the stored magnitudes
without converting either operand, so dimensionally compatible but differently scaled units remain invalid. Strict
identity may still test a nullable or other nonnumeric sentinel arm, as in $duration !== null; a bare numeric arm
remains invalid because it can participate in the magnitude comparison.
Native exponentiation requires a statically known integer exponent. For exact runtime quantities,
Quantity::root($degree) infers the rooted unit when the degree is one statically known positive integer and every
symbolic unit power is divisible by it. PHPStan cannot prove that the runtime rational magnitude has an exact root, so a
statically valid call may still throw NonExactRootException. A dynamic degree falls back to the nongeneric Quantity
return type. Native sqrt() and integer-exponent pow() support are described below. Rational exponents, approximate
real powers, and unlisted unit-transforming native functions are not part of the current model.
Casts And Scalar Functions
Explicit integer and float casts preserve the unit while changing the native numeric kind. Yumemi also tracks brands
through a small set of built-in scalar functions. Most retain the input unit; sqrt() transforms it when the symbolic
square root is exact:
| Expression | Inferred result |
|---|---|
(float) $unitInteger | Same unit, retaining a known constant |
(int) $unitFloat | Same unit, retaining a known constant |
(int) $unitNumericString | unit_int<'same unit'> |
(float) $unitNumericString | unit_float<'same unit'> |
intval() | Same as an integer cast when base is omitted or 10 |
floatval() and doubleval() | Same behavior as an explicit float cast |
abs($unitFloat) | Same unit, retaining a known float constant |
abs($unitInteger) | Branded integer bounds, with possible overflow promotion |
ceil() and floor() | Same unit, retaining a known numeric constant |
round() | Same unit, retaining supported known results |
min() and max() | Common brand, retaining known extrema or integer bounds |
array_sum() | Common brand, retaining known sums or integer bounds |
array_product() | Composed brand for sealed, statically known array shapes |
range() | List with one common endpoint and step brand |
sqrt($unitNumber) | Rooted unit, retaining a finite nonnegative constant |
fdiv($left, $right) | Quotient unit, matching native / unit algebra |
intdiv($left, $right) | Integer quotient unit with truncation toward zero |
fmod($left, $right) | Common definitionally equivalent unit |
hypot($left, $right) | Common definitionally equivalent unit |
pow($base, $exponent) | Base unit raised to a constant integer exponent |
deg2rad($degrees) | unit_float<'radian'> |
rad2deg($radians) | unit_float<'arc_degree'> |
sin(), cos(), and tan() | unit_float<'1'> from canonical radians |
asin(), acos(), and atan() | unit_float<'radian'> from an exact unscaled ratio |
atan2($y, $x) | unit_float<'radian'> from equivalent operand units |
For example, these transformations remain ordinary native PHP operations at runtime:
<?php
/**
* @param unit_float<'meter'> $offset
* @return unit_float<'meter'>
*/
function absolutePlatformOffset(float $offset): float
{
return abs($offset);
}
/** @var unit_int<'meter'> $measuredHeight */
$measuredHeight = 12;
/** @var unit_float<'meter'> $displayHeight */
$displayHeight = round((float) $measuredHeight, 1);
/** @var unit_float<'meter^2'> $platformArea */
$platformArea = 144.0;
$platformWidth = sqrt($platformArea);
/** @var unit_float<'arc_degree'> $bearing */
$bearing = 180.0;
$bearingInRadians = deg2rad($bearing);
$horizontalComponent = sin($bearingInRadians);
/** @var unit_float<'1'> $slopeRatio */
$slopeRatio = 0.5;
$inclination = asin($slopeRatio);
/** @var unit_float<'meter'> $rise */
$rise = 3.0;
/** @var unit_float<'meter'> $run */
$run = 4.0;
$direction = atan2($rise, $run);
$platformVolume = pow($platformWidth, 3);
/** @var unit_int<'meter'> $surveyedLength */
$surveyedLength = 7;
/** @var unit_int<'meter'> $additionalSurveyedLength */
$additionalSurveyedLength = 5;
$totalSurveyedLength = array_sum([$surveyedLength, $additionalSurveyedLength]);
$surveyedArea = array_product([$surveyedLength, $additionalSurveyedLength]);
/** @var unit_int<'meter'> $surveyEnd */
$surveyEnd = 11;
$surveyMarkers = range($surveyedLength, $surveyEnd);
$wholeHalfLength = intdiv($surveyedLength, 2);
assert((float) $displayHeight === 12.0);
assert((float) $platformWidth === 12.0);
assert((float) $bearingInRadians === M_PI);
assert((int) $totalSurveyedLength === 12);
assert((int) $surveyedArea === 35);
assert(count($surveyMarkers) === 5);
assert((int) $wholeHalfLength === 3);
Crossing a known integer constant to a float retains both its value and unit. Integer ranges still generalize because
PHPStan has no corresponding public float-range type. intval(), floatval(), and doubleval() follow the same brand
rules as explicit casts, including moving a unit_numeric_string brand onto the resulting number. For a branded numeric
string, intval() preserves the brand only when base is omitted or statically known to be 10; another or dynamic
base changes how the text is interpreted, so Yumemi leaves the result unbranded. The base argument does not affect
integer or float inputs. abs(), ceil(), and floor() retain a constant value when the input and result are known.
sqrt() does so for finite nonnegative inputs. round() retains a finite constant result when the input, precision,
and rounding mode are each omitted or resolve completely to supported constants. Finite precision and mode alternatives
produce the union of every possible rounded result rather than selecting one path. Dynamic arguments, invalid modes,
non-finite values, and excessively large alternative sets retain unit_float<'same unit'> without claiming a constant.
The four longstanding PHP_ROUND_HALF_* modes are supported as integer constants. On PHP 8.4 and later, their
corresponding RoundingMode enum cases are also supported when PHPStan’s configured target and the PHP runtime
executing PHPStan use the same rounding-semantics era. The four directional enum cases introduced in PHP 8.4 currently
retain the unit but generalize the value. A target/runtime mismatch across PHP 8.4 also generalizes the value because
configuring a target version does not make the analyzer execute another PHP runtime’s rounding algorithm. On PHP 8.2 and
8.3, native round() still requires the legacy integer modes; the polyfilled enum is available to Yumemi’s runtime APIs
but does not change that native signature.
min() and max() preserve a unit when every value they can return is branded with one definitionally equivalent unit.
This works with direct arguments, arrays, and unpacked arrays. When every candidate is required and is a known finite
constant, the selected integer or float value is retained. Known integer ranges are narrowed when every candidate is
required; a general array keeps its declared branded range because its runtime members are not known individually. If a
possible nonempty input contains an unbranded value or a different unit, Yumemi does not infer one brand for the result
and reports yumemi.invalidUnitSelection. A possible empty-array input does not contribute a result because native
min() and max() throw on that path.
array_sum() preserves a unit when every possible array value is a unit_int or unit_float with one definitionally
equivalent unit. Exact array shapes retain known sums and integer bounds; general integer arrays also model native
overflow promotion according to yumemi.integerOverflowToFloat. The empty result is the branded additive identity when
the array’s declared element type supplies a unit, while a literal array_sum([]) remains PHPStan’s bare integer zero.
An unbranded or differently branded possible summand reports yumemi.invalidUnitAggregation. Convert compatible units
before aggregation, and explicitly cast unit_numeric_string elements before summing them; array_sum() is not an
implicit brand-preserving numeric-string conversion.
array_product() composes the units of every factor in a sealed array shape whose possible positions are statically
known. Factors may carry different units, and an explicit bare int or float acts as a dimensionless scalar. Fixed
constants and integer ranges retain the same multiplication and overflow policy as native branded *; optional keys
produce the finite alternatives for presence and absence. A literal array of meters and seconds therefore produces a
meter-second unit, while two meter factors produce square meters.
An array with unknown cardinality cannot produce one sound symbolic unit because n values branded with meter yield
meter^n. Yumemi reports yumemi.invalidUnitAggregation rather than erasing that uncertainty. Unsealed shapes, a
possible nonempty fixed shape without any unit-bearing factor, implicit string coercion, more than 128 possible fixed
products, and derived units outside the supported exponent range report the same identifier. Cast branded numeric
strings explicitly before multiplying them. A unit-free call, including literal array_product([]), remains owned by
PHPStan’s native return type.
range() preserves a unit when both endpoints and any explicit step are branded int or float values with one
definitionally equivalent unit. A small range of known constants retains its exact list; larger or dynamic integer
endpoints retain their combined bounds, and any possible float endpoint or step contributes a branded float result. On
PHP 8.2, an explicit float step that may be NAN produces an ordinary list because that call may return an empty array;
successful ranges are otherwise non-empty. The omitted native step is interpreted contextually in the endpoints’ unit.
An explicitly supplied step must be branded even when its runtime value is 1, because native PHP carries no unit that
Yumemi could safely infer from that bare argument. Constant folding follows PHPStan’s configured target PHP version; a
known constant call rejected by that target is left to PHPStan instead of receiving a branded success type.
Mixed or incompatible endpoints and explicit steps report yumemi.invalidUnitRange. Convert compatible values before
constructing the range, and cast unit_numeric_string values explicitly. Calls with no branded arguments remain owned
by PHPStan’s native range() inference.
Unlike those preserving operations, sqrt() transforms the unit. It infers unit_float<'meter'> from either an integer
or float branded as meter^2, because native sqrt() always returns a float. Every symbolic unit power must be
divisible by two. A non-rootable brand such as meter produces yumemi.invalidUnitRoot instead of silently losing its
unit. A known negative or non-finite magnitude keeps the rooted unit but generalizes to unit_float rather than
creating a branded NAN or infinite constant.
The check uses the symbolic expression as written; it does not substitute catalog definitions before taking the root.
For example, kilometer * millimeter is dimensionally an area but lacks an exact symbolic square root. Express the
native brand with square powers, or use Quantity::simplify()->root(2) when runtime definition substitution and exact
magnitude checking are required.
For a union containing only branded numeric alternatives, Yumemi roots every alternative. Any non-rootable branded arm
produces the diagnostic. If an otherwise valid union also contains an unbranded numeric arm, PHPStan keeps its ordinary
native sqrt() result because one precise unit cannot describe every runtime path.
fdiv() follows the same unit algebra as native /: it divides two unit expressions, preserves a unit when the other
operand is a bare numeric scalar, and produces the reciprocal unit when only the divisor is branded. fmod() and
hypot() instead require both operands, across every possible numeric union arm, to carry one definitionally equivalent
unit; they report yumemi.invalidUnitMathFunction rather than infer a misleading brand from mixed or differently
branded numeric operands. Calls containing nonnumeric alternatives are left to PHPStan’s native argument checking. All
three functions return unit_float and retain a known finite result when both magnitudes are known. Non-finite results
retain only the derived brand. fdiv() also reports yumemi.invalidUnitMathFunction when the quotient unit would
exceed Yumemi’s supported exponent range.
intdiv() applies the same quotient unit algebra to integer operands and returns unit_int. Two branded operands
produce the quotient of their units; one branded operand preserves that unit or produces its reciprocal according to its
position. Known constants and integer ranges retain truncation-toward-zero bounds. A wholly bare call remains under
PHPStan’s native inference, while float or otherwise invalid operands remain under its native argument checking. Every
possible operand pairing must retain a unit; if union alternatives permit a wholly bare pairing, Yumemi reports
yumemi.invalidUnitMathFunction instead of inferring a same-carrier branded/bare union. Division by zero and
PHP_INT_MIN / -1 retain PHPStan’s native throw analysis; Yumemi keeps a conservative branded return type for those
exceptional paths rather than inventing a successful value. An unrepresentable quotient unit reports
yumemi.invalidUnitMathFunction.
pow() follows the same branded-unit contract as native **. Its base may be a branded integer or float, while every
possible exponent must be a bare constant integer from -10000 through 10000. The unit is raised to that exponent;
negative exponents produce branded floats, and nonnegative integer powers retain integer bounds and the configured
overflow-to-float policy. Finite constant exponent alternatives are evaluated independently and may produce a union of
result units. A dynamic, fractional, or unit-bearing exponent, a mixed branded-and-bare base, or a resulting unit beyond
the exponent limit reports yumemi.invalidUnitMathFunction. Wholly bare calls and calls containing nonnumeric
alternatives remain under PHPStan’s native analysis.
deg2rad() accepts arc_degree and aliases that resolve canonically to it, then returns unit_float<'radian'>.
rad2deg() accepts canonical radian aliases and returns unit_float<'arc_degree'>. Both functions accept branded
integers and floats and retain finite constant results. They do not treat merely equal-scale or dimensionless units as
angles: for example, degree_north is not an arc_degree alias, and steradian is not a radian alias. Such calls
report yumemi.invalidUnitAngleFunction rather than silently relabeling the result. Bare calls remain ordinary PHPStan
float expressions, and an otherwise valid union containing a bare numeric alternative falls back to that native
result.
sin(), cos(), and tan() likewise require canonical radian input, because native PHP does not convert another
angular scale before evaluating them. Their results are unscaled ratios branded as unit_float<'1'>. asin(),
acos(), and atan() reverse that relation: they require an expression that reduces structurally to the unscaled unit
1 and return canonical unit_float<'radian'>. A ratio such as meter / meter qualifies because its symbols cancel.
Named dimensionless units such as percent, count, radian, and steradian do not qualify merely because their
dimensions or normalized definitions are dimensionless. Convert the magnitude to 1, or deliberately rebrand an already
unscaled ratio as unit_float<'1'>, when its ratio semantics are known.
atan2($y, $x) accepts two branded operands only when their units are definitionally equivalent across every possible
union pairing. The common unit may be dimensional: meter and 100 * centimeter qualify because their scale is the
same, while meter and foot do not because native PHP performs no conversion. Mixing a branded operand with a bare
number reports yumemi.invalidUnitAngleFunction. A wholly bare call remains an ordinary PHPStan float expression. The
branded result is canonical unit_float<'radian'>.
The modeled trigonometric functions retain finite constant results. A branded call whose native result falls outside
that finite set, such as asin(unit(2.0, '1')), retains the output unit but generalizes its magnitude instead of
representing NAN as a branded constant.
The identity check applies to the unit expression statically visible at the native call. Yumemi preserves structurally
distinct alternatives such as unit_float<'arc_degree'>|unit_float<'degree_north'> so that the call fails closed.
Ordinary assignment remains definitionally based, however: passing degree_north through a parameter, property, or
return explicitly declared as only arc_degree leaves the native call with that declared type. Convert before that
boundary when the nominal distinction must remain visible.
These fixed native contracts require the configured registry’s effective radian and arc_degree semantic records and
fully resolved meanings to match the bundled canonical entries. Descriptive catalog prose and record-key order do not
affect that check, but redefinitions of dependencies such as pi or rad disable angle inference. An isolated registry
or one that changes either canonical meaning leaves angle calls to PHPStan instead of inferring a potentially false
brand. Additional aliases remain valid when they resolve to the verified canonical entries. Convert another angular
scale explicitly before calling the native function.
For branded integers, abs() retains exact constants and computes the hull of known ranges. The PHP_INT_MIN case can
produce a float at runtime, so unbounded or partially exposed ranges follow the same integerOverflowToFloat policy as
native arithmetic. With promotion enabled, their result may be a benevolent union of a nonnegative branded integer and a
branded float; disabling promotion widens the result to an unbounded unit_int.
Boundary Helpers
Yumemi provides three functions for introducing and converting native unit values:
unit($value, $unit)validates a multiplicative unit and returns the unchanged native magnitude branded asunit_intorunit_float, retaining a known scalar value.unit_factor($from, $to)returns a native conversion ratio branded asto / from. Multiplying it by a source value cancels the source unit and produces the target unit; known factors remain float constants during analysis.unit_to($value, $from, $to)performs the conversion directly and returns a float. Multiplicative targets retain aunit_floatbrand, and a statically known input and conversion retain the resulting float value.
<?php
use function jbboehr\Yumemi\unit;
use function jbboehr\Yumemi\unit_factor;
use function jbboehr\Yumemi\unit_to;
/** @param unit_float<'meter'> $meters */
function acceptHeightInMeters(float $meters): void {}
$height = unit(6, 'foot');
$byFactor = $height * unit_factor('foot', 'meter');
$converted = unit_to($height, 'foot', 'meter');
acceptHeightInMeters($byFactor);
acceptHeightInMeters($converted);
assert($height === 6);
assert(abs($byFactor - $converted) < 1e-12);
unit_factor() supports multiplicative units only. Units::conversionFactor() is the corresponding exact runtime API
and returns a Rational; the native helper returns a float so converting a branded integer promotes it to a branded
float.
unit_to() also performs affine conversions. A multiplicative target such as kelvin remains branded. An affine target
such as celsius is plain float because the current native type model cannot distinguish absolute coordinates from
delta temperatures. A known affine conversion may still produce an unbranded PHPStan float constant.
Constant helper results describe the PHP float that the corresponding runtime call will return. They do not promise
decimal exactness: for example, a converted binary float may be displayed as 0.9144000000000001 rather than 0.9144.
Use the exact runtime object APIs when that distinction matters.
If a branded value is passed to unit_to(), its brand must match the declared source unit. unit() and known helper
arguments are also validated against the configured catalog. Unknown expressions and incompatible conversions fail
analysis before the result type is used.
Constant Unit Expressions
By default, every unit argument to unit(), unit_factor(), and unit_to() must resolve during analysis to one exact
constant string or a finite set of exact alternatives. Class constants and expressions that PHPStan constant-folds are
accepted. A broad string or literal-string is not enough because Yumemi cannot recover the expression text to parse
and validate it.
Finite alternatives are accepted only when every valid path gives the operation one semantic result. Aliases such as
'meter'|'metre' are therefore valid for unit(). Several source units are valid for unit_to() when one target is
fixed, because the target determines the returned brand. Alternatives such as 'meter'|'foot' passed to unit(), or as
the target of unit_to(), are ambiguous even though PHP can represent a union of their brands.
<?php
use function jbboehr\Yumemi\unit;
/** @return unit_float<'meter'> */
function brandKnownDistanceAlias(float $value, bool $useAlias): float
{
return unit($value, $useAlias ? 'meter' : 'metre');
}
function brandDynamicDistance(float $value, string $unitExpression): float
{
// yumemi.dynamicUnitExpression
return unit($value, $unitExpression);
}
function brandAmbiguousDistance(float $value, bool $useMetric): float
{
$unitExpression = $useMetric ? 'meter' : 'foot';
// yumemi.ambiguousUnitExpression
return unit($value, $unitExpression);
}
The first rejected call does not expose its expression text. The second exposes two valid units, but they do not normalize to one semantic result.
The two diagnostics preserve the functions’ ordinary native fallback types, so an intentional dynamic boundary can use an identifier-specific local suppression:
<?php
use function jbboehr\Yumemi\unit_to;
function convertUncheckedUnitExpression(float $value, string $sourceUnit, string $targetUnit): float
{
// @phpstan-ignore yumemi.dynamicUnitExpression
return unit_to($value, $sourceUnit, $targetUnit);
}
Projects whose native-helper calls fundamentally depend on runtime strings may disable only the dynamic-expression diagnostic:
parameters:
yumemi:
requireConstantNativeUnitExpressions: false
Ambiguous finite alternatives remain errors because no one output unit applies; suppress
yumemi.ambiguousUnitExpression locally when that loss of precision is deliberate. Runtime parsing APIs such as
Units::parse() and the Quantity methods remain the intentional dynamic path and are not affected by this option.
Their constant and finite-union inference remains described below.
Statically known expressions are also subject to the shared parser resource limits. An
oversized constant helper argument reports yumemi.invalidUnitCall. If the native helper executes, it throws its usual
InvalidArgumentException and retains Parser\ExpressionLimitExceededException as the previous exception. Suppressing
the PHPStan diagnostic does not relax the runtime budget.
Quantity Types
Runtime quantities have the generic PHPStan forms Quantity<'unit'> and PointQuantity<'coordinate'>.
Units::quantity(), parseQuantity(), deltaQuantity(), and point() infer the corresponding type when their
relevant string is constant or a finite literal-string union. Fluent methods preserve or transform the generic brand
while performing the real exact operation at runtime. Finite unions of branded quantity or point receivers and operands
are evaluated arm by arm; an operation is rejected when any possible pairing is incompatible.
<?php
use jbboehr\Yumemi\Quantity;
use jbboehr\Yumemi\Units;
/** @param Quantity<'meter / second'> $speed */
function storeAverageSpeed(Quantity $speed): void {}
$units = Units::default();
$distance = $units->quantity(100, 'meter');
$duration = $units->quantity(10, 'second');
$speed = $distance->div($duration);
storeAverageSpeed($speed);
assert($speed->toString() === '10 * meter / second');
The extension models current unit-sensitive methods, including:
- arithmetic through
abs(),add(),sub(),addWithSameUnit(),subWithSameUnit(),mul(),div(),neg(),pow(), and exactroot(); - conversion through
to(),toPreferred(),toCompact(), andvalueIn(); - native extraction through
intValueIn(),exactIntValueIn(),decimalValueIn(),significantDecimalValueIn(),exactDecimalValueIn(), andfloatValueIn(); - unit transformation through
normalize()andsimplify(); - comparisons through
compareTo(),equals(),lessThan(),lessThanOrEqualTo(),greaterThan(), andgreaterThanOrEqualTo().
Quantity::isZero(), Quantity::isCompatibleWith(), and PointQuantity::isCompatibleWith() return ordinary native
bool values from their declared signatures and require no unit-specific return-type inference. A compatibility check
remains valid when PHPStan knows the dimensions differ: its result is false, not a diagnostic.
Known invalid arithmetic, construction, conversion, and comparison calls produce standalone diagnostics even when the
method result is unused. A branded magnitude supplied to Units::quantity() must match the unit being assigned:
quantity() labels an existing magnitude and does not implicitly convert it.
Integer and float extraction methods return a native brand when their target unit is known. For example,
floatValueIn('foot') returns unit_float<'international_foot'>, bridging an exact quantity back to a statically
branded native value. Decimal extraction returns a string while retaining static validation of the target unit.
An explicit target can also brand conversion and extraction results from an unbranded Quantity. PHPStan cannot prove
the unknown source dimension in that case, but it can represent the requested result. A genuinely dynamic target falls
back to an unbranded return type.
toPreferred() and toCompact() also return an unbranded Quantity: the former depends on the runtime contents of a
PreferredUnitProfile, while the latter depends on the runtime magnitude. Use to('target') when subsequent static
analysis needs one exact quantity brand.
PointQuantity<'celsius'> carries both the coordinate origin and its difference scale. Coordinate aliases are
definitionally equivalent, but different scales such as Celsius, Fahrenheit, and Kelvin remain distinct generic types
even though their points can be converted and compared. PHPStan models the affine operation rules:
PointQuantity::add()andsub()accept a dimensionally compatibleQuantityand preserve the point type;difference()accepts a compatible point and returnsQuantity<'delta-unit'>in the receiver’s scale;to()returns a point branded with the target coordinate scale;- point comparisons and numeric extraction validate constant targets and preserve their native return types.
PointQuantity::isCompatibleWith() remains an ordinary bool predicate. Unlike an operation that combines points, it
is valid to call with known incompatible point dimensions and returns false at runtime.
Direct PHPDoc may use forms such as PointQuantity<'celsius'>. Dynamic coordinate strings fall back to unbranded
PointQuantity, following the same policy as ordinary quantities.
Registry Configuration
PHPStan uses the default UDUNITS2 catalog unless parameters.yumemi.registryFactory names an autoloadable class
implementing UnitRegistryFactory. Its static create() method returns the complete immutable registry used by every
Yumemi extension path:
<?php
namespace App\PHPStan;
use jbboehr\Yumemi\Dimension;
use jbboehr\Yumemi\PHPStan\UnitRegistryFactory;
use jbboehr\Yumemi\Registry\UnitRegistry;
use jbboehr\Yumemi\Registry\UnitRegistryBuilder;
final class DocumentationRegistryFactory implements UnitRegistryFactory
{
public static function create(): UnitRegistry
{
return UnitRegistryBuilder::default()
->baseUnit('USD', Dimension::CURRENCY)
->define('EUR = 100 / 107 * USD')
->define('widget = 12 * meter')
->alias('widgets', 'widget')
->build();
}
}
Configure the factory in phpstan.neon:
parameters:
yumemi:
registryFactory: App\PHPStan\DocumentationRegistryFactory
Use UnitRegistryBuilder::default() to extend or override UDUNITS2, or UnitRegistryBuilder::empty() for an isolated
catalog. baseUnit() introduces a named primitive dimension; subsequent define() calls derive related units through
ordinary expressions. Unit definitions and primitive-dimension metadata both contribute to PHPStan’s result-cache
fingerprint.
The configured registry controls static analysis only. Applications using custom units in both layers should construct
their runtime Units context from the same factory. Instance APIs use that context directly; applications using
unit(), unit_factor(), or unit_to() should install it with Units::setDefault() and restore the previous context
in finally. PHPStan assumes one authoritative registry for an analysis run and does not track a separate catalog
identity on each value. See Contexts And Construction for runtime installation
and Custom Registries for builder and overlay semantics.
Extension-Optional Annotations
Libraries that cannot require Yumemi from every consumer can pair ordinary fallback PHPDoc with @yumemi-param,
@yumemi-return, or @yumemi-var. Enable promotion explicitly after the primary extension:
includes:
- vendor/jbboehr/yumemi/extension.neon
- vendor/jbboehr/yumemi/yumemi-tags.neon
Without yumemi-tags.neon, these are unknown tags and the ordinary PHPDoc or native types remain effective. With it,
Yumemi promotes them onto PHPStan’s normal type surface for parameters, returns, properties, and local variables.
A Yumemi tag may replace a fallback only when erasing its units produces the same PHPDoc structure. Every
unit_int<'...'> must erase to int, every unit_float<'...'> to float, every unit_numeric_string<'...'> to
numeric-string, every Quantity<'...'> to Quantity, and every PointQuantity<'...'> to PointQuantity, including
within nullable, union, intersection, and generic types. For example, unit_int<'second'>&int<0, max> erases to
int<0, max>, while 3&unit_int<'meter'> erases to 3. Parameter references and variadic markers must also match.
Union and intersection order and nullable spelling do not matter. @phpstan-* takes priority over the ordinary tag. An
already promoted @phpstan-* tag with exactly the same unit-bearing structure is accepted idempotently. Any other
mismatch leaves the fallback unchanged and reports a diagnostic.
<?php
use function jbboehr\Yumemi\unit;
/**
* @param int $length
*
* @yumemi-param unit_int<'meter'> $length
*/
function storeWarehouseLength(int $length): void {}
// @akashi-phpstan-error argument.type: unit_int<'meter'>, int given
storeWarehouseLength(5);
// @akashi-phpstan-error argument.type: unit_int<'meter'>, 3&unit_int<'international_foot'> given
storeWarehouseLength(unit(3, 'foot'));
Without tag promotion, both calls are checked against the ordinary int fallback and are valid.
The integration is opt-in because it replaces internal PHPStan parser services for analyzed source and stubs. It may conflict with another extension replacing the same services and remains an upgrade risk. Application code should normally use direct Yumemi types; integrations for third-party libraries can use ordinary PHPStan stubs or the separately packaged integrations described below.
Third-Party Integrations
Curated stubs for third-party packages live in the separately versioned
Yumemi Apocrypha package. Apocrypha uses the generic @yumemi-*
promotion mechanism above while owning package selection, supported-version policy, upstream fixtures, and integration
documentation. Keeping those concerns outside core avoids adding framework scope or dependencies to Yumemi itself.
Diagnostics
Yumemi emits stable rule identifiers so errors can be suppressed or included in a PHPStan baseline at the appropriate scope:
| Identifier | Reported condition |
|---|---|
yumemi.dynamicUnitExpression | A native helper argument does not reveal its complete unit expression during analysis |
yumemi.ambiguousUnitExpression | Native helper alternatives produce more than one semantic result unit |
yumemi.invalidUnitAggregation | Native array_sum() or array_product() cannot derive one sound unit from every possible input |
yumemi.invalidUnitAngleFunction | Native angle function received a noncanonical input, or atan2() received mixed or inequivalent operands |
yumemi.invalidUnitCall | An invalid constant unit(), unit_factor(), or unit_to() call |
yumemi.invalidUnitComparison | A native equality, identity, ordering, or spaceship comparison whose units are not definitionally equivalent |
yumemi.invalidUnitMathFunction | Native binary math received incompatible operands, an invalid exponent, or an unrepresentable result unit |
yumemi.invalidUnitRange | Native range() received mixed, unbranded, nonnumeric, or differently branded endpoints or an explicit step |
yumemi.invalidUnitRoot | Native sqrt() received a branded unit without an exact symbolic square root |
yumemi.invalidUnitSelection | Native min() or max() can return an unbranded or differently branded candidate |
yumemi.invalidQuantityConstruction | Invalid Units::quantity(), parseQuantity(), deltaQuantity(), or point() construction |
yumemi.invalidQuantityArithmetic | Invalid quantity arithmetic operands, powers, or exact-root degrees and unit expressions |
yumemi.invalidQuantityConversion | An invalid or incompatible Quantity conversion or native-extraction target |
yumemi.invalidQuantityComparison | A Quantity comparison whose statically known units are incompatible |
yumemi.invalidPointQuantityOperation | An invalid point translation, difference, conversion, extraction, or comparison |
yumemi.docTagSyntax | Invalid @yumemi-param, @yumemi-return, or @yumemi-var syntax |
yumemi.docTagDuplicate | More than one Yumemi tag targets the same fallback position |
yumemi.docTagUnsupported | A Yumemi tag appears on a declaration that does not support that tag kind |
yumemi.docTagParameter | A parameter name is unknown or an unnamed @yumemi-var fallback is ambiguous |
yumemi.docTagType | A Yumemi tag contains an invalid unit-bearing type |
yumemi.docTagTransform | Erasing the units does not reproduce the fallback PHPDoc structure |
binaryOp.invalid | Invalid native unit arithmetic; this is PHPStan’s standard binary-operation identifier, not Yumemi’s |
Call and operation diagnostics apply even when an invalid call’s result is unused. Syntax diagnostics preserve the runtime parser’s bounded caret excerpt while PHPStan anchors the error to the containing PHP or PHPDoc line.
When a Yumemi-owned diagnostic still has one exact constant unit argument, it uses the caller’s reduced symbolic
spelling, such as metres rather than meter. Inferred types and diagnostics formed after unions, arithmetic, or other
semantic joins remain canonical because no single source spelling necessarily survives those operations.
Use the identifier to choose the first corrective step:
- For
binaryOp.invalidoryumemi.invalidUnitComparison, remember that native PHP does not convert either operand. Convert explicitly withunit_to()orunit_factor(), or useQuantitywhen the operation should convert compatible units. See Definitional Equivalence And Compatibility. - For
yumemi.dynamicUnitExpressionoryumemi.ambiguousUnitExpression, provide one statically recoverable semantic result, use an identifier-specific local suppression for an intentional dynamic boundary, or choose an explicit runtime object API. See Constant Unit Expressions. - For
yumemi.invalidUnitCall,yumemi.invalidQuantityConstruction, oryumemi.invalidQuantityConversion, check the constant unit spelling, the configured registry, dimensional compatibility, and whether an affine coordinate was used where multiplicative algebra requires adelta_*unit. - For
yumemi.invalidUnitRoot, express the native brand with unit powers divisible by two. Nativesqrt()does not substitute catalog definitions; useQuantity::simplify()->root(2)when that runtime transformation is intended. - For
yumemi.invalidUnitAngleFunction, passdeg2rad()anarc_degreealias and passrad2deg()or direct trigonometric functions aradianalias. Inverse trigonometric functions require an explicitly unscaled1ratio. Convert explicitly rather than relying on dimensional or scale equivalence, for exampledeg2rad(unit_to($latitude, 'degree_north', 'arc_degree'))orasin(unit_to($grade, 'percent', '1')). When the value is already an unscaled ratio and no conversion is intended, deliberately declare it asunit_float<'1'>. Foratan2(), give both operands one definitionally equivalent brand; convert either magnitude before the call when their units are merely compatible. - For
yumemi.invalidUnitMathFunction, give bothfmod()orhypot()operands one definitionally equivalent brand, or ensure every possibleintdiv()pairing retains a brand. Reducefdiv()orintdiv()operand exponents so their quotient unit remains representable. Forpow(), use a bare constant integer exponent within the supported range and ensure the resulting unit remains representable. Convert compatible magnitudes explicitly before calling the function. - For
yumemi.invalidUnitRange, give both endpoints and any explicit step one definitionally equivalent numeric brand. Omit the step to use the contextual native default, or explicitly brand it when choosing another increment. Cast branded numeric strings before constructing the range. - For
yumemi.invalidUnitSelection, ensure every value thatmin()ormax()can return has one definitionally equivalent unit. Convert compatible but differently branded values before selecting an extreme. - For
yumemi.invalidUnitAggregation, ensure every possiblearray_sum()value has one definitionally equivalent numeric brand. Forarray_product(), use a sealed, statically known shape whose possible nonempty paths include a unit-bearing factor. Convert summands where required and explicitly cast branded numeric strings before aggregation. - For quantity arithmetic, comparison, or point diagnostics, verify the statically known dimensions and distinguish a
PointQuantitycoordinate from a multiplicative difference. Static generic types do not establish runtime context identity; objects combined at runtime must also belong to the sameUnitscontext. - For
yumemi.docTag*, confirm that the optional integration is enabled and that erasing every Yumemi unit type exactly reproduces the ordinary fallback PHPDoc structure.
Limitations
Important limits of the current static model are:
- Native
unit(),unit_factor(), andunit_to()calls require statically recoverable unit expressions by default. Dynamic object parsing and conversion remain supported, but cannot retain a specific generic unit type. - PHPStan supports one configured registry and does not track runtime registry identity per value.
- Explicit integer/float casts and
intval()/floatval()/doubleval()preserve native numeric brands and move aunit_numeric_stringbrand onto the resulting number. Implicit arithmetic and weak numeric coercion do not preserve a numeric-string brand; comparisons still require definitionally equivalent brands.abs(),ceil(),floor(),round(),min(),max(),array_sum(), andrange()preserve numeric unit brands when their operation has one sound result unit.array_product()composes brands for sealed, statically known shapes. Supported constantround()calls also retain every possible finite result; dynamic or version-incompatible policies generalize the value.sqrt()transforms exact symbolic roots,fdiv()andintdiv()follow division algebra,fmod()/hypot()require equivalent brands, andpow()raises a branded unit to a constant integer exponent.deg2rad(),rad2deg(), and the trigonometric functions enforce their canonical angle, exact-unscaled-ratio, or equivalent-operand contracts. Other unsupported casts and PHP built-ins can erase brands. - Native
+and-cannot convert dimensionally compatible magnitudes; use an explicit conversion orQuantity. - Native affine targets remain unbranded because native scalars do not retain point-versus-difference identity. Use
PointQuantity<'...'>when that identity must remain statically visible. unit_to()andunit_factor()validate the Cartesian product of independent source and target alternatives and reject the call if any pairing is invalid. Valid alternatives must also collapse to one semantic result unit.- Unit exponentiation through
**,pow(), and runtime exact-value APIs supports integer exponents only; native branded operations additionally require each possible exponent to be statically constant. - PHPStan has no corresponding native float-range syntax, so branded floats can retain known constants but not continuous bounds.
- Dimensional analysis cannot distinguish different physical meanings with the same dimension, such as gray and sievert.
Add targeted PHPStan integrations for demonstrated application workflows rather than assuming every cast, built-in, or third-party API preserves a unit brand.
Unit Syntax Reference
Within the blue cupola, bronze bees circled an empty place in the mosaic where no star had been set, and their wings sounded like rain upon a sealed roof. The learned accused the vacancy, but the swarm entered it with their labor and made no image. Thus was the unfigured place guarded from invention. Let the unfinished heaven remain open above the choir.
— Scholia of the Fifth Archive 92:41
Unit strings can be simple names such as meter, products such as kilogram * meter / second^2, or exact constants
such as 100 centimeter. Yumemi uses the same parser and catalog resolver at runtime and in its PHPStan extension, so
these strings have the same meaning in Units::parse(), Quantity, unit_int<'...'>, unit_float<'...'>, and
unit_numeric_string<'...'>.
Supported Expressions
The semantic unit language supports:
| Form | Examples | Meaning |
|---|---|---|
| Identifier | meter, international_foot, Pa | Catalog unit name, alias, symbol, or prefixed name |
| Multiplication | m * s, m s, m.s, m · s | Product of unit expressions |
| Division | meter / second | Quotient of unit expressions |
| Integer power | meter^2, second^-2, meter², second⁻² | Unit expression raised to an integer power |
| Grouping | (meter / second)^2 | Parenthesized subexpression |
| Exact constant | 1000, 1.25, 1e3, 1000 meter | Exact rational alone or scaling a unit expression |
Precedence warning: Exponentiation binds more tightly than multiplication and division. Adjacency,
*,.,·, and/otherwise share precedence and associate left, matching UDUNITS2. Thereforemeter / foot secondmeans(meter / foot) * second, notmeter / (foot * second).
Whitespace is ignored except that adjacent simple expressions imply multiplication. Use parentheses around a compound denominator:
centimeter / (foot * second)
Decimal and scientific constants are parsed exactly as rational numbers. They are not converted through binary floating point.
For example:
<?php
use jbboehr\Yumemi\Units;
$units = Units::default();
assert($units->parse('meter · second⁻²')->toString() === 'meter * second ^ -2');
assert($units->parse('1000')->toString() === '1000');
assert($units->parse('1.25 meter')->toString() === '5/4 * meter');
assert($units->parse('meter / second kilogram')->equals($units->parse('meter / second * kilogram')));
assert($units->dimension('(meter / second)^2')->toString() === 'length ^ 2 / time ^ 2');
Temperatures And Offset Units
Temperature scales such as Celsius have an offset as well as a scale. Convert them with convert(), convertFloat(),
or unit_to(). Use Units::point() when an exact value must retain its coordinate scale.
The parser form identifier @ number defines an affine coordinate origin. For example, kelvin @ 273.15 maps zero in
the new coordinate system to exactly 273.15 kelvin. areCompatible() and dimension() inspect the resulting
dimension. conversionFactor() succeeds only when the conversion has no offset and otherwise throws
NonMultiplicativeConversionException. Custom registry definitions may use the same @ form.
Affine units are not part of ordinary multiplicative expression or quantity algebra. Use their generated difference
units, such as delta_celsius, delta_fahrenheit, Δ°C, or Δ°F, when a temperature interval participates in
products, quotients, or powers. Yumemi never rewrites celsius / second implicitly; write delta_celsius / second
explicitly. See Affine Conversion for executable conversion and point operations.
Unit Names
Unit lookup is case-sensitive. Exact names always win before prefix decomposition. This matters for short symbols:
Pais pascal.pais pico-are.PAis peta-ampere.
Yumemi does not add case-folded aliases or special-case catalog-valid ambiguities. A wrong-case name is rejected even when a differently cased unit exists.
The generated UDUNITS2 catalog contains canonical names, declared aliases, symbols, explicit plurals, and unambiguous generated plurals. Runtime lookup does not guess additional plural forms. Prefixes are tried only after exact lookup, with longer prefix spellings considered first, and the remaining suffix must itself be an exact catalog name.
For example, micrometer resolves as the micro prefix applied to meter, while an exact catalog entry such as
minute is never decomposed merely because its first characters resemble a prefix.
Parsing, Resolution, And Formatting
These operations intentionally answer different questions:
Units::parse()parses the complete expression, resolves every identifier through the catalog, and reduces the resulting expression.Units::parseUnit()is an explicit alias ofUnits::parse().Units::parseQuantity()folds all explicit constants into one exact magnitude and preserves the remaining symbolic unit. Catalog conversion factors are not extracted from named units.Units::unit()resolves one catalog unit name, including dynamic prefix decomposition.Units::format()parses string input symbolically and formats the supplied spelling without requiring every name to exist in the catalog.Units::normalize()parses and resolves string input, then substitutes derived-unit definitions.
Formatting is therefore not a unit-validation API. Use parse(), parseUnit(), parseQuantity(), unit(),
conversion, compatibility, or quantity construction when unknown names must fail.
Unicode Syntax
The parser accepts the Unicode middle dot · as multiplication and superscript integers as postfix powers. Superscript
+ and - signs are accepted only when followed by at least one superscript digit. ASCII and Unicode forms can be
mixed in one expression.
Supported examples include:
m · kg / s²
(meter / second)⁺²
second⁻²
Unicode formatter output remains parser-compatible when the dimensionless style is numeric. See the runtime reference for formatting policy.
Semantic Capabilities
The parser recognizes a few UDUNITS2 forms that ordinary multiplicative expressions deliberately do not implement:
- addition and subtraction inside unit expressions, such as
meter + second; - affine-offset syntax using
@outside explicit conversion boundaries; - non-integer powers, such as
meter^0.5; - logarithmic unit definitions.
Unsupported syntax written through an expression API throws UnsupportedSyntaxException. The
runtime reference defines where standalone affine units can execute, and
Catalog Semantic Support defines the introspection model for affine, logarithmic,
and unsupported expressions.
Synthesized delta_* and Δ names are ordinary multiplicative catalog entries, not special parser syntax.
Quantity::pow() and PHPStan’s unit exponent inference likewise accept only integer powers. Quantity::root() is a
separate exact operation for positive integer degrees; it does not add fractional-power syntax to the parser. Rational
exponents and explicit approximate powers remain deferred, and a float exponent will not be silently accepted. Integer
exponents are limited to the inclusive range -10000 through 10000, including powers formed by reducing nested
expressions; root degrees are limited to 1 through 10000.
Resource Limits
Every unit-expression entry point uses the same fixed parser budget:
| Resource | $limit key | Maximum |
|---|---|---|
| Complete input | input-bytes | 4,096 bytes |
| Non-whitespace lexical tokens | token-count | 256 |
| Parenthesis nesting | nesting-depth | 64 |
| One identifier or numeric token | token-bytes | 1,024 bytes |
The $limit keys in the table are the stable discriminator values emitted by Yumemi.
Source bytes include whitespace and multibyte UTF-8 encoding. Operators and parentheses count as tokens. The token limit also bounds the size and depth of the resulting expression tree.
The shared parser reports an exceeded limit with Parser\ExpressionLimitExceededException, which extends PHP’s
LengthException and implements Yumemi’s common ExceptionInterface. Its limit, maximum, and observed properties
identify the failed budget. A token-level failure also provides a span; an input-length failure occurs before
tokenization and has no source span. The span is available through both the span property and getSpan(). Direct
parser-backed APIs such as Units::parse() throw this exception. The native unit(), unit_factor(), and unit_to()
helpers preserve their InvalidArgumentException boundary and retain the limit exception as the previous exception.
When a limit is exceeded inside a catalog or custom definition while resolving a parsed outer expression, the outer exception’s span identifies the unit name supplied by the caller. The definition-local limit failure remains available as the previous exception.
These limits apply to bundled and custom unit definitions as well as direct runtime and PHPStan parsing. They are defense in depth, not a sandbox. An application accepting unit expressions from an external boundary should still apply the smaller input and complexity policy appropriate to that boundary.
Errors And Source Locations
Malformed syntax throws Parser\ParseException. When available, its SourceSpan is a zero-based, half-open byte range
in the decoded unit expression. The exception message renders a one-based line and column plus a bounded caret excerpt.
Malformed numeric text such as 1.2.3 is reported as syntax, and the source span covers the complete malformed token.
Unknown names throw UnitNotFoundException. Parsed but unsupported constructs throw UnsupportedSyntaxException or a
more specific semantic exception. These runtime exceptions expose an optional span property using the same zero-based,
half-open byte convention. A direct failure identifies the offending name or construct. When resolution descends through
an alias or stored catalog definition, the span remains attached to the outer identifier written by the caller rather
than referring to source text that the caller did not provide.
The PHPStan extension uses the same parser and resolver, and its parse-result objects expose the same range through
errorSpan(). Its handling of constant and dynamic strings is documented in Limitations.
Runtime Reference
At the longest night, kindle the amber horizon within the underground cloister, yet veil its eastern quarter with linen and leave the brothers one hour of darkness. Let the crafted radiance warm the sick, ripen the winter figs, and guide the late pilgrim, but suffer it not to counterfeit morning. When the linen brightens from the farther side, extinguish every lamp without lament, for the lesser glory hath completed its obedience.
— Ordinances of the Synthetic Dawn 14:37
Yumemi’s runtime API provides exact unit conversion and quantity arithmetic. Rational is the authoritative magnitude
type; conversion to native integers, decimals, or floats is always explicit.
See the unit syntax reference for accepted expressions and the catalog reference for default units and custom registries.
Most applications can start with quantity construction, arithmetic, conversion, and native numeric output. The expression, dimension, formatting, and string-form sections cover lower-level manipulation and presentation when those needs arise.
Common Tasks
| I need to… | Use |
|---|---|
| Construct an exact quantity | Units::quantity() |
| Construct an exact decimal magnitude | Rational::fromDecimalString() |
| Construct an exact coordinate point | Units::point() |
| Construct a difference for a coordinate scale | Units::deltaQuantity() |
| Parse a value and unit together | Units::parseQuantity() |
| Convert a quantity | Quantity::to() |
| Apply application-preferred units | Quantity::toPreferred() |
| Choose an engineering prefix in a named unit family | Quantity::toCompact() |
| Convert a coordinate point | PointQuantity::to() |
| Preserve the exact rational result after conversion | Quantity::valueIn() |
| Obtain a minimal exact terminating decimal | Quantity::exactDecimalValueIn() |
| Obtain a rounded decimal with a requested scale | Quantity::decimalValueIn() |
| Round to a requested significant-digit precision | Quantity::significantDecimalValueIn() |
| Obtain a native binary floating-point result | Quantity::floatValueIn() |
| Convert a native scalar | unit_to() |
| Add compatible quantities | Quantity::add() |
| Reject implicit unit conversion | Quantity::addWithSameUnit() |
| Take the exact absolute value of a quantity | Quantity::abs() |
| Test whether an exact quantity is zero | Quantity::isZero() |
| Check whether two quantities are compatible | Quantity::isCompatibleWith() |
| Check whether two coordinate points are compatible | PointQuantity::isCompatibleWith() |
| Select symbols or Unicode notation | FormatOptions and formatting methods |
exactDecimalValueIn() throws when the exact rational result has a non-terminating decimal expansion. Use
decimalValueIn() or significantDecimalValueIn() with an explicit rounding mode in that case.
Every throwable explicitly created by Yumemi implements jbboehr\Yumemi\Exception\ExceptionInterface. Yumemi’s wrappers
also extend their corresponding native PHP classes, so callers may catch the common interface, a specific Yumemi
exception, or a native parent such as InvalidArgumentException. Errors raised directly by PHP or a dependency are not
covered by this marker. Direct parser-backed APIs report the shared
parser resource limits with Parser\ExpressionLimitExceededException, a
LengthException subtype. The native helpers retain their existing InvalidArgumentException boundary and chain the
limit exception as the cause.
Contexts And Construction
Units::default() returns one shared context backed by the generated UDUNITS2 catalog. Repeated calls return the same
instance, so quantities created by separate calls can be combined.
Use new Units($registry) for an isolated or customized catalog. Quantities can interact only when they belong to the
same Units instance. Combining quantities from different contexts throws IncompatibleQuantityContextException, even
when their unit strings happen to match.
Native helpers such as unit(), unit_factor(), and unit_to() use the process-wide default context. Applications
that configure the PHPStan extension with custom units can install the matching runtime context temporarily. Save and
restore the previous context in finally, especially in tests and long-running workers:
<?php
use jbboehr\Yumemi\Registry\UnitRegistryBuilder;
use jbboehr\Yumemi\Units;
use function jbboehr\Yumemi\unit_to;
$units = new Units(
UnitRegistryBuilder::default()
->define('widget = 2 * meter')
->build(),
);
$previous = Units::setDefault($units);
try {
assert(unit_to(3, 'widget', 'meter') === 6.0);
} finally {
Units::setDefault($previous);
}
setDefault(null) clears the shared context, causing the next default() call to create a fresh built-in context.
Already-created quantities retain their original context.
Create quantities through the context:
<?php
use jbboehr\Yumemi\Number\Rational;
use jbboehr\Yumemi\Units;
$units = Units::default();
$distance = $units->quantity(new Rational(3, 2), 'kilometer');
assert($distance->valueToString() === '3/2');
assert($distance->unitToString() === 'kilometer');
assert($distance->valueIn('meter')->toString() === '1500');
assert($distance->dimension()->toString() === 'length');
Quantity strings use the same expression grammar:
<?php
use jbboehr\Yumemi\Units;
$units = Units::default();
$speed = $units->parseQuantity('2 meter / (4 second)');
assert($speed->valueToString() === '1/2');
assert($speed->unitToString() === 'meter / second');
assert($units->parseQuantity('meter')->valueToString() === '1');
parseQuantity() combines every explicit numeric factor into the exact Rational magnitude. It does not extract scales
introduced by catalog resolution: 100 centimeter retains value 100 and unit centimeter, while conversions still
account for the centi prefix. A constant-only expression is dimensionless.
The public value() and unit() accessors return the exact Rational magnitude and symbolic Expr. The corresponding
public readonly properties remain available.
Exact Rational Values
Quantity and point factories accept int|Rational, not float. This prevents a binary floating-point approximation
from entering an exact calculation without an explicit conversion. Construct fractions directly, and construct decimal
or scientific values from their source strings:
<?php
use jbboehr\Yumemi\Number\Rational;
use jbboehr\Yumemi\Units;
$fraction = new Rational(3, 2);
$decimal = Rational::fromDecimalString('1.25');
$scientific = Rational::fromDecimalString('1e-3');
$length = Units::default()->quantity($decimal, 'meter');
assert($fraction->toString() === '3/2');
assert($decimal->toString() === '5/4');
assert($scientific->toString() === '1/1000');
assert($length->valueToString() === '5/4');
Rational provides exact abs(), add(), sub(), mul(), div(), integer pow(), and integer-degree root()
operations, together with compareTo(), equals(), and isZero(). A root succeeds only when the numerator and
denominator both have exact integer roots; negative values therefore accept only odd degrees. toString() returns a
fraction, toDecimalExact() requires a terminating decimal, and toDecimal() uses an explicit scale and
RoundingMode. Conversion to native values remains explicit through toInt(), toIntExact(), and toFloat(). Integer
powers, positive root degrees, and the effective decimal exponent accepted by Rational::fromDecimalString() are
limited to 10000 in magnitude. Zero powers follow PHP’s computing convention: every base, including zero, raised to
zero returns one.
Quantity Arithmetic
Multiplication and division combine magnitudes and reduce the chosen symbolic unit syntax. They do not substitute unit definitions or automatically convert compatible units:
<?php
use jbboehr\Yumemi\Units;
$units = Units::default();
$distance = $units->quantity(2, 'meter / second')->mul($units->quantity(3, 'second'));
$ratio = $units->quantity(3, 'meter')->div($units->quantity(2, 'foot'));
$displacement = $units->quantity(-3, 'meter');
assert($distance->toString() === '6 * meter');
assert($ratio->toString() === '3/2 * meter / foot');
assert($displacement->abs()->toString() === '3 * meter');
assert(!$displacement->isZero());
assert($units->quantity(0, 'meter')->isZero());
mul() and div() also accept an int or Rational scalar. abs() and neg() change only the magnitude, while
isZero() tests that exact magnitude without converting or discarding the unit. pow() raises both the magnitude and
unit expression to an integer power. pow(0) returns dimensionless one, including when the original magnitude is zero.
root() is the exact inverse for a positive integer degree when both the rational magnitude and every reduced symbolic
unit power have exact roots:
<?php
use jbboehr\Yumemi\Number\Rational;
use jbboehr\Yumemi\Units;
$rootedArea = Units::default()
->quantity(new Rational(4, 9), 'centimeter^2 / second^4')
->root(2);
assert($rootedArea->toString() === '2/3 * centimeter / second ^ 2');
The degree must be between 1 and 10000. A non-exact magnitude, an even root of a negative magnitude, or a symbolic
power not divisible by the degree throws NonExactRootException. Rooting preserves the units the caller wrote: for
example, kilometer * millimeter is not a symbolic square even though substitution reduces it to meter^2. Call
simplify() or normalize() first when that substitution is intentional, then call root() on the explicit result.
Addition and subtraction require compatible dimensions. The right operand is converted exactly into the left operand’s unit, and the result preserves the left symbolic unit:
<?php
use jbboehr\Yumemi\Units;
$units = Units::default();
$total = $units->quantity(1, 'meter')->add($units->quantity(100, 'centimeter'));
assert($total->toString() === '2 * meter');
assert($total->sub($units->quantity(1, 'foot'))->unitToString() === 'meter');
addWithSameUnit() and subWithSameUnit() reject conversions. They require definitionally equivalent normalized units
with the same scale. Thus meter and 100 * centimeter may be equivalent, but meter and centimeter are not.
Conversion And Comparison
to() returns a new quantity whose magnitude has been converted to the requested symbolic unit. valueIn() returns
only the exact converted magnitude and leaves the quantity unchanged.
compareTo(), equals(), lessThan(), lessThanOrEqualTo(), greaterThan(), and greaterThanOrEqualTo() convert a
compatible right operand exactly before comparing. Incompatible dimensions throw IncompatibleUnitException.
isCompatibleWith() checks whether two quantities belong to the same Units context and have compatible dimensions. It
returns false for a different context or dimension; it does not convert either magnitude or throw merely because the
quantities are incompatible.
Do not use PHP’s object comparison operators as unit-aware comparisons. They compare PHP object state rather than Yumemi’s conversion semantics.
<?php
use jbboehr\Yumemi\Units;
$units = Units::default();
$meter = $units->quantity(1, 'meter');
assert($meter->equals($units->quantity(100, 'centimeter')));
assert($meter->greaterThan($units->quantity(3, 'foot')));
assert($meter->lessThan($units->quantity(4, 'foot')));
assert($meter->compareTo($units->quantity(1000, 'millimeter')) === 0);
assert($meter->isCompatibleWith($units->quantity(1, 'foot')));
assert(!$meter->isCompatibleWith($units->quantity(1, 'second')));
$rate = $units->quantity(2, 'centimeter / second')->div($units->quantity(3, 'foot'));
assert($rate->toString() === '2/3 * centimeter / (foot * second)');
assert($rate->valueIn('1 / second')->toString() === '25/1143');
Preferred Unit Profiles
Use a preferred-unit profile when one application boundary should consistently select explicit output units for several dimensions:
<?php
use jbboehr\Yumemi\Units;
$units = Units::default();
$displayUnits = $units->preferredUnitProfile([
'kilometer / hour',
'kilowatt * hour',
]);
$speed = $units->quantity(25, 'meter / second')->toPreferred($displayUnits);
$duration = $units->quantity(5, 'second')->toPreferred($displayUnits);
assert($speed->valueToString() === '90');
assert($speed->unitToString() === 'kilometer / hour');
assert($duration->toPreferred($displayUnits) === $duration);
Each target expression determines its own dimension, and a profile accepts at most one target per dimension. Conversion
is exact. When no target matches, toPreferred() returns the same immutable quantity unchanged. The profile and
quantity must belong to the same Units instance so custom definitions cannot be silently reinterpreted in another
context.
A dimension does not identify a quantity’s purpose. Gray and sievert share dimensions, while information units are
dimensionless. Scope each profile to an application boundary where its dimension-to-target choices make sense; use
separate profiles or explicit to() calls when the same dimension has several meanings.
The profile is application configuration and is intentionally not serializable. Because its contents are runtime state,
PHPStan infers an unbranded Quantity from toPreferred(). Use explicit to('target') conversion when subsequent
statically checked arithmetic needs to retain one known unit brand.
Compact Unit Selection
Use toCompact() near an output boundary when the unit family is known but its engineering prefix should follow the
exact magnitude:
<?php
use jbboehr\Yumemi\Number\Rational;
use jbboehr\Yumemi\Units;
$units = Units::default();
$distance = $units->quantity(12_500, 'meter')->toCompact('meter');
$mass = $units->quantity(new Rational(1, 100), 'kilogram')->toCompact('gram');
assert($distance->valueToString() === '25/2');
assert($distance->unitToString() === 'kilometer');
assert($mass->valueToString() === '10');
assert($mass->unitToString() === 'gram');
The argument names one multiplicative unit family. Yumemi considers the unprefixed unit and registry prefixes whose
exact scales are powers of 1000, then chooses a magnitude in the half-open interval [1, 1000) when an eligible family
member exists in that range. Selection and conversion use Rational; no floating-point logarithm or approximation is
involved. Negative values use their absolute magnitude for selection and retain their sign. Zero uses the unprefixed
base.
At the registry’s prefix limits, the smallest or largest available candidate is used. A candidate name that already
exists in the catalog is accepted only when its exact conversion matches the expected prefix scale. This admits
kilogram into the gram family without mistaking an unrelated exact-name collision for a valid prefixed unit. Results
use canonical prefix and unit names; formatting may render symbols afterward. If an invalid collision leaves a gap, the
selector uses the greatest available scale not exceeding the magnitude; the result may therefore remain at or above
1000 rather than crossing into a falsely prefixed unit.
The family root must reduce to one named unit. Compound expressions, powers, and explicit numeric multipliers throw
UnsupportedUnitCompactionException; use a preferred-unit profile for a compound target. The caller must choose the
family because dimensional compatibility cannot decide among meter, foot, nautical-mile, or application-specific
policies. PHPStan returns an unbranded Quantity because the result depends on the runtime magnitude.
Native Numeric Output
Exact Rational values can be extracted after conversion:
intValueIn()followsintdiv()-style truncation toward zero and throws if the result does not fit a PHP integer.exactIntValueIn()additionally requires an integral result.decimalValueIn()returns a fixed number of decimal places using an explicitRoundingMode.significantDecimalValueIn()returns a requested number of significant decimal digits in plain or scientificDecimalNotation.exactDecimalValueIn()returns a minimal terminating decimal or throws for non-terminating rational values.floatValueIn()rounds to binary64 with ties to even. Its defaultFloatRangePolicy::Strictthrows if the result overflows to infinity or a nonzero value underflows to zero.
PHP 8.2 and 8.3 receive the PHP 8.4 RoundingMode enum through symfony/polyfill-php84.
<?php
use jbboehr\Yumemi\Units;
use jbboehr\Yumemi\Number\DecimalNotation;
use jbboehr\Yumemi\Number\FloatRangePolicy;
use jbboehr\Yumemi\Number\Rational;
$length = Units::default()->quantity(1, 'foot');
assert($length->intValueIn('meter') === 0);
assert($length->exactDecimalValueIn('meter') === '0.3048');
assert($length->decimalValueIn('meter', 2, \RoundingMode::HalfEven) === '0.30');
assert($length->significantDecimalValueIn('meter', 3, \RoundingMode::HalfEven) === '0.305');
assert($length->significantDecimalValueIn(
'meter',
3,
\RoundingMode::HalfEven,
DecimalNotation::Scientific,
) === '3.05e-1');
assert($length->floatValueIn('meter') === 0.3048);
$large = Units::default()->quantity(new Rational(gmp_pow(2, 1024)), 'meter');
assert($large->floatValueIn('meter', FloatRangePolicy::Ieee754) === INF);
Scale and precision answer different questions. A scale of 2 requests two places after the decimal point, while a
precision of 3 requests three significant digits wherever the decimal point falls. Significant output retains
fractional trailing zeros, so zero at precision 3 is 0.00 in plain notation and 0.00e+0 in scientific notation.
Plain integral text cannot distinguish whether trailing zeros are significant: use scientific notation when that
distinction must remain visible. Precision is limited to 1 through 10000, and scientific exponents use Yumemi’s
existing -10000 through 10000 bound.
Pass FloatRangePolicy::Ieee754 to Rational::toFloat(), Quantity::floatValueIn(), or
PointQuantity::floatValueIn() when binary64’s signed infinity and signed zero are preferable to range exceptions.
Finite values use the same ties-to-even rounding under either policy. This option applies only when extracting a native
float from an exact value; convertFloat(), unit_to(), and unit_factor() remain strict native-float boundaries.
Affine Conversion
Explicit conversion supports UDUNITS2 affine temperature units and custom @ definitions. The exact conversion core
maps each coordinate into canonical base units as scale * value + offset; decimal catalog constants remain exact
Rational values. A custom @ origin may be a signed integer or finite decimal literal:
<?php
use jbboehr\Yumemi\Units;
use function jbboehr\Yumemi\unit_to;
$units = Units::default();
assert($units->convert(0, 'celsius', 'kelvin')->toString() === '5463/20');
assert($units->convert(100, 'celsius', 'fahrenheit')->toString() === '212');
assert($units->convert(-40, 'celsius', 'fahrenheit')->toString() === '-40');
assert(abs($units->convertFloat(37.0, 'celsius', 'fahrenheit') - 98.6) < 1e-12);
assert(unit_to(32, 'fahrenheit', 'celsius') === 0.0);
dimension() and areCompatible() understand the affine unit’s reference dimension. conversionFactor() succeeds for
an identity or another offset-free conversion, such as celsius to an equivalent alias, but it cannot represent
celsius to kelvin because that result depends on the input value.
Use PointQuantity when a program must retain an exact coordinate point and perform affine arithmetic:
<?php
use jbboehr\Yumemi\Units;
$units = Units::default();
$freezing = $units->point(0, 'celsius');
$rise = $units->deltaQuantity(18, 'fahrenheit');
$warmer = $freezing->add($rise);
$interval = $units->point(100, 'celsius')->difference($freezing);
assert($freezing->valueIn('kelvin')->toString() === '5463/20');
assert($freezing->isCompatibleWith($units->point(32, 'fahrenheit')));
assert(!$freezing->isCompatibleWith($units->point(1, 'meter')));
assert($warmer->toString() === '10 * celsius');
assert($interval->toString() === '100 * delta_celsius');
assert($interval->valueIn('delta_fahrenheit')->toString() === '180');
A PointQuantity retains an exact Rational coordinate and a named scale. to(), valueIn(), comparisons, and native
numeric output apply full scale-and-offset conversion. difference() subtracts another compatible point and returns a
Quantity in the receiver’s delta unit. add() and sub() translate the point by a compatible Quantity while
preserving the point’s coordinate unit. isCompatibleWith() checks for the same Units context and compatible
coordinate dimensions without converting either value; a different context or dimension returns false. Operations that
combine points still throw for context or dimension incompatibility.
The catalog provides explicit multiplicative difference units such as delta_celsius, delta_fahrenheit, Δ°C, and
Δ°F. They participate in ordinary quantity and expression algebra, so delta_celsius / second is valid. Formatter
symbol mode renders the named aliases as Δ°C and Δ°F with Unicode typography.
No affine unit is silently rewritten inside algebra. celsius / second and Units::quantity(1, 'celsius') remain
invalid; use delta_celsius / second for a rate or Units::point(1, 'celsius') for a coordinate. parse(),
parseUnit(), unit(), parseQuantity(), quantity(), normalization, multiplication, division, powers, and prefixes
continue to reject the affine unit itself. Logarithmic definitions remain recognized but unevaluable.
Normalization And Simplification
normalize() and simplify() deliberately have different value behavior:
Quantity::normalize()substitutes unit definitions but does not change the stored magnitude. Scale remains in the resulting unit expression.Quantity::simplify()substitutes unit definitions and moves the normalized scale into the stored magnitude.
<?php
use jbboehr\Yumemi\Units;
$quantity = Units::default()->quantity(2, 'centimeter');
$normalized = $quantity->normalize();
$simplified = $quantity->simplify();
assert($normalized->valueToString() === '2');
assert($normalized->unitToString() === '1/100 * meter');
assert($simplified->valueToString() === '1/50');
assert($simplified->unitToString() === 'meter');
Neither operation chooses a preferred human-scale unit. Use to() for one explicit target, toPreferred() for an
application profile, or toCompact() for a caller-selected engineering-prefix family.
Expression Operations
The Units facade exposes expression-level operations:
parse()resolves and reduces a unit expression.parseUnit()is an explicit alias ofparse().parseQuantity()separates explicit constants from a symbolic unit expression and returns aQuantity.unit()resolves one catalog unit name.dimension()returns the seven-axis SI dimension vector.areCompatible()checks dimensional compatibility.conversionFactor()returns an exact value-independent factor and throwsNonMultiplicativeConversionExceptionwhen the conversion includes an offset.convert()applies an exact scale-and-offset conversion to anintorRational.convertFloat()applies the equivalent affine map in binary floating point.deltaUnit()returns the multiplicative unit used for differences on a named coordinate scale.deltaQuantity()constructs an exact difference using a coordinate scale’s multiplicative unit.point()constructs an exactPointQuantityon a named coordinate scale.normalize()substitutes derived definitions and retains their scale in the expression.
The Expr values returned by these APIs expose mul(), div(), integer pow(), exact positive integer-degree
root(), reduce(), and structural equals() operations. Expr::root() reduces the expression’s current symbolic
factors but does not normalize definitions; it throws NonExactRootException when the constant or any symbolic power
has no exact root. Those current factors depend on how the expression was obtained: parse('kilometer * millimeter')
resolves the prefixes while parsing and produces meter^2, whose square root is meter. By contrast, a quantity’s
unit() preserves the names kilometer * millimeter, so its expression has no exact symbolic square root unless the
caller explicitly simplifies or normalizes it first. Expr::root() itself performs neither substitution.
Incompatible conversions throw IncompatibleUnitException; unknown names throw UnitNotFoundException. Native float
conversion rejects non-finite inputs, results that overflow to infinity, and nonzero exact results that underflow to
zero. Exact results should use convert() instead.
For native arithmetic, unit_factor() returns the conversion factor as a float. PHPStan brands that value as the
target unit divided by the source unit, so ordinary multiplication cancels the source brand:
<?php
use function jbboehr\Yumemi\unit;
use function jbboehr\Yumemi\unit_factor;
$meters = unit(3, 'meter');
$feet = $meters * unit_factor('meter', 'foot');
assert(abs($feet - 9.84251968503937) < 1e-12);
When applying the same conversion repeatedly, calculate unit_factor() once outside the loop. The resulting branded
float can be reused with ordinary native multiplication; see
Keep Unit Setup Outside Hot Loops.
Use conversionFactor() when the exact Rational is required. Both factor APIs reject conversions involving an offset;
use convert(), convertFloat(), or unit_to() for affine conversion.
Dimensions
Units::dimension(), Quantity::dimension(), and resolved expressions expose a Dimension. Its ordinary fast path is
the seven fixed SI powers in this order:
length, mass, time, electric current, temperature, amount of substance, luminous intensity
Application registries may add sparse named axes through UnitRegistryBuilder::baseUnit(). Physical and nonphysical
axes use the same dimension algebra; for example, an application-defined currency / time dimension combines one
extension axis with the fixed SI time axis. Dimension::CURRENCY provides the conventional currency extension name
without adding a bundled unit or exchange-rate policy.
Dimensions support multiplication, division, integer powers, exact positive integer-degree roots, equality,
isDimensionless(), and the existing SI axis accessors. Dimension::root() requires every power to be divisible by the
degree. powers() retains its seven-element SI view. namedPowers() returns every nonzero SI and extension power,
powerOf() reads either kind by name, and fromNamedPowers() constructs a mixed dimension directly. Extension names
use lower snake case and format after SI axes in deterministic bytewise order.
All axes participate equally in compatibility. Dimensional equality still cannot distinguish semantically different quantities with the same dimension, such as gray and sievert; that distinction would require a separate quantity-kind model rather than another dimension subclass.
Formatting
Units::format(), Quantity::format(), and Quantity::formatUnit() accept immutable FormatOptions. Options control:
UnitNameStyle::Preserve,Canonical, orSymbolunit names;Typography::AsciiorUnicodeoperators and powers;DimensionlessStyle::One,Word, orEmptyoutput;DivisionStyle::FractionorNegativePowerslayout.
Formatting changes presentation only. It does not normalize derived definitions, convert magnitudes, or choose preferred
or compact units. Units::formatter() returns a reusable formatter for repeated calls with the same options.
The default format preserves supplied names, uses parser-compatible ASCII, renders dimensionless expressions as 1, and
uses fraction layout. Unicode output with numeric dimensionless style is also parser-compatible.
Options may be constructed with named arguments or an immutable create()->with...() chain:
<?php
use jbboehr\Yumemi\Formatter\DivisionStyle;
use jbboehr\Yumemi\Formatter\FormatOptions;
use jbboehr\Yumemi\Formatter\Typography;
use jbboehr\Yumemi\Formatter\UnitNameStyle;
use jbboehr\Yumemi\Units;
$units = Units::default();
$options = FormatOptions::create()
->withUnitNameStyle(UnitNameStyle::Symbol)
->withTypography(Typography::Unicode)
->withDivisionStyle(DivisionStyle::NegativePowers);
assert($units->format('kilometers / second^2', $options) === 'km · s⁻²');
assert($units->quantity(3, 'kilometers / second^2')->formatUnit($options) === 'km · s⁻²');
Preserve keeps the names supplied by the caller. Canonical resolves aliases, generated plurals, and one dynamic
prefix to canonical names. Symbol selects the shortest deterministic catalog symbol; ASCII falls back to a canonical
name when only Unicode symbols exist. Unknown expression leaves are preserved.
Unicode typography emits · and superscript integer powers. The parser accepts those forms, so Unicode output remains
round-trippable when the dimensionless style is One. Word and Empty are presentation-only.
NegativePowers moves denominator units to negative powers without rewriting exact rational coefficients. For example,
1/2 * meter / second becomes 1/2 * meter * second ^ -1 in ASCII. The default Fraction style retains the
denominator.
Units::format() parses string input symbolically before rendering, whereas an Expr returned by Units::parse() has
already been catalog-resolved. Formatting never recovers source spelling that has already been resolved away.
String Forms
Quantity::toString() and unitToString() use the default display formatter. valueToString() returns the exact
rational magnitude. Expr::toString() is a structural/debug representation and is not the configurable display API.
Expression equality is structural. It does not compare either display strings or normalized physical dimensions.
Debugging, JSON, And Serialization
Rational, Quantity, PointQuantity, Dimension, and the catalog descriptor value objects implement
JsonSerializable. Exact rational components are decimal strings, so JSON encoding never loses precision:
<?php
use jbboehr\Yumemi\Number\Rational;
use jbboehr\Yumemi\Units;
$quantity = Units::default()->quantity(new Rational(1, 3), 'meter / second');
$json = json_encode($quantity, JSON_THROW_ON_ERROR | JSON_UNESCAPED_SLASHES);
assert($json === '{"value":{"numerator":"1","denominator":"3"},"unit":"meter / second"}');
$restored = unserialize(serialize($quantity));
assert($restored->units() === Units::default());
assert($restored->valueToString() === '1/3');
assert($restored->unitToString() === 'meter / second');
The supported JSON keys are:
Rational:numeratoranddenominator, both decimal strings.QuantityandPointQuantity:value, containing the nestedRationalobject, andunit, containing the formatted unit string.Dimension:length,mass,time,electricCurrent,temperature,amountOfSubstance, andluminousIntensity, all integers, plus anadditionalPowersobject mapping names to integers when application-defined axes are present.PrefixDescriptor: string values formatchedName,canonicalName,matchedAs, anddefinitionExpression.PrefixDecomposition:prefixandunit, containing nested descriptor objects.UnitDescriptor:matchedName,canonicalName,matchedAs,kind,definitionExpression,documentation,comment,aliases,symbols,explicitPlurals,generatedPlurals,semantics, andprefixDecomposition. Names and enum values are strings; definition and prose fields are strings ornull; aliases, symbols, and plurals are arrays of strings; and prefix decomposition is a nested object ornull.
Descriptor JSON follows these documented properties, renders backed enums as strings, and nests dynamic prefix decomposition. JSON object key order and insignificant whitespace are not compatibility guarantees.
Compact __debugInfo() output follows the same representation. Quantities add only a short context identity; dumping a
quantity does not recursively print its Units registry and catalog.
Native serialization is versioned and verifies normalized unit semantics and resolved dimensions when restoring a
quantity or point. This detects a custom base-unit name being reassigned to another extension axis. Values created
through Units::default() may use PHP’s ordinary serialize() and unserialize() and always return to the shared
default context. A quantity from a custom registry must be restored through that context:
<?php
use jbboehr\Yumemi\Registry\UnitRegistryBuilder;
use jbboehr\Yumemi\Units;
$units = new Units(
UnitRegistryBuilder::default()
->define('smoot = 1.7018 * meter')
->build(),
);
$serialized = serialize($units->quantity(2, 'smoot'));
$restored = $units->deserialize($serialized);
assert($restored->units() === $units);
assert($restored->valueIn('meter')->toString() === '8509/2500');
Serialization compatibility applies to values emitted by PHP’s serialize() in tagged Yumemi releases. Direct calls to
__serialize() and the returned PHP array layout are implementation details. Newly serialized bytes may change while
previously released values remain readable.
Raw unserialize() rejects a custom-context quantity with an exception directing the caller to Units::deserialize().
The scoped method restores its previous context in finally, including across nested calls, and forwards PHP’s native
unserialize() options. Pass allowed_classes to restrict which classes a known graph may instantiate and max_depth
to bound nesting. The allow-list must include every serialized object class in the graph, including Dimension for new
quantity and point payloads; allowed_classes: false produces __PHP_Incomplete_Class objects and cannot restore a
quantity. Yumemi does not choose a default allow-list because deserialize() may return any caller-defined graph.
Serialized unit semantics are checked against the selected registry, so a changed or incorrect registry is rejected
rather than silently reinterpreting the value.
One serialized graph may contain default values and values from one custom context. Graphs containing values from
several distinct custom contexts require a future registry-identifier resolver. Never pass untrusted data to PHP
deserialization; allowed_classes and max_depth reduce exposure but do not make arbitrary payloads safe. Serialize
value objects directly: casting one to an array bypasses its controlled serialization representation.
Built-In and Custom Units
The painted eclipse upon the archive ceiling darkeneth no field, yet it preserveth the hour when the proud astronomers confessed their limit. Condemn not every likeness; ask whether it kneels before the event it remembers, or would supplant the heaven.
— Scholia of the Fifth Archive 84:36
Yumemi combines a generated unit catalog derived from UDUNITS2 with a small authored supplement. The same composed catalog drives runtime name resolution, conversion, formatting, and PHPStan analysis.
The imported UDUNITS2 material is distributed under the terms in UDUNITS-COPYRIGHT. Yumemi’s own code remains under the project license described in the root README and license files.
Default Catalog
Units::default() layers the checked-in data/yumemi.php supplement over the generated data/udunits2.php catalog.
The generated UDUNITS2 data includes:
- base, dimensionless, and derived units;
- canonical names, aliases, symbols, explicit plurals, and unambiguous generated plurals;
- multiplicative difference units synthesized from affine definitions, including
delta_celsius,delta_fahrenheit,Δ°C, andΔ°F; - decimal and scientific prefix definitions;
- source definitions, comments, and documentation when present upstream.
Common accepted spellings include long names such as meter, foot, second, kilogram, and celsius; symbols such
as m, ft, s, kg, and Pa; and composed expressions such as kilometer / hour. These examples are not
exhaustive. Names and symbols remain case-sensitive, and aliases such as foot may resolve to a more specific canonical
name such as international_foot.
The authored supplement provides exact units needed by image and document APIs:
| Unit | Meaning |
|---|---|
pixel | Base unit of the nominal image_sample dimension |
css_pixel | CSS reference length equal to exactly inch / 96 |
typographic_point | Modern publishing point equal to exactly inch / 72 |
twip | Twentieth of a typographic point, exactly inch / 1440 |
english_metric_unit | Office Open XML EMU, equal to exactly inch / 914400 |
The corresponding plurals are accepted, and EMU is a symbol for english_metric_unit. The CSS relationships follow
the W3C absolute-length definitions. The EMU relationship
follows the
Microsoft Office Drawing specification.
Raster pixel is deliberately not a length. A conversion from pixels to inches requires a resolution, represented by an
expression such as pixel / inch; pixel is therefore incompatible with css_pixel, inch, and meter. css_pixel
represents the separate CSS reference length. The ambiguous abbreviation px is not defined.
Existing UDUNITS2 spellings retain their meanings: pt is the US liquid-pint symbol, and pica is the historical
printer’s pica based on printers_point. Likewise, dpi and ppi remain prefix decompositions of pi, not density
units. Use typographic_point and explicit density expressions rather than relying on those abbreviations.
The supported application path for customization is UnitRegistryBuilder. Its optional alternate generated-catalog file
parameters and the concrete registry implementations are lower-level generation and testing boundaries, not stable
application APIs. The generated PHP array layout is likewise not an application data format. Use builder definitions and
aliases when an application needs custom units.
Lookup is case-sensitive. Exact names win before dynamic prefix decomposition, and prefixes apply only when the remaining suffix is an exact unit name. See the unit syntax reference for examples.
Custom Registries
Use UnitRegistryBuilder::default() to layer custom definitions and aliases over Yumemi’s supplement and UDUNITS2. Use
UnitRegistryBuilder::empty() for an isolated catalog. Calling includeUdunits2() on an empty builder adds only the
upstream catalog, without the Yumemi supplement.
Definitions use the normal unit language and are parsed against the completed registry on first use. Multiplicative
definitions work throughout the runtime. An affine definition such as degree_widget = kelvin @ 100 works as a
coordinate scale and receives a generated multiplicative delta_degree_widget definition. The affine name remains
unavailable to expression and Quantity algebra; use PointQuantity for coordinates and the generated delta name for
differences. The builder is mutable: each fluent method updates and returns the same builder. Every build() call
creates an immutable registry snapshot that is unaffected by later builder changes.
<?php
use jbboehr\Yumemi\Registry\UnitRegistryBuilder;
use jbboehr\Yumemi\Units;
$registry = UnitRegistryBuilder::default()
->define('widget = 12 * meter')
->define('degree_widget = kelvin @ 100')
->alias('widgets', 'widget')
->build();
$units = new Units($registry);
assert($units->quantity(2, 'widgets')->valueIn('meter')->toString() === '24');
assert($units->describe('widgets')?->canonicalName === 'widget');
assert($units->convert(0, 'degree_widget', 'kelvin')->toString() === '100');
assert($units->point(0, 'degree_widget')->valueIn('kelvin')->toString() === '100');
assert($units->deltaQuantity(2, 'degree_widget')->valueIn('kelvin')->toString() === '2');
Use baseUnit() when an application needs a genuinely independent primitive dimension rather than another unit derived
from the seven SI axes. The call declares the canonical base unit and its lower-snake-case dimension name atomically;
ordinary define() calls then establish exact relationships to that base:
<?php
use jbboehr\Yumemi\Dimension;
use jbboehr\Yumemi\Registry\UnitRegistryBuilder;
use jbboehr\Yumemi\Units;
$currencyRegistry = UnitRegistryBuilder::default()
->baseUnit('USD', Dimension::CURRENCY)
->define('EUR = 100 / 107 * USD')
->build();
$currencyUnits = new Units($currencyRegistry);
$currencyTotal = $currencyUnits->quantity(100, 'USD')
->add($currencyUnits->quantity(107, 'EUR'));
assert($currencyUnits->dimension('EUR')->toString() === 'currency');
assert($currencyUnits->convert(107, 'EUR', 'USD')->toString() === '100');
assert($currencyTotal->valueToString() === '200');
assert($currencyTotal->unitToString() === 'USD');
Dimension::CURRENCY is a conventional extension name, not an eighth fixed dimension. Yumemi neither ships nor fetches
exchange rates. The application owns the snapshot’s source, effective time, bid/ask and fee policy, and monetary
rounding. Choose one primitive currency per immutable registry snapshot and express every other currency through an
exact declared rate. Quantities from different snapshots retain different Units contexts and cannot be combined.
baseUnit() rejects the seven fixed SI dimension names. Define another length, mass, time, current, temperature,
substance, or luminous-intensity unit relative to its corresponding SI base with define() so its scale remains
explicit.
An overlay definition wins over a base UDUNITS2 record with the same name. Aliases resolve through the composed
registry, so an overlay alias may target either another custom definition or a base catalog unit. Affine delta synthesis
runs when build() creates the immutable snapshot, after all overlay definitions and aliases are known. An explicit
overlay name that conflicts with one of its generated delta_* or Δ names is rejected rather than silently replaced.
Reusing a bundled unit name as a custom base deliberately re-roots that name and catalog definitions that depend upon
it. Avoid doing so unless replacing that part of the effective catalog is intentional.
For PHPStan, configure one UnitRegistryFactory that returns the complete registry. Runtime code should construct its
Units context from the same registry. PHPStan assumes one authoritative registry for an analysis run and does not
track a separate catalog identity for each value.
Introspection
Units::describe() first describes an exact catalog spelling and follows aliases to its canonical entry. If no exact
entry exists, it applies the same one-prefix-plus-exact-unit decomposition used by ordinary resolution.
Units::describePrefix() describes one exact prefix name or symbol. Descriptors preserve whether the matched spelling
was canonical, an alias, a symbol, an explicit plural, a generated plural, or dynamically prefixed.
describe() does not accept compound expressions as lookup names, normalize definitions, or materialize dynamically
prefixed spellings as exact catalog entries. Generated affine-difference units are exact catalog entries. To report
truthful capabilities, introspection lazily resolves the complete canonical or dynamically prefixed spelling against the
effective registry. A dynamically prefixed descriptor exposes its prefix and exact residual unit through
prefixDecomposition:
<?php
use jbboehr\Yumemi\Catalog\CatalogNameKind;
use jbboehr\Yumemi\Catalog\UnitSemantics;
use jbboehr\Yumemi\Units;
$units = Units::default();
$kilopascal = $units->describe('kPa');
assert($kilopascal !== null);
assert($kilopascal->canonicalName === 'kilopascal');
assert($kilopascal->matchedAs === CatalogNameKind::Prefixed);
assert($kilopascal->definitionExpression === '1e3 * pascal');
assert($kilopascal->isDynamicallyPrefixed());
assert($kilopascal->semantics === UnitSemantics::Multiplicative);
assert($kilopascal->supportsMultiplicativeAlgebra());
assert($kilopascal->supportsConversion());
assert($kilopascal->prefixDecomposition !== null);
assert($kilopascal->prefixDecomposition->prefix->matchedName === 'k');
assert($kilopascal->prefixDecomposition->prefix->canonicalName === 'kilo');
assert($kilopascal->prefixDecomposition->prefix->matchedAs === CatalogNameKind::Symbol);
assert($kilopascal->prefixDecomposition->unit->matchedName === 'Pa');
assert($kilopascal->prefixDecomposition->unit->canonicalName === 'pascal');
assert($kilopascal->prefixDecomposition->unit->matchedAs === CatalogNameKind::Symbol);
The synthesized descriptor’s top-level alias, symbol, and plural lists are empty because it is not an exact catalog
entry. The residual descriptor retains the underlying unit’s complete metadata. Exact spellings still take precedence:
Pa describes the exact pascal symbol rather than decomposing as peta-are.
Prefixed affine and logarithmic names receive synthesized descriptors whose top-level semantics are
UnitSemantics::UnsupportedExpression, because the complete prefixed spelling is executable by neither runtime path.
The residual descriptor retains UnitSemantics::Affine or UnitSemantics::Logarithmic, identifying the underlying
reason. Use supportsMultiplicativeAlgebra() and supportsConversion() to inspect concrete capabilities.
Catalog Semantic Support
The expression and quantity models intentionally support only multiplicative unit algebra with integer powers.
UnitSemantics describes the capabilities of the complete name represented by a descriptor:
Multiplicativesupports expression algebra and conversion.Affinerejects expression algebra but supports explicit conversion.Logarithmicidentifies a direct logarithmic definition and supports neither operation.UnsupportedExpressionidentifies a complete expression that supports neither operation, including invalid composites, malformed or cyclic custom definitions, missing dependencies, and invalid prefixes.
Known affine and logarithmic UDUNITS2 definitions remain in the generated catalog. Aliases are classified through their
canonical entry, and direct custom @ or lg(...) definitions receive the same classification. Descriptors lazily
resolve and cache capabilities against the effective registry, so transitive definitions and overlays cannot leave
capability methods out of sync with runtime behavior.
Internal catalog records store direct or exact-name-inherited affine and logarithmic markers, but do not eagerly
materialize UnsupportedExpression or transitive composite results. Generated delta records are ordinary multiplicative
declarations materialized during catalog import or immutable-registry build. This keeps catalog generation deterministic
and avoids resolving the full catalog merely for introspection.
Affine classification means “unsupported by multiplicative Expr algebra,” not “unsupported everywhere.” See
Affine Conversion for executable boundaries and Limitations
for static-analysis behavior. Logarithmic definitions remain unsupported at every execution boundary; their descriptors
nevertheless distinguish them from unknown names and preserve the canonical unit, semantics, and original definition for
diagnostics.
Contributors changing the imported data or generator should follow Regenerating the UDUNITS2 Catalog.
Regenerating the UDUNITS2 Catalog
In the year of the dim harvest, the western gate was shut with chains, yet a child found wheat springing between its hinges. The elders preserved neither chain nor lock; they carried the green blades through every street, and the city remembered that inheritance returns first in a frail and living sign.
— Acts of the Western Court 65:48
The checked-in data/udunits2.php file is generated from the UDUNITS2 XML distribution. This procedure is for
contributors changing the importer, exporter, source package, or generated catalog; applications using Yumemi do not
need to run it.
Rebuild
Do not edit data/udunits2.php manually. In the Nix development shell, run either command:
composer generate-catalog
make generate-catalog
The flake sets UDUNITS_XML_DIR to the installed UDUNITS2 XML directory. Outside the development shell, specify an
equivalent directory explicitly:
UDUNITS_XML_DIR=/path/to/share/udunits make generate-catalog
Source Inputs
The Make target supplies these files in the order declared by the upstream udunits2.xml manifest:
udunits2-prefixes.xmludunits2-base.xmludunits2-derived.xmludunits2-accepted.xmludunits2-common.xml
The generator imports the XML, materializes aliases, plurals, semantic metadata, and affine-difference units such as
delta_celsius and Δ°C, then exports deterministic PHP through brick/varexporter. A successful rebuild should leave
no diff unless an input listed above or the importer has changed.
Verify
Run the full test suite after regeneration. The catalog smoke tests resolve every supported definition, verify generated affine-difference entries, and pin the known unsupported affine and logarithmic sets, making source-data drift explicit.
Return to Built-in and Custom Units for the user-facing catalog behavior.