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