Skip to content
ericeallenPublic

About

A calculus of units of measure with conversion, mechanized in Lean 4

Topics

Resources

Stars

0 stars

Watchers

1 watching

Forks

Repository files navigation

Λs: a calculus for units of measure with conversion

CI Docs

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.

Status

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.

Build

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-checks

lake 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.

What is in here

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 and the module documentation

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.

Theorem index

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).

Auditing the trust base

lake env lean scripts/Audit.lean > /tmp/axioms.txt
python3 scripts/check_axioms.py /tmp/axioms.txt

The 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 sorry or admit in the sources;
  • runs scripts/verify_theorems_index.py and scripts/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 than propext, Classical.choice, and Quot.sound;
  • runs the binary, failing if it exits nonzero or if its output lacks the passing numeric, declaration-solver, Jacobi, and declared-conversion lines.

Trusted base

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.

Citing

@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}
}

License

Apache-2.0; see LICENSE.

About

A calculus of units of measure with conversion, mechanized in Lean 4

Topics

Resources

Stars

0 stars

Watchers

1 watching

Forks

Releases

Packages

Used by

Contributors

Languages