Skip to content

Commit 7a22f6f

Browse files
Cel.Format recognizer laws, and audit the last generated example
The conformance battery is finitely many labelled points; these are properties that hold for every input. The load-bearing one is ipv4Value?_lt — the value an accepted IPv4 address denotes really is below 2^32, which is exactly what isIpPrefix's host-bit masking (v % 2 ^ (32 - len)) assumes. The rest pin documented shape claims (four dotted parts, the 253-character hostname bound, the port range, and that only versions 0/4/6 are ever accepted) so they cannot drift out of the implementation unnoticed. //lean:format_laws_assurance audits their axiom closures. //examples/common:money_valid_assurance closes the last generated example without one: shipping consumes Money through valid_deps, so its theorems are now audited at their source too. The URI, email and IPv6 grammars carry no laws; the README says so.
1 parent 080efac commit 7a22f6f

4 files changed

Lines changed: 177 additions & 5 deletions

File tree

‎README.md‎

Lines changed: 20 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -96,15 +96,17 @@ which downstream code can project and rewrite along without re-running any
9696
checks. `Test/PredSchemeTest.lean` keeps a hand-written mirror of the emission
9797
scheme compiling.
9898

99-
Three `lean_assurance_test` targets audit axiom closures at compile time (no
99+
Five `lean_assurance_test` targets audit axiom closures at compile time (no
100100
`sorryAx`, standard axioms only), covering both the instances and the scheme
101-
they instantiate:
101+
they instantiate. Every generated example in the repo is audited:
102102

103103
| target | principal theorems |
104104
|---|---|
105105
| `//lean:runtime_assurance` | the generic `Decision` lemmas (`pred_of_toExcept_ok`, `toExcept_isOk`, `decodeThenValidate_sound`), the presence plumbing, `size_mapArray` — plus a scan of the whole `Protovalidate.Cel`/`Runtime` surface the generated propositions are stated over |
106+
| `//lean:format_laws_assurance` | the `Cel.Format` recognizer laws (see [format assurance](#format-assurance-what-the-battery-does-and-does-not-say)), headed by `ipv4Value?_lt` |
106107
| `//examples/shipping:shipping_valid_assurance` | `shipping.v1.Valid.{Order,Item}` (soundness, completeness, decode) and the validated `Order.payer_Type` sum |
107108
| `//examples/shipping:inventory_valid_assurance` | `catalog.v1.Valid.{Product,Payment}` and the `Payment.method_Type` sum (standard rules, enums, element rules, validated oneof) |
109+
| `//examples/common:money_valid_assurance` | `common.v1.Valid.Money` — the cross-target embed, audited at its source |
108110

109111
A repeated-message field's own rules are stated over the *validated* array
110112
(`Protovalidate.mapArray b.f Elem.ofPred h.f_elems`), because those rules may
@@ -513,14 +515,26 @@ checking in URI hosts. All are fixed; the corpus is what keeps them fixed.
513515
`well_known_regex`) are rejected at generation time, so they have no
514516
corpus rows.
515517

518+
**Proved, not sampled.** `Cel.Format`'s recognizer laws
519+
(`lean/Protovalidate/Cel/FormatLaws.lean`, audited by
520+
`//lean:format_laws_assurance`) carry the complementary kind of evidence —
521+
properties holding for *every* input rather than for corpus points. The
522+
load-bearing one is `ipv4Value?_lt`: the value an accepted IPv4 address denotes
523+
really is below `2 ^ 32`, which is exactly what `isIpPrefix`'s host-bit masking
524+
(`v % 2 ^ (32 - len)`) assumes. The others pin documented shape claims
525+
(`ipv4Value?_length`, `isHostname_length`, `isPort_le`, `isIp_version` — only
526+
versions 0/4/6 are ever accepted). The URI, email and IPv6 grammars carry no
527+
such laws; they rest on the corpus alone.
528+
516529
## Layout
517530

518531
```
519532
celtolean/ Go library: CEL AST → Lean expression (golden tests)
520533
cmd/cel2lean/ CLI: single expression, -json, -batch corpus→Lean
521534
cmd/protoc-gen-lean-protovalidate/ protoc plugin entry point
522535
leanvalidate/ plugin codegen: descriptors + buf.validate rules → Lean
523-
lean/Protovalidate/ Lean runtime (modules Protovalidate.Cel, .Runtime)
536+
lean/Protovalidate/ Lean runtime (modules Protovalidate.Cel, .Runtime) plus
537+
Cel/FormatLaws.lean (recognizer theorems, not runtime)
524538
protovalidate/ Starlark API: lean_protovalidate_library (defs.bzl)
525539
Test/ corpus TSV + genrule compiling every translation with
526540
Decidable assertions (build_test) + stand-in msg types.
@@ -569,6 +583,7 @@ non-generated, option-only proto dependencies such as
569583
per-pattern differential batteries (see
570584
[regex assurance](#regex-assurance-what-the-guards-do-and-do-not-say)), nor
571585
between `Cel.Format`'s grammars and their RFCs beyond the conformance
572-
battery (see
586+
battery and the IPv4/hostname/port recognizer laws (see
573587
[format assurance](#format-assurance-what-the-battery-does-and-does-not-say)).
574-
Both corpora are vendored snapshots of upstream references.
588+
Both corpora are vendored snapshots of upstream references; the URI, email
589+
and IPv6 grammars carry no laws yet.

‎examples/common/BUILD.bazel‎

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
consumed by //examples/shipping via proto_deps/valid_deps."""
44

55
load("@protobuf//bazel:proto_library.bzl", "proto_library")
6+
load("@rules_lean//lean:defs.bzl", "lean_assurance_test")
67
load("@rules_lean_grpc//:defs.bzl", "lean_proto_library")
78
load("//protovalidate:defs.bzl", "lean_protovalidate_library")
89

@@ -24,3 +25,16 @@ lean_protovalidate_library(
2425
lean_proto = ":money_lean",
2526
proto = ":money_proto",
2627
)
28+
29+
# The last generated example without an axiom audit: the target //examples/
30+
# shipping consumes through valid_deps, so the cross-target embed's theorems
31+
# are audited at their source too.
32+
lean_assurance_test(
33+
name = "money_valid_assurance",
34+
principal_theorems = [
35+
"common.v1.Valid.Money.validate_sound",
36+
"common.v1.Valid.Money.validate_complete",
37+
"common.v1.Valid.Money.decodeValid_sound",
38+
],
39+
deps = [":money_valid"],
40+
)

‎lean/BUILD.bazel‎

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,16 @@ lean_library(
3434
strip_module_prefix = "lean",
3535
)
3636

37+
# Recognizer laws for Cel.Format: properties that hold for every input, as
38+
# opposed to the conformance battery's finitely many labelled points. Not part
39+
# of the runtime library — nothing generated depends on them.
40+
lean_library(
41+
name = "protovalidate_format_laws",
42+
srcs = ["Protovalidate/Cel/FormatLaws.lean"],
43+
strip_module_prefix = "lean",
44+
deps = [":protovalidate_cel"],
45+
)
46+
3747
# Compile-time audit of the proof scheme itself. Every generated
3848
# validate_sound / validate_complete / decodeValid_sound is a one-line instance
3949
# of a lemma below, so this is the root of trust the per-message audits stand
@@ -62,3 +72,20 @@ lean_assurance_test(
6272
":protovalidate_runtime",
6373
],
6474
)
75+
76+
# The Cel.Format recognizer laws must be theorems closing over no axiom beyond
77+
# the standard three. `ipv4Value?_lt` is the load-bearing one: isIpPrefix's
78+
# host-bit masking assumes exactly it.
79+
lean_assurance_test(
80+
name = "format_laws_assurance",
81+
principal_theorems = [
82+
"Cel.Format.ipv4Value?_lt",
83+
"Cel.Format.ipv4Octet?_le_255",
84+
"Cel.Format.ipv4Value?_length",
85+
"Cel.Format.isIpv4_splitOn_length",
86+
"Cel.Format.isHostname_length",
87+
"Cel.Format.isPort_le",
88+
"Cel.Format.isIp_version",
89+
],
90+
deps = [":protovalidate_format_laws"],
91+
)
Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
module
2+
3+
public import Protovalidate.Cel.Format
4+
import all Protovalidate.Cel.Format
5+
6+
/-!
7+
Kernel-checked recognizer laws for `Protovalidate.Cel.Format`.
8+
9+
The formats are trusted code: what ties them to protovalidate is the
10+
conformance battery (`//Test:format_corpus_test`), which is differential
11+
testing over a finite corpus. These theorems are the complementary kind of
12+
evidence — properties that hold for *every* input, proved once.
13+
14+
They cover the tractable formats. The headline is `ipv4Value?_lt`: the value an
15+
accepted IPv4 address denotes really is a 32-bit number, which is what
16+
`isIpPrefix`'s host-bit masking (`v % 2 ^ (32 - len)`) silently assumes. The
17+
rest pin down documented shape claims (four dotted parts, the 253-character
18+
hostname bound, the port range, the accepted IP versions) so they cannot drift
19+
out of the implementation unnoticed.
20+
21+
Not covered: the URI grammar, the email grammar, and IPv6 — each would need
22+
substantially more machinery, and none has a comparable arithmetic assumption
23+
riding on it.
24+
-/
25+
26+
@[expose] public section
27+
28+
namespace Cel.Format
29+
30+
/-! ## IPv4 -/
31+
32+
/-- An octet is at most 255 — the strictness `ipv4Octet?`'s final guard states. -/
33+
theorem ipv4Octet?_le_255 {p : String} {v : Nat} (h : ipv4Octet? p = some v) : v ≤ 255 := by
34+
simp only [ipv4Octet?] at h
35+
repeat' first | split at h | cases h
36+
omega
37+
38+
/-- Accumulating octets base-256 keeps the running value under `256 ^ n`
39+
(stated with the accumulator general so it inducts). -/
40+
theorem ipv4Fold_bound (l : List String) : ∀ (acc v : Nat),
41+
l.foldlM (fun acc p => (ipv4Octet? p).map (acc * 256 + ·)) acc = some v →
42+
v + 1 ≤ (acc + 1) * 256 ^ l.length := by
43+
induction l with
44+
| nil =>
45+
intro acc v h
46+
simp only [List.foldlM] at h
47+
simp only [pure] at h
48+
injection h with h
49+
subst h
50+
simp
51+
| cons p rest ih =>
52+
intro acc v h
53+
simp only [List.foldlM] at h
54+
cases ho : ipv4Octet? p with
55+
| none => rw [ho] at h; simp at h
56+
| some o =>
57+
rw [ho] at h
58+
simp only [Option.map_some] at h
59+
simp only [bind, Option.bind] at h
60+
have hb := ih _ _ h
61+
have h255 := ipv4Octet?_le_255 ho
62+
calc v + 1 ≤ (acc * 256 + o + 1) * 256 ^ rest.length := hb
63+
_ ≤ (acc + 1) * 256 * 256 ^ rest.length := Nat.mul_le_mul_right _ (by omega)
64+
_ = (acc + 1) * 256 ^ (rest.length + 1) := by rw [Nat.pow_succ]; ac_rfl
65+
66+
/-- An address that parses has exactly four dot-separated parts. -/
67+
theorem ipv4Value?_length {s : String} {v : Nat} (h : ipv4Value? s = some v) :
68+
(s.splitOn ".").length = 4 := by
69+
simp only [ipv4Value?] at h
70+
split at h
71+
· exact absurd h (by simp)
72+
· rename_i hlen
73+
simp only [bne_iff_ne, ne_eq, Decidable.not_not] at hlen
74+
exact hlen
75+
76+
/-- **The value an accepted IPv4 address denotes is a 32-bit number.** This is
77+
what `isIpPrefix` assumes when it masks host bits with `v % 2 ^ (32 - len)`. -/
78+
theorem ipv4Value?_lt {s : String} {v : Nat} (h : ipv4Value? s = some v) : v < 2 ^ 32 := by
79+
have hlen := ipv4Value?_length h
80+
simp only [ipv4Value?] at h
81+
split at h
82+
· exact absurd h (by simp)
83+
· have hb := ipv4Fold_bound _ 0 v h
84+
rw [hlen] at hb
85+
have h256 : (0 + 1) * (256 : Nat) ^ 4 = 2 ^ 32 := by decide
86+
omega
87+
88+
theorem isIpv4_splitOn_length {s : String} (h : isIpv4 s = true) :
89+
(s.splitOn ".").length = 4 := by
90+
simp only [isIpv4, Option.isSome_iff_exists] at h
91+
obtain ⟨v, hv⟩ := h
92+
exact ipv4Value?_length hv
93+
94+
/-! ## Hostnames, ports, versions -/
95+
96+
/-- The documented 253-character bound really is enforced (trailing dot
97+
included, as protovalidate counts it). -/
98+
theorem isHostname_length {s : String} (h : isHostname s = true) : s.length ≤ 253 := by
99+
simp only [isHostname, Bool.and_eq_true, decide_eq_true_eq] at h
100+
exact h.1
101+
102+
/-- An accepted port denotes a number in the 16-bit range. -/
103+
theorem isPort_le {s : String} (h : isPort s = true) :
104+
s.toList.foldl (fun acc c => acc * 10 + (c.toNat - 48)) 0 ≤ 65535 := by
105+
simp only [isPort, Bool.and_eq_true, decide_eq_true_eq] at h
106+
exact h.2
107+
108+
/-- Only versions 0, 4 and 6 are ever accepted: `isIp(s, 5)` is false for every
109+
`s`, matching protovalidate (whose conformance suite tests exactly that). -/
110+
theorem isIp_version {s : String} {version : Int} (h : isIp s version = true) :
111+
version = 0 ∨ version = 4 ∨ version = 6 := by
112+
simp only [isIp] at h
113+
repeat' first | split at h | cases h
114+
all_goals simp_all
115+
116+
end Cel.Format

0 commit comments

Comments
 (0)