Paper: Eric Allen, A Calculus for Units of Measure with Conversion, arXiv:2609.38242 [cs.PL], 2026. This repository is its artifact.
API documentation: ericeallen.github.io/lambda-s,
generated by doc-gen4 on every push to main.
Λs is a typed lambda calculus in which conversion between units of the same dimension is a primitive, and the price of admitting it is characterized exactly. This repository is the full Lean 4 mechanization: statics, dynamics, denotational semantics, two abstraction theorems, a decidable drift diagnostic, a verified checker for unit declarations, adequacy, erasure, and the Pi theorem of dimensional analysis.
Typed unit calculi, beginning with Kennedy's, prove that well-typed programs are invariant under rescaling: no program can depend on how big a meter is. They obtain this by admitting no operation that can observe a unit, and so no conversion. Practical languages provide conversion but no invariance theorem. Λs has both. For example, converting a measurement from meters to feet and back multiplies it by reciprocal factors, so the round trip does not depend on the declared foot-to-meter ratio.
For terms without unit constants (parametric terms) the development proves two abstraction theorems. A convert-free term is invariant under every rescaling. A term with conversions is invariant under every coherent rescaling, one that scales all units of a dimension by the same factor; under any other rescaling, some conversion of a nonzero value is not invariant. For a first-order program, a verified diagnostic assigns its conversions an accumulated ratio (or drift): the ratio through which an error in the declared conversion factors would scale its answer. Where the program's value is nonzero, the ratio is trivial exactly when the program is invariant under all rescalings, and triviality is decidable. The diagnostic may instead decline to assign a ratio; a decline gives no verdict either way.
The development is 11,000 lines of definitions and proofs and 6,600 lines
of documentation (20,300 lines of source in all; the rest are blank lines).
scripts/count_lines.py counts LambdaS/*.lean, and CI checks these figures
and the table below against it.
Units and dimensions are exponent vectors over ℚ, so unit equality is vector equality and needs no normalization pass over unit syntax. Substitution on these vectors is a linear map, and its algebraic laws follow by reordering finite sums.
| Lean | 4.33.0 (pinned in lean-toolchain) |
| mathlib | pinned in lake-manifest.json |
sorry / admit |
none |
| lines of definitions and proofs | 11,000 |
| lines of documentation | 6,600 |
| theorem and lemma declarations | 588 |
| axioms | propext, Classical.choice, Quot.sound |
Examples.lean, RatioCompareExamples.lean, QM.lean, and Algorithms.lean
run the checker, the drift analysis, and the evaluator at build time
through #guard, and DeclarationSolverExamples.lean runs the declaration
solver the same way; if a stated result were different, the library would
not compile. The particle in
a box is checked this way, numerically as well as dimensionally: its
uncertainty product and its ground-state energy are #guard assertions. The
two-state system reaches the BLAS stubs, whose arithmetic does not run at
build time, so the compiled binary checks its numbers instead and CI asserts
that output.
Prerequisites: elan (which installs the pinned Lean toolchain on first use), git, and a C compiler (on macOS, the Command Line Tools).
lake exe cache get # fetch prebuilt mathlib oleans (multi-GB, needs network)
lake build # check every proof and build the lambdas executable
lake exe lambdas # run the worked examples and their numeric self-checkslake exe lambdas (entry point Main.lean) prints the particle-in-a-box,
free-fall, declared-conversion, and two-state reports, then validation lines
for the compiled evaluator, the declaration solver, and the Jacobi example.
It exits nonzero if any check fails.
Without the cache, lake build would compile mathlib from source. With it,
a GitHub-hosted Linux runner fetches the cache in under two minutes and
builds in about ten; most of that build compiles about 3,000 dependency
modules, mostly mathlib, to native code for the lambdas executable. Expect
several gigabytes of disk, almost all of it mathlib.
The BLAS shim in c/lambdas_blas.c calls Accelerate's cblas on macOS and
portable C loops elsewhere, so no separate BLAS installation is needed, and
the binary reports which backend it uses. On macOS the link goes through the
Command Line Tools SDK's libblas, because Lean's bundled linker has no
framework search path. If your Mac has Xcode but not the Command Line Tools,
change moreLinkArgs in lakefile.lean to the usr/lib directory of the
SDK that xcrun --show-sdk-path prints.
The algebra. Uom defines a unit as a ℚ-valued exponent vector, takes
its abelian group structure from mathlib, and proves the laws of rational
powers. Scaling gives rescalings and the pullback laws that
make substitution commute with them. Unify verifies elimination and
solution preservation for rational unit equations. It has no assembled unifier
or principal-type inference theorem; inference with conversion's dimension
constraints remains open in this development.
Space, Map, and Density give dimensioned vectors, linear maps whose
entry units are rank-one in Hart's sense (entry (j, i) carries W j / V i,
whatever the numerical rank), and densities.
Statics. Syntax gives types and terms, scope-indexed so ill-scoped unit and dimension
syntax is unrepresentable. Term-variable indices are checked by context lookup. Typing gives both the declarative judgment
HasTy and the checker, and the checker returns derivations: soundness
holds by construction, and completeness and decidability are proved.
Notation makes programs readable.
Dynamics. Num is the numeric carrier, a type class with a real
instance (the object of the theorems) and a Float instance (compiled, and
calling the C functions in c/). Dynamics is a definitional interpreter,
generic in the carrier and instrumented with units. Soundness proves
preservation (eval_sound). Normalization proves that evaluation
terminates (eval_terminates), by Tait-style reducibility on type
skeletons, so the fuel a definitional interpreter carries is produced by a
theorem rather than assumed; it also states unit soundness
(unit_soundness_total, Theorem 4.1). The calculus has no reduction
relation, so this is termination of the evaluator, not strong normalization.
Semantics. Parametricity builds the logical relation. Fundamental
proves both abstraction theorems (Theorems 5.1 and 5.2) and that coherence is
the exact price of conversion (Theorem 5.3). Adequacy proves that the
evaluator, run in real arithmetic with conversion factors from a valuation
that satisfies the declarations, computes the denotation, and that declared
factors reach it (Theorem 7.1). Erasure proves that an evaluator without
unit annotations or dynamic unit checks computes the same numbers
(Theorem 7.2).
Conversion and declarations. Conversion defines a conversion factor as
a ratio of one valuation's magnitudes, so path independence is a theorem, and
proves that a rescaling is coherent exactly when it factors through a
rescaling of dimensions (Scaling.coherent_iff_factors). Declare proves
the consistency criterion for declaration sets in both directions
(consistent_iff_dependencies_mul, Theorem 3.1). RationalSolver performs
executable elimination and back-substitution, with right-hand sides in any
decidable ℚ-module. LogFactor represents rational combinations of
logarithms exactly, by denominator clearing and rational product equality,
so no logarithm or root is evaluated while deciding consistency.
Determinacy and DeclarationComplete prove and execute the span tests for
individual factors and for coverage of the whole dimension kernel.
DeclareSolver combines dimensional soundness, consistency, and completeness
into a checked certificate: the checker accepts exactly the declaration sets
with all three properties (DeclSolver.check_isSome_iff, Theorem 3.2). It
returns each conversion factor exactly, as a positive rational radicand and a
positive root degree. The solver may choose reference magnitudes, but it
returns a queried factor only when the declarations determine it in every
satisfying valuation.
Accumulated ratios. Ratio gives the first-order syntax of accumulated
ratios, and Twist implements the drift analysis (Theorem 6.1 is
Twist.invariant_iff). RatioCompare compares two ratios by evaluating them
symbolically, with a fresh unit variable for each scalar component of the
inputs, so no fuel bound is needed. The comparison is exact when every input
is a scalar, vector, or matrix, even if the program applies higher-order
functions or unit binders internally (Tw.normEq_firstOrder_iff). When an
input is a function or a unit family, the analysis falls back to a sound
bounded comparison with no completeness claim. The comparison accepts
everything the bounded reducer alone accepts (Tw.normEq_of_legacy); neither
result is a completeness theorem for the drift analysis as a whole.
Dimensional analysis. Pi and PiTheorem derive Buckingham
factorization and descent to n - rank A rational dimensionless coordinates,
where A is the exponent matrix of the arguments; the factorization allows
signed outputs on positive inputs. Pi also works the pendulum: mass
appears in no dimensionless group, so the period cannot depend on it.
PiCoherent proves the dimension-level scaling law for closed programs with
conversions (unit and dimension binders may still appear inside them) and
states the paper's Pi theorem as one dichotomy (den_pi_coherent_dichotomy,
Theorem 8.1): the program factors through dimensionless groups when some
power product of its inputs has the result's dimension, and is zero
otherwise. The stronger unit-level law applies to parametric programs that
are convert-free or drift-free. NonDefinability proves that rational powers
and conversion must be primitive: no term of the arithmetic fragment has a
square root's type (NonDef.sqrt_not_definable), and no parametric
convert-free term converts between distinct base units
(NonDef.convert_not_definable). Definability proves the converse for
conversion: a single conversion of a nonzero value is invariant under every
rescaling exactly when it converts a unit to itself.
Programs. Examples, QM, and Algorithms are the worked examples,
including the yard/foot/meter declarations end to end, the particle in a
box, and the trace, determinant, and cofactor inverse of a dimensioned
endomorphism, with A⁻¹ ∘ A = I checked in the executable.
RatioCompareExamples and PiExamples check boundary cases of the exact
ratio comparison and of the Pi theorem with conversion.
DeclarationSolverExamples executes disconnected/linked units, redundant and
conflicting cycles, dimension errors, empty bases, dependent dimension rows,
and exact rational, square-root, and wavefunction-amplitude (nm^(-1/2) to
m^(-1/2)) factors. Its four kernel theorems connect
actual returned factors to every satisfying valuation. The native executable
runs this battery and exits nonzero if it fails. JacobiChecks exercises the
actual Float evaluator and native matrix operations on 125 Jacobi sweep cases
and 12 stopping cases. It checks off-diagonal elimination, symmetry, trace and
determinant preservation, unchanged diagonal inputs, and signed residuals.
These numerical checks run in the executable, separately from typing and drift
guards; a failed check makes the executable exit nonzero.
Each module carries a header docstring explaining what it is for and why it
exists; those are the intended entry points for a reader, and the groups
above are the intended reading order.
The header of LambdaS.lean defines the recurring vocabulary (parametric,
convert-free, drift) and collects the mechanization notes. A reader arriving
from the paper can look up each cited name in THEOREMS.md.
The paper states each result briefly and cites its Lean name. The module header docstrings carry the full explanations, and the API documentation renders them with every definition and theorem. Where prose and a formal statement differ, the formal statement determines the claim's scope.
THEOREMS.md lists every declaration the paper cites, and the supporting
results behind them, with the claim each supports and its file and line. Line numbers are re-derived from the
sources by scripts/verify_theorems_index.py (CI fails on drift;
--fix repairs the index in place). scripts/count_lines.py is the
method behind the size figures above (--check fails CI when the opening
paragraph's figures or the status table stop matching the sources; --fix
rewrites both).
lake env lean scripts/Audit.lean > /tmp/axioms.txt
python3 scripts/check_axioms.py /tmp/axioms.txtThe first command prints the axiom dependencies of every declaration THEOREMS.md indexes
(scripts/verify_theorems_index.py fails if any indexed declaration is
missing from the audit). The second fails unless each audited declaration has
exactly one report and each report names only propext, Classical.choice,
and Quot.sound.
CI (.github/workflows/ci.yml) runs on Linux, so it exercises the portable
C loops rather than Accelerate. It builds the library and the binary, then:
- rejects any
sorryoradmitin the sources; - runs
scripts/verify_theorems_index.pyandscripts/count_lines.py --check; - runs the audit and checks it with
scripts/check_axioms.py, which fails if any audited declaration lacks a report or depends on an axiom other thanpropext,Classical.choice, andQuot.sound; - runs the binary, failing if it exits nonzero or if its output lacks the passing numeric, declaration-solver, Jacobi, and declared-conversion lines.
A reader who believes a theorem trusts the Lean kernel and the three
axioms above. A reader who believes a number the binary prints trusts, in
addition, Lean's compiler and runtime, the Float carrier, and the three C
functions in c/lambdas_blas.c: lambdas_ddot and lambdas_dgemv, which
call Accelerate's cblas on macOS and portable loops elsewhere, and
lambdas_blas_backend, which names the backend compiled in. Proofs see only
the Lean bodies these functions replace (ddot and dgemv in
LambdaS/Num.lean); agreement with the C code is assumed, and
floating-point reordering makes it approximate. The abstraction and adequacy theorems use real
arithmetic and do not equate it with compiled floating-point arithmetic.
Rounding affects defined operations. The carriers also totalize partial
operations differently: for example, Float.pow returns NaN on negative
bases with non-integer exponents, while Real.rpow uses the real part of
the principal complex power. Concrete #guard checks execute through Lean's
compiler; they are distinct from kernel-checked theorem proofs. The binary
checks selected arithmetic and boundary cases, not a general correspondence.
@misc{allen2026units,
author = {Eric Allen},
title = {A Calculus for Units of Measure with Conversion},
year = {2026},
eprint = {2609.38242},
archivePrefix = {arXiv},
primaryClass = {cs.PL},
url = {https://arxiv.org/abs/2609.38242}
}Apache-2.0; see LICENSE.