Conditional claims and lemmas¶
A claim is unconditional by default: for x in [0, 1], f(x) >= 0
quantifies over its whole declared region. An assuming clause makes
the claim conditional, and mathema gives the premise real semantics on
both routes: the region narrows, the evidence chain records what the
conclusion rests on, and a premise that cannot be established caps or
blocks the conclusion rather than being quietly ignored.
The clause leads the claim, before any quantifier:
assuming x > 0, for x in [-10, 10], f(x) > 0
Relation premises¶
The simplest premise is a relation over the claim's own variables, or
several joined with and:
assuming b^2 - 4*a*c >= 0.01, f(a, b, c) <= f(a, -b, c)
assuming k != 0 and j > 0, for x in [1, 5], f(x, k, j) * k * j == x
A relation premise narrows the region actually adjudicated. On the derive route the constraint reaches the prover as an assumption; on the probe route sampling stays inside the assumed region. The rendered record states the narrowed region explicitly, so a conditional claim and its unconditional twin can never read alike, and never share an identity.
A premise relating two parameters (a <= b) is handled by the
assumed-gap machinery rather than by interval narrowing, because that
region is not a box.
is_defined as a claim: restriction or totality¶
Besides the premise below, is_defined is a claim in its own right,
and it carries two readings, told apart by whether the claim states a
region:
- name: is_defined # RESTRICTION: f returns on exactly this region
statement: "x >= 0"
is_defined(f) # TOTALITY: f returns everywhere, it never raises
f is defined # the same claim, postfix spelling
One predicate, because the English reads correctly either way: "f is
defined on x >= 0" states a restriction, "f is defined" states that
there is none. A restriction claim proves when the stated region
matches the region mathema computes from the body's raise guards, and
falsifies when it differs (naming the fresh region) or when the
function is in fact total. The bare form proves when the body has no
raise region at all, and falsifies when it has one, naming the region
f actually returns on. mathema suggests the restriction form for a
partial function and never for a total one.
When there is no body to compute a region from (a builtin, a numpy
ufunc), or the comparison does not decide (a library function whose
region reads det(a) or dim(a)), the claim is adjudicated by
execution: every sampled point inside the region must return a value
and every point outside it must not, where a raise, a nan and an
infinity for a finite input are all no value. Where the computation
leaves float range is not a definedness fact but a computation one,
stated by an is_overflow_safe row (see
claims transfer), and a
library function's bare is_defined is adjudicated inside that
region. A region over det(a)
draws square matrices, singular ones on its boundary; one over
dim(a) draws sequences, the empty one below it. An indexed row
(is_defined[2], one conjunct of a region with several) only asks
for no value outside it, since inside it the other conjuncts decide.
The verdict is holds at best, and the note says how many points were
executed on each side.
The definedness premise¶
assuming is_defined(f), abs(f(x, y)) <= 1
assuming f is defined, abs(f(x, y)) <= 1
Both spellings mean the same thing: the claim quantifies over exactly
the region where every call to f returns. mathema computes that
region from the function's own raise guards and from registered
partiality lemmas (below), then states it in the record behind an
arrow:
assuming f is defined --> b != 0, b*f(a, b) = a
The region after --> is the resolved fact the claim was adjudicated
under. On a later run the pin is validated, never trusted: while the
code still agrees, the pin is kept byte-for-byte, so the record is
stable; when the code has moved out from under it, the region is
recomputed and the note says so, naming both. A function with no raise
region at all keeps the premise arrow-free (assuming f is defined),
because there is nothing to state.
Dimension premises¶
A premise may relate the shapes of a function's arguments, not just
their values. dim(x, axis) is the canonical spelling for a
dimension; len(x), rows(A) and cols(A) are sugar for
dim(x, 0), dim(A, 0) and dim(A, 1).
assuming len(x) == len(y), f(x, y) == f(y, x)
assuming dim(x, 0) >= 3, f(x) >= x[2]
On the probe route a dimension premise is drawn to hold by construction: an equality between two dimensions shares one drawn size across them, and a bound against a constant constrains the draw, so a claim about a function that legitimately requires equal lengths no longer falsifies on synthesised mismatches. The four spellings share one identity, so a record is stable however the premise was written.
Matrices, and named dimensions¶
The mechanism is axis-general. A parameter carrying a Shape marker
(types.Shape("m", "n")) has as many axes as the marker names, and
dim(A, 1) reads the second one:
def matvec(a: Annotated[list, Shape("m", "n")],
x: Annotated[list, Shape("n")]) -> Annotated[list, Shape("m")]:
...
assuming n >= 2, dim(f(a, x), 0) == dim(a, 0)
Two things follow from the marker. First, a marked matrix parameter
is synthesised as a nested list of the right shape, so ordinary claims
run against matrix functions (an unmarked nested-list parameter stays
a plain one-dimensional sequence, mathema never invents a second axis
it was not told about). Second, a marker dimension name is a
first-class symbol in a premise or law: a premise may bound it
(assuming n >= 2, ...), a law may relate it (dim(a, 1) == n), and
a name shared between two parameters (Shape("m", "n") and
Shape("n") both naming n) is drawn once, so the arguments are
conformable every trial. len, rows and cols remain sugar for
dim on axes 0, 0 and 1.
A parameter's domain can also state the SPACE directly, rather than
its element domain: for v in R^n is a real vector of length n,
for xs in [0, 1]^n a vector with elements in [0, 1], and
for A in R^(m,n) an m-by-n matrix (rows, then columns, the
order of a numpy shape). The element domain is the base and the
dimensions the exponent; in unicode the exponent renders as a
superscript (ℝⁿ, ℝᵐˣⁿ, ℝ²ˣ³), the missing-value clause trailing
the whole space. A dimension whose name the superscripts cannot carry
(one containing an x, which would read as the ˣ separator, or a
letter with no superscript form such as q) renders as the plain
exponent instead (ℝ^(x,n)). The matrix space is also accepted as
R^{m,n}, R^(m×n), R^{m×n}, R^(m*n), ℝᵐˣⁿ and, for a fixed
size, ℝ³ˣ³ or ℝ^{3×3}; every one of these is the same claim and
is written back as R^(m,n). A dimension NAME is the constraint: R^n on two
parameters draws them to one length every trial, so a claim about a
function that requires equal-length inputs holds without a separate
premise. len/rows/cols remain the axis-0/0/1 sugar for reading a
dimension back.
To enforce a dimension premise at RUNTIME, the shape analogue of
@enforce_domain is @enforce_dimensions:
from mathema import claims_decorator, enforce_dimensions
@enforce_dimensions()
@claims_decorator("assuming len(x) == len(y), f(x, y) == f(y, x)")
def dot(x, y):
return sum(a * b for a, b in zip(x, y))
dot([1, 2], [3]) # ValueError: dot: dim(x, 0)=2 violates the declared premise dim(x, 0) == dim(y, 0)=1
@enforce_domain guards a parameter's VALUE domain.
@enforce_dimensions guards SHAPES: the rank and the fixed sizes a
marker or a binding states, a dimension name shared across parameters,
the relations a premise states between them, and the result against
the return marker (see shape
markers). Neither
makes a claim true by fiat: each makes the function reject inputs the
claim was never about, so a premise and its runtime guard are one
precondition stated once.
Beyond shape, a matrix parameter can declare its STRUCTURE (symmetric, positive definite, and so on) and state linear-algebra identities over it. See Matrix structure and linear algebra.
Prerequisite claims¶
A claim may rest on a sibling claim from the same batch or file:
assuming positive holds, for x in [0, 10], f(2*x) >= f(x)
assuming real_roots is proven, f(a, b, c) <= f(a, -b, c)
The referenced claim must reach the named verdict first. mathema
orders the batch by dependency (a forward reference works), reports a
cycle as a skip naming both claims, and enforces the evidence ladder:
a prerequisite that only holds empirically caps the resting claim at
holds too, with meta["mathema.capped_by"] naming the premise. A
proof cannot stand on sampling.
A bare sibling name (assuming real_roots, ...) borrows that claim's
relation as a region constraint instead of consulting its verdict.
Partiality lemmas¶
The definedness machinery reads raise guards from the function's own body, and, for functions it calls, from registered lemmas:
import sympy
import mathema.partiality
def stable_kernel(u, tol):
... # raises for u <= tol
mathema.partiality.register_raises_when(
stable_kernel, lambda u, tol: sympy.Le(u, tol), "ValueError")
A claim about any function that calls stable_kernel then treats
u <= tol exactly like an explicit raise guard: a value claim
quantifying over that region is falsified with a witness, and a
definedness premise excludes it. The math module's own partiality
(sqrt, log, asin, ...) ships registered out of the box.
The compendium¶
The registration generalises to claims files about libraries: a file
that declares compendium: numpy (and a versions: range) holds
claims whose keys are the library's functions, and mathema bundles such
files for math and numpy. A project states its own in any claims
file (claims/numpy.claims.yaml), shadowing the bundled entry per
function. An is_defined row there states the region where the
function returns a value, so a caller's claim is read against it the
way it is read against an explicit raise guard, and a compendium claim
may be named as a premise, by bare name or by its qualified spelling
(assuming numpy.clip.clip_lower holds). The bundled files apply to
every check, the Python API included, with nothing to install; a
project's own compendium files apply wherever mathema reads the
project. Outside an is_defined region the call has no value: a
raise, a nan, and an infinity returned for a finite input all count
the same, and each falsifies a value claim that reaches it.
A compendium verdict never enters the evidence chain silently: mathema
verify adjudicates the library rows your project calls or rests on by
executing them against the installed library, and the premise resolves
at the verdict recorded there. Those rows gate the run like the
project's own claims. A row verify cannot settle fails it and leaves
the resting claim unknown, both naming mathema accept <key> <claim>
--as trusted, which takes the row at the level it claims (the
conclusion caps there, provenance named). A file whose versions
range does not match the installed library contributes nothing. The
wider story, including transfer between implementations, is
Claims transfer.
What the record carries¶
The premise is part of the claim's canonical text, so it survives every store round trip and participates in the claim's identity: a conditional claim is never superseded by its unconditional twin. The resolved definedness region is recorded for the reader but stripped from the identity, because it is recomputed evidence, not something the author asserted.