Λs: a calculus for units of measure with conversion #
This is the root module; importing it imports the whole development. The
module list above follows the paper's order: syntax and typing, the checker,
the denotational semantics with both abstraction theorems, the ratio
calculus, the evaluator with its erasure, unit declarations, and the worked
examples. THEOREMS.md in the repository maps every identifier the paper
cites to its module.
From the paper's long form: Mechanization notes #
The paper's tag long-form carries this section in full; it is reproduced
here, converted to Markdown, so the documentation develops what the paper
now summarizes. Section references name the module that carries the
section; theorem references name the declaration.
The development is about eight thousand lines of Lean 4 [de Moura and Ullrich 2021] definitions and proofs, with as much again in documentation,
building with no sorry (Lean's placeholder for an unproved
obligation) and no axioms beyond the three of Lean's
standard library (propositional extensionality, choice, and quotient
soundness; the check by Lean's kernel is
part of the build, and no result adds an
axiom). The compiled evaluator calls BLAS where the platform supplies it,
falling back to portable C loops elsewhere. The
examples of “Unit Declarations” (Declare.lean) through “Dimensioned Linear Algebra” (Map.lean) run as
build-time assertions; the two numerical demonstrations whose
arithmetic reaches the foreign-function interface are checked by the
compiled binary when it runs. In this section we report what made the
mechanization small, one thing it caught, and what a reader must trust
beyond the kernel.
Exponent vectors make the metatheory linear algebra.
The decision of “Units and Dimensions” (Typing.lean) propagates through every file.
Substitution of units is a linear map; simultaneous substitution, weakening,
and their composition laws are equalities of finite sums, proved by
reordering summation rather than by structural induction. The one genuinely
dimensional lemma in the soundness proof says that grounding, the
substitution of
ground units for a term's unit variables, commutes with taking dimensions.
It is a Fubini argument: an interchange of the order of a finite double sum. In his Coq development, Kennedy [2008] reports the
substitution lemma for his logical relation as “awkward (needs equality
coercions)”; the corresponding lemmas here are changes in the order of
summation, and the value-typing relation, being indexed by syntax (a
ground type) rather than by a metalanguage type, never needs such coercions. Where Foster and Wolff [2023] implement dimension normalization in ML
because their host type system cannot reduce
L·T⁻¹·T,
our equality is definitional: no datatype of unit expressions exists
anywhere in the mechanization (“Units and Dimensions” (Typing.lean)), so a unit
expression is its own normal form.
Derivations as data collapse the trusted base.
Making the typing judgment Type-valued and the checker
derivation-returning (“Statics” (Typing.lean)) follows the
intrinsically-typed tradition [Altenkirch and Reus 1999; Poulsen et al. 2018]; the technique is
standard, and what we report is what it bought in this development. It
deleted an entire class of theorems: checker soundness, elaboration
soundness (elabConvert, which elaborates the
e in v form, returns the core term with its
derivation), and every “reconstruct the derivation”
lemma. Derivation uniqueness came free from
the theorem “Completeness” (check_eq, Typing.lean), and with it the right to define semantics
by recursion on derivations while stating side conditions on terms.
An environment-passing normalizer avoids Kripke structure.
The ratio normalizer of “Accumulated Ratios, and a Decidable Diagnostic” (Twist.lean) must interpret a binder
whose body lives at a larger unit scope. The standard treatment indexes the
model by scopes and quantifies over extensions, a Kripke structure with
unit scopes as the worlds; instead the normalizer holds
the scope fixed and reads the program's unit variables through an
environment, so binders extend the environment and nothing is ever weakened.
The two lemmas this rests on are the pullback laws of the scaling algebra,
which read a scaling through a substitution: the pulled-back scaling scales
a unit exactly as the original scales its substituted image. One law is
immediate from the definitions; the other is again an interchange of the
order of summation.
What the composition laws caught. One defect in the development was found neither by examples, nor by the worked physics computations, nor by inspection. It was found during the metatheory the type-soundness proof rests on, when the composition law for substitutions failed to hold. Our substitution on types, under a nested quantifier, substituted for the bound variable instead of the outer one: de Bruijn's classic capture error, in the one function the scope indexing could not protect, because both variables inhabit the same scope. Every test passed; weakening had the matching defect, and the two canceled in the round-trip lemma we had proved! The failure of the composition law pointed at the exact clause. We record this as evidence for a practice: prove the full set of algebraic laws of a binding structure, because a subset can hold by cancellation of matching defects, as our round-trip lemma did. The repaired functions are derived from a single parallel substitution with proved composition and identity laws, and the incident is preserved in the artifact as a regression test.
Numbers, twice.
The semantic carrier is a type class with two instances. At ℝ it
is noncomputable, and it is the object of the theorems. At Float
it is compiled, calling BLAS through Lean's FFI where the platform supplies
it, on unboxed arrays; “Dimensioned Linear Algebra” (Map.lean)'s rank-one structure is why
the arrays can be unboxed. The class carries almost no laws, deliberately:
Float
satisfies neither associativity nor the field axioms, so any law strong
enough to be useful would exclude the instance that runs. The
carrier-generic theorems (type soundness, strong normalization, erasure)
therefore hold of the compiled evaluator and constrain its units, array extents,
and control flow; the theorems that pin down which number comes out
(adequacy, the theorem “Declared factors reach the compiled evaluator” (one_yard_in_meters, Erasure.lean), drift independence)
are stated at the ℝ instance. The binary's printed numbers are
checked by assertion instead. The yard report's 300 and 91.44 are
checked at build
time, in exact arithmetic where the declarations live and in Float
where the binary computes; the FFI-reaching reports are checked by the
binary itself when it runs.
The carrier boundary.
The abstraction and adequacy theorems use real arithmetic, not a proof
that floating-point evaluation equals it. Rounding affects defined operations.
For negative bases and non-integer exponents, Float.pow returns NaN,
whereas Real.rpow uses the real part of the principal complex power,
|x|^qcos(qπ). The carriers also choose different totalizations for division
by zero and logarithms. The covariance identity in Fundamental.lean
concerns the real operation; it is not a theorem about the compiled number.
The binary checks selected boundary cases, including these differing
power conventions, at startup.
We claim no IEEE
conformance, and a claim would say little: the
standard [IEEE 2019] requires correct rounding of the field
operations but only recommends it for pow, log, and exp,
so conformance pins down nothing about the accuracy of exactly the
operations at issue.
The trusted base, enumerated.
A reader who believes a theorem of the paper trusts the Lean kernel and
the three standard axioms. A reader who believes the number the compiled
binary prints trusts, in addition: Lean's code generator and runtime;
Float arithmetic, whose operations are opaque primitives with no
formal semantics; and three C stubs behind the FFI (a dot product, a
matrix-vector product, and a probe that reports which backend is linked).
The stubs are proved about their Lean fallback bodies and
assumed to agree with the C; floating-point reordering makes that
assumption approximate rather than exact. Everything upstream of the final
number (the checker, the consistency criterion, the semantics, both
abstraction theorems, the drift analysis) is kernel-checked and involves
none of this.