Skip to content

Commit 78d1c5e

Browse files
committed
entailment
1 parent 60bdcde commit 78d1c5e

1 file changed

Lines changed: 57 additions & 0 deletions

File tree

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
# /// script
2+
# requires-python = ">=3.10"
3+
# dependencies = ["z3-solver"]
4+
# ///
5+
"""Check that a field's constructor validation (CV) entails its deserialization
6+
validation (DV), i.e. that ∀x. CV(x) ⟹ DV(x).
7+
8+
If the implication ever fails, Z3 hands us a counterexample: a value that the
9+
constructor would happily accept but that an older deserializer would reject.
10+
That value is exactly the message an engineer could build and send to break a
11+
peer that is still running the previous schema version.
12+
13+
Run with: uv run entailment.py
14+
"""
15+
16+
from z3 import Int, Implies, ForAll, Not, Solver, sat
17+
18+
19+
def check_entailment(name: str, cv, dv) -> None:
20+
x = Int("page_number")
21+
22+
# We want to prove: ∀x. CV(x) ⟹ DV(x)
23+
# Equivalently, we ask Z3 for a counterexample to that claim:
24+
# ∃x. CV(x) ∧ ¬DV(x)
25+
solver = Solver()
26+
solver.add(cv(x))
27+
solver.add(Not(dv(x)))
28+
29+
print(f"== {name} ==")
30+
if solver.check() == sat:
31+
model = solver.model()
32+
witness = model[x]
33+
print(f" CV does NOT entail DV (unsafe).")
34+
print(f" Counterexample: page_number = {witness}")
35+
print(f" The constructor accepts {witness}, but the deserializer rejects it.\n")
36+
else:
37+
print(" CV entails DV (safe): no value passes the constructor but fails the deserializer.\n")
38+
39+
40+
if __name__ == "__main__":
41+
# Safe narrowing: constructor tightened to <= 10 while the deserializer
42+
# still admits the older <= 20. Every value the constructor accepts is
43+
# still deserializable, so the implication holds.
44+
check_entailment(
45+
"constructor `this <= 10`, deserializer `this <= 20`",
46+
cv=lambda x: x <= 10,
47+
dv=lambda x: x <= 20,
48+
)
49+
50+
# Unsafe widening: constructor relaxed to <= 30 before the deserializer was
51+
# taught to accept anything above 20. Z3 finds a value (e.g. 21..30) that a
52+
# producer can construct but an old consumer cannot deserialize.
53+
check_entailment(
54+
"constructor `this <= 30`, deserializer `this <= 20`",
55+
cv=lambda x: x <= 30,
56+
dv=lambda x: x <= 20,
57+
)

0 commit comments

Comments
 (0)