Formal Methods — Hoare Logic, Model Checking, TLA+
Formal Methods: Hoare Logic, Model Checking, and TLA+
Testing দেখায় bug আছে; formal methods দেখায় bug নেই — কিন্তু শুধু যা আপনি স্পেসিফাই করেছেন তার জন্য। Hoare logic দিয়ে sequential program-এর deductive proof, model checking দিয়ে exhaustive state-space exploration, আর TLA+ দিয়ে distributed protocol স্পেসিফিকেশন — তিনটা ভিন্ন হাতিয়ার, একই লক্ষ্য।
আগে এটা বুঝি
একটা payment system-এর গল্প। দুইটা request প্রায় একই মুহূর্তে আসে — একজন ব্যবহারকারী দ্রুত দুইবার “Pay” বোতাম চাপেন, নেটওয়ার্ক সামান্য দেরি করে, আর দুইটা request-ই একই ব্যালেন্স পড়ে ফেলে:
Thread A: balance পড়ে (= 100)
Thread B: balance পড়ে (= 100)
Thread A: balance - 50 লিখে (= 50)
Thread B: balance - 50 লিখে (= 50) ← হওয়া উচিত ছিল 0দুইটা টাকা কাটা হলো, কিন্তু balance একবারই কমলো। কোম্পানি ৫০ টাকা হারালো। QA team মাসের পর মাস test চালিয়েছে — কখনো ধরা পড়েনি, কারণ এই bug ধরা পড়তে দুইটা thread-কে ঠিক এই ক্রমে interleave হতে হবে, আর সেটা কোটিতে একবার ঘটে। Load test-এও প্রায়ই মিস হয়ে যায়।
এখানেই টেস্টিং-এর একটা মৌলিক সীমাবদ্ধতা দেখা যায়:
Formal methods এই ফাঁকটা ভরাট করার চেষ্টা — গাণিতিকভাবে প্রমাণ করা যে একটা program বা protocol একটা নির্দিষ্ট property সব পরিস্থিতিতেই মেনে চলে, শুধু যেগুলো test করা হয়েছে সেগুলোতে নয়।
এই লেসনে আমরা তিনটা সম্পূরক হাতিয়ার দেখব:
- Hoare logic — একটা sequential program-এর correctness হাতে-কলমে deductive proof দিয়ে প্রমাণ করার নিয়ম।
- Model checking — একটা finite state space সম্পূর্ণভাবে exhaustively explore করে একটা temporal property যাচাই করার automated পদ্ধতি।
- TLA+ — concurrent ও distributed system-কে state machine + temporal logic দিয়ে স্পেসিফাই করার ভাষা, যা দিয়ে Amazon সত্যিই production-এর আগে DynamoDB আর S3-তে সূক্ষ্ম bug খুঁজে পেয়েছিল।
তিনটাই একই প্রশ্নের ভিন্ন উত্তর: “আমি কীভাবে নিশ্চিত হব যে এই সিস্টেম যা করার কথা ঠিক তাই করবে — সব ক্ষেত্রে?”
মূল ধারণা
Hoare Logic — program সম্পর্কে দাবি করার ভাষা
programming-languages/formal-semantics-এ আমরা Hoare
triple-এর প্রাথমিক পরিচয় পেয়েছিলাম axiomatic semantics-এর
আলোচনায়। এখানে আমরা সেই নিয়মগুলো সম্পূর্ণভাবে, প্রতিটা inference
rule সহ দেখব — যাতে একটা প্রকৃত program-এর সম্পূর্ণ correctness
প্রমাণ করা যায়।
নোটেশন
P— precondition: programCচালানোর আগে যা সত্য বলে ধরা হয়C— একটা program বা program-এর টুকরা (statement)Q— postcondition:Cসফলভাবে শেষ হওয়ার পরে যা সত্য হবে
অর্থ (partial correctness): “যদি P সত্য থাকা অবস্থায় C চালানো
হয়, আর C যদি থেমে যায় (terminate করে), তাহলে থামার মুহূর্তে Q
সত্য থাকবে।”
লক্ষ্য করুন শব্দটা — “যদি থামে”। এই triple C-র termination
সম্পর্কে কিছু বলে না, শুধু বলে “থামলে ফলাফল সঠিক হবে”। এটাকেই বলে
partial correctness। (Termination আলাদাভাবে প্রমাণ করতে হয় —
নিচে দেখব।)
চারটা মূল inference rule
১. Assignment axiom — সবচেয়ে মৌলিক, আর সবচেয়ে পাল্টা-স্বজ্ঞাত (counter-intuitive), কারণ এটা পেছনের দিকে কাজ করে:
এখানে Q[E/x] মানে “Q-তে প্রতিটা free occurrence-এর x-কে E দিয়ে
প্রতিস্থাপন করা”। উদাহরণ:
আমরা postcondition x > 5 দিয়ে শুরু করি, তারপর x-এর জায়গায়
x + 1 বসিয়ে precondition বের করি: (x+1) > 5, যা সরল করে x > 4।
এটাই সঠিক precondition — x-এর মান 4-এর বেশি হলে, x := x + 1-এর
পরে x-এর মান 5-এর বেশি হবে।
২. Sequence rule — দুইটা statement একের পর এক:
C1-এর postcondition আর C2-এর precondition একই R হতে হবে —
এটাই দুইটা statement-কে “সেলাই” করে।
৩. If-then-else rule:
দুইটা branch-ই একই Q-তে পৌঁছাতে হবে, কিন্তু প্রতিটা branch-এর
নিজস্ব অতিরিক্ত অনুমান আছে — then branch জানে B সত্য, else
branch জানে B মিথ্যা।
৪. While rule — সবচেয়ে গুরুত্বপূর্ণ, কারণ এখানেই loop invariant আসে:
I হলো loop invariant — এমন একটা predicate যা প্রতিটা iteration-এর
আগে ও পরে সত্য থাকে (loop-এর ভেতরে সাময়িকভাবে মিথ্যা হতে পারে,
কিন্তু প্রতিটা সম্পূর্ণ iteration শেষে আবার সত্য)। Loop শেষ হলে (B
মিথ্যা হয়ে গেলে), আমরা জানি I তখনও সত্য, প্লাস ¬B — এই দুইটা
মিলিয়েই postcondition।
৫. Rule of consequence — precondition শক্তিশালী করা বা postcondition দুর্বল করা সবসময় নিরাপদ:
এই rule-টা একটা “adapter” — বাকি চারটা rule দিয়ে পাওয়া triple-কে
আপনার আসল দরকারি P আর Q-এর সাথে মেলাতে ব্যবহার হয়।
| Construct | Rule | মূল ধারণা |
|---|---|---|
| Assignment | {Q[E/x]} x:=E {Q} | পেছন থেকে সামনে — postcondition-এ x কে E বসানো |
| Sequence | {P}C1{R}, {R}C2{Q} ⟹ {P}C1;C2{Q} | মাঝের শর্ত R দিয়ে জোড়া লাগানো |
| If | দুই branch-ই Q-তে পৌঁছায় | B জানা যায় branch-এর ভেতরে |
| While | {I∧B}C{I} ⟹ {I}while B do C{I∧¬B} | Invariant maintenance = inductive step |
| Consequence | P⟹P', {P'}C{Q'}, Q'⟹Q ⟹ {P}C{Q} | Precondition আঁটসাঁট, postcondition ঢিলা করা |
ভেতরে কী ঘটছে
একটা সম্পূর্ণ correctness প্রমাণ — sum-computing loop
তত্ত্ব যথেষ্ট। এবার একটা বাস্তব program পুরোপুরি প্রমাণ করি।
// precondition: n ≥ 0
i := 1;
s := 0;
while (i ≤ n) do
s := s + i;
i := i + 1
// postcondition: s = n(n+1)/2এই program-টা 1 + 2 + ⋯ + n গণনা করে — mathematics/induction-এর
প্রথম উদাহরণের ঠিক সেই সমষ্টি, শুধু এবার আমরা এটাকে একটা program
হিসেবে প্রমাণ করব, mathematical induction দিয়ে নয়, Hoare rules
দিয়ে।
ধাপ ১ — invariant বেছে নেওয়া
Invariant বাছার সবচেয়ে ভালো উপায়: postcondition-কে “সাধারণীকরণ”
করা। Postcondition বলে s = n(n+1)/2 — i = n+1-এর সময়কার একটা
বিশেষ ক্ষেত্র। সাধারণ invariant:
মানে: “যতগুলো সংখ্যা ইতিমধ্যে যোগ হয়েছে (1 থেকে i-1 পর্যন্ত),
s-এ ঠিক তাদের যোগফল আছে।”
ধাপ ২ — Initialization
দেখাতে হবে: {n ≥ 0} i:=1; s:=0 {I}
i:=1; s:=0-এর পরে I-তে i=1, s=0 বসিয়ে যাচাই:
n ≥ 0 precondition ঠিক এই শর্তটাই দেয়। Assignment axiom দুইবার
প্রয়োগ করে (পেছন থেকে) এই একই সিদ্ধান্তে পৌঁছানো যায় — নিজে চেষ্টা
করুন Question 1-এ।
ধাপ ৩ — Maintenance (এটাই মূল ধাপ)
দেখাতে হবে: {I ∧ i ≤ n} s:=s+i; i:=i+1 {I}
Assignment axiom পেছন থেকে প্রয়োগ করি। শেষ statement
(i := i+1) থেকে শুরু — I-তে i-র জায়গায় i+1 বসাই:
এখন প্রথম statement (s := s+i) — এই নতুন predicate-এ s-এর
জায়গায় s+i বসাই:
তাহলে দেখাতে হবে:
বাঁ পাশ খুলে লিখি: I মানে s = (i-1)i/2 ∧ 1 ≤ i ≤ n+1, প্লাস
i ≤ n। তাহলে s = (i-1)i/2 = i(i-1)/2 — মিলে যায়। আর 1 ≤ i ≤ n+1 প্লাস i ≤ n থেকে 1 ≤ i ≤ n, যা 0 ≤ i ≤ n-কে entail করে।
Implication সত্য — rule of consequence প্রযোজ্য। ∎ (maintenance)
ধাপ ৪ — While rule প্রয়োগ
Maintenance প্রমাণিত হওয়ায়, while rule সরাসরি দেয়:
অর্থাৎ {I ∧ i > n}। এই শর্তে I-এর 1 ≤ i ≤ n+1 আর i > n
একসাথে মানে i = n+1। সেটা I-এর প্রথম অংশে বসিয়ে:
ঠিক আমাদের postcondition! rule of consequence-এর দরকারই পড়ল না — সরাসরি মিলে গেল। ∎
| ধাপ | কী প্রমাণ করা হলো | কোন rule |
|---|---|---|
| Initialization | {n≥0} i:=1;s:=0 {I} | Assignment ×2 + Sequence |
| Maintenance | {I∧i≤n} s:=s+i;i:=i+1 {I} | Assignment ×2 + Sequence + Consequence |
| Loop | {I} while(i≤n) do ⋯ {I∧i>n} | While rule |
| উপসংহার | {I∧i>n} ⟹ {s=n(n+1)/2} | সরাসরি বীজগণিত |
Partial বনাম total correctness — termination আলাদা
উপরের পুরো প্রমাণ শুধু partial correctness দেখায়: “যদি loop-টা থামে, তাহলে উত্তর সঠিক।” কিন্তু loop-টা যে আদৌ থামবে, সেটা আলাদা প্রমাণ।
Total correctness = partial correctness + termination।
Termination প্রমাণ করতে দরকার একটা variant function (বা “loop measure”) — এমন একটা রাশি যা:
- প্রতিটা iteration-এ কঠোরভাবে কমে
- নিচে সীমাবদ্ধ (একটা নির্দিষ্ট মানের নিচে যেতে পারে না)
এখানে variant n - i। প্রতিবার loop body চললে i এক বাড়ে, তাই
n - i এক কমে। আর যতক্ষণ loop চলছে (i ≤ n), n - i ≥ 0 — নিচে
সীমাবদ্ধ। mathematics/induction-এর well-ordering principle
অনুযায়ী, অ-ঋণাত্মক পূর্ণসংখ্যার কঠোরভাবে হ্রাসমান কোনো অসীম ক্রম
থাকতে পারে না — তাই loop-টা সসীম সংখ্যক ধাপে থামবেই।
উদাহরণ
Model checking — একদম ভিন্ন কৌশল
Hoare logic deductive — আপনি হাতে (বা tool-এর সাহায্যে) একটা
proof tree বানান, প্রতিটা ধাপ একটা inference rule দিয়ে justify করা।
এটা সসীম আর অসীম, উভয় ধরনের state space-এই কাজ করে (কারণ n একটা
variable, নির্দিষ্ট সংখ্যা নয়) — কিন্তু invariant মানুষকে বের করতে
হয়, যেটা সৃজনশীল আর কঠিন কাজ।
Model checking সম্পূর্ণ ভিন্ন দর্শন: system-টার state space যদি সসীম হয়, পুরো state space automatically exhaustively explore করে ফেলা — প্রতিটা reachable state, প্রতিটা সম্ভাব্য interleaving — আর যাচাই করা একটা temporal property সবসময় সত্য কি না।
উদাহরণ — আমাদের intro-র race condition, model checking-এর চোখে
মনে করুন দুইটা thread একই কাউন্টার বাড়ায়, প্রতিটা counter += 1
তিনটা atomic sub-step-এ ভাঙে: load, inc, store।
Thread A: a := counter; a := a + 1; counter := a
Thread B: b := counter; b := b + 1; counter := bState-টা (pc_A, pc_B, counter, a, b) — pc মানে প্রতিটা thread
কোন sub-step-এ আছে। দুইটা thread-এর ৩টা করে sub-step, তাই সম্ভাব্য
interleaving সংখ্যা \binom{6}{3} = 20।
Model checker সবগুলো ২০টা interleaving চালিয়ে দেখে — safety
property counter = 2 (দুইবার increment-এর পরে) শেষে সবসময় সত্য
কি না। ফলাফল: বেশিরভাগ interleaving-এ ঠিক, কিন্তু একটাতে ভুল —
ঠিক intro-র bug-টা:
A: load counter (0)
B: load counter (0) ← এখানেই সমস্যার বীজ
A: inc a (1)
A: store counter (1)
B: inc b (1)
B: store counter (1) ← হওয়া উচিত ছিল 2Model checker শুধু “bug আছে” বলে না — এটা এই কাউন্টার-উদাহরণ trace-টাও দেয়, ঠিক যে interleaving-এ property ভাঙে। এটাই model checking-এর সবচেয়ে বড় practical সুবিধা: একটা concrete, reproducible bug trace, যেটা Hoare logic-এর “এই invariant বাঁচে না” প্রমাণ থেকে সরাসরি পাওয়া যায় না।
- কী explore করেHoare: symbolic proof tree (হাতে বা tool-সাহায্যে) — variable-এর মান নির্দিষ্ট নয়
- Model checking: concrete reachable state-এর সসীম সেট — প্রতিটা explicit
- কে বুদ্ধি খরচ করেHoare: মানুষ — invariant বের করা সৃজনশীল কাজ
- Model checking: মেশিন — একবার model লিখে দিলে exploration automatic
- ScaleHoare: symbolic, তাই n = 10 বা n = 10¹⁰⁰ একই প্রমাণ
- Model checking: state space সসীম হতেই হবে, আর বড় হলে exponential ব্যয়
- আউটপুটHoare: "প্রমাণিত" বা "প্রমাণ পাইনি" (invariant খুঁজে না পেলে অনিশ্চিত)
- Model checking: "সত্য" অথবা একটা concrete counterexample trace
State space explosion
সমস্যা: reachable state-এর সংখ্যা প্রতিটা নতুন process/variable/thread-এর
সাথে exponentially বাড়ে। উপরের ২-thread উদাহরণে 20টা
interleaving; 3 thread হলে কয়েক হাজার; 10 thread হলে astronomically
বড়। বাস্তব distributed system-এ (যেমন একটা consensus protocol-এর
৫টা node, প্রতিটার নিজস্ব state, নেটওয়ার্ক message loss/duplication/
reorder-সহ) reachable state-এর সংখ্যা কোটি কোটি ছাড়িয়ে যায় —
একে বলে state space explosion, model checking-এর মূল সীমাবদ্ধতা।
সমাধানের কৌশল: symbolic model checking (state-কে explicit না
রেখে boolean formula/BDD দিয়ে represent করা), bounded model
checking (শুধু k ধাপ পর্যন্ত explore করা — bug থাকলে অনেক সময়
অল্প ধাপেই পাওয়া যায়), আর abstraction (কম গুরুত্বপূর্ণ detail
বাদ দিয়ে state space ছোট করা)।
নিজে চালিয়ে দেখুন
নিজের হাতে exhaustive interleaving-explore করে race condition ধরা
from itertools import permutations
# প্রতিটা thread-এর sub-step: ('load', thread), ('inc', thread), ('store', thread)
def make_steps(thread_id):
return [('load', thread_id), ('inc', thread_id), ('store', thread_id)]
steps_a = make_steps('A')
steps_b = make_steps('B')
all_steps = steps_a + steps_b
# একটা interleaving-এ A আর B-এর নিজেদের ভেতরের order ঠিক থাকতে হবে,
# শুধু A আর B-এর মধ্যে interleave করা যায়। তাই সব 6-permutation না,
# শুধু সেইগুলো যেগুলোতে relative order রক্ষা পায়।
def valid_interleavings(a, b):
if not a:
yield list(b)
return
if not b:
yield list(a)
return
for rest in valid_interleavings(a[1:], b):
yield [a[0]] + rest
for rest in valid_interleavings(a, b[1:]):
yield [b[0]] + rest
def simulate(interleaving):
counter = 0
reg = {'A': None, 'B': None}
for op, tid in interleaving:
if op == 'load':
reg[tid] = counter
elif op == 'inc':
reg[tid] = reg[tid] + 1
elif op == 'store':
counter = reg[tid]
return counter
total = 0
bugs = 0
for interleaving in valid_interleavings(steps_a, steps_b):
total += 1
result = simulate(interleaving)
if result != 2:
bugs += 1
if bugs == 1:
print("প্রথম বাগ-উৎপাদনকারী interleaving পাওয়া গেল:")
for op, tid in interleaving:
print(f" {tid}: {op}")
print(f" ফলাফল: counter = {result} (প্রত্যাশিত: 2)\n")
print(f"মোট interleaving পরীক্ষা করা হলো: {total}")
print(f"যেগুলোতে property ভেঙেছে (counter != 2): {bugs}")
print(f"safety property রক্ষা পেয়েছে: {total - bugs} / {total}")প্রত্যাশিত আউটপুট (সংক্ষিপ্ত):
প্রথম বাগ-উৎপাদনকারী interleaving পাওয়া গেল:
A: load
B: load
A: inc
A: store
B: inc
B: store
ফলাফল: counter = 1 (প্রত্যাশিত: 2)
মোট interleaving পরীক্ষা করা হলো: 20
যেগুলোতে property ভেঙেছে (counter != 2): 18
safety property রক্ষা পেয়েছে: 2 / 20লক্ষ্য করুন সংখ্যাটা — মাত্র 2/20 interleaving-এ property রক্ষা
পায় (যেগুলোতে একটা thread সম্পূর্ণ শেষ করে তারপর আরেকটা শুরু হয়)।
18/20 মানে ৯০% সম্ভাব্য schedule-এই bug ঘটে! তাও production-এ
এটা “rare” মনে হয়, কারণ বাস্তব thread scheduler প্রায়ই দুইটা
consecutive sub-step একসাথে চালায় (কম context switch), তাই bug-এর
আসল সম্ভাবনা কম মনে হয় — কিন্তু গাণিতিকভাবে state space-এ এটা
সর্বত্র লুকিয়ে আছে, আর load বেশি হলে (বেশি thread, বেশি contention)
সেই সম্ভাবনা বেড়ে যায়।
এই ছোট প্রোগ্রামটাই একটা bare-bones model checker — valid_interleavings
হলো state-space exploration, simulate হলো transition function, আর
result != 2 চেক হলো safety property যাচাই। SPIN, TLC-এর মতো আসল
tool ঠিক এই একই কাজ করে, শুধু অনেক বেশি efficient ভাবে (symbolic
representation, partial-order reduction — একই ফলাফল দেয় এমন
interleaving-গুলো একসাথে গোনা) আর অনেক বড় state space-এর জন্য।
একটা ছোট মডেল checker হাতে লিখলে দেখা যায় কীভাবে সব সম্ভাব্য interleaving enumerate করে একটা safety property ভাঙার exact trace বের করা যায় — model checking আসলে কী করে, তার একটা micro-scale সংস্করণ।
নিজে বানান
TLA+ style-এ একটা counter protocol স্পেসিফাই করা
- State variable ঘোষণা করুন — system-এর snapshot কী কী variable দিয়ে গঠিত
- Init predicate লিখুন — শুরুর state কেমন
- Next relation লিখুন — প্রতিটা সম্ভাব্য action, একটা disjunction হিসেবে
- Safety property লিখুন — একটা Invariant predicate যা সবসময় সত্য থাকা উচিত
- (ধারণাগতভাবে) TLC exhaustively সব reachable state explore করে invariant যাচাই করে
TLA+ (Temporal Logic of Actions) একটা system-কে state machine হিসেবে স্পেসিফাই করে — Init predicate (শুরুর state) + Next relation (কোন state থেকে কোন state-এ যাওয়া যায়) + সব সময় সত্য থাকা উচিত এমন Invariant।
নিচেরটা আসল TLA+ syntax নয় (সেটা শিখতে আলাদা সময় লাগে), কিন্তু এর কাঠামো — variable, Init, Next, Invariant — হুবহু TLA+-এর মতোই। আমাদের race-condition counter-টাকে এভাবে স্পেসিফাই করা যায়:
VARIABLES counter, pc_A, pc_B, reg_A, reg_B
Init ==
/\ counter = 0
/\ pc_A = "load" /\ pc_B = "load"
/\ reg_A = 0 /\ reg_B = 0
\* প্রতিটা thread-এর একটা action — "/\" মানে AND, "\/" মানে OR
StepA_Load == /\ pc_A = "load"
/\ reg_A' = counter
/\ pc_A' = "inc"
/\ UNCHANGED <<counter, pc_B, reg_B>>
StepA_Inc == /\ pc_A = "inc"
/\ reg_A' = reg_A + 1
/\ pc_A' = "store"
/\ UNCHANGED <<counter, pc_B, reg_B>>
StepA_Store == /\ pc_A = "store"
/\ counter' = reg_A
/\ pc_A' = "done"
/\ UNCHANGED <<pc_B, reg_A, reg_B>>
\* StepB_Load, StepB_Inc, StepB_Store — প্রতিসম, B-এর জন্য
Next == StepA_Load \/ StepA_Inc \/ StepA_Store
\/ StepB_Load \/ StepB_Inc \/ StepB_Store
Spec == Init /\ [][Next]_<<counter, pc_A, pc_B, reg_A, reg_B>>
\* Safety property — দুই thread-ই "done" হলে counter ঠিক 2 হওয়া উচিত
CounterCorrect ==
(pc_A = "done" /\ pc_B = "done") => (counter = 2)পড়ার নিয়ম:
/\আর\/— logical AND/OR, indentation দিয়ে grouping বোঝানো হয়x'(prime) — “পরের state-এx-এর মান” — একটা action কীভাবে state বদলায় তা বর্ণনা করেUNCHANGED <<...>>— যে variable-গুলো এই action-এ বদলায় না তা স্পষ্ট করে বলা (TLA+-এ বাধ্যতামূলক — নাহলে অনির্দিষ্ট থেকে যায়)[][Next]_vars— “সবসময় (□), হয়Nextঘটবে নয় সব variable অপরিবর্তিত থাকবে” — পুরো execution-এর একটা legal step describe করেSpec— Init দিয়ে শুরু, তারপর সবসময়Next-এর কোনো একটা disjunct অনুসরণ করা — এটাই পুরো protocol-এর সংজ্ঞা
TLC model checker এই স্পেসিফিকেশন নিয়ে সব reachable state
exhaustively explore করে, প্রতিটাতে CounterCorrect যাচাই করে। যদি
কোথাও ভাঙে (ঠিক আমাদের experiment-এ পাওয়া সেই ১৮টা bad interleaving-এর
মতো), TLC একটা concrete counterexample trace প্রিন্ট করে — ঠিক
কোন state থেকে কোন state-এ গিয়ে property ভাঙল, ধাপে ধাপে।
নিজে বাড়ান:
- একটা
Fixaction যোগ করুন যাload-কে atomic করে (compare-and-swap-এর মতো) — নতুন Next-এ সেই fix যোগ করলেCounterCorrectসব state-এ সত্য থাকবে, TLC-তে ভাবুন কীভাবে - তৃতীয় thread
Cযোগ করুন — Init, Next বাড়ান, ভাবুন state space কতগুণ বাড়ল - একটা liveness property লিখুন (ধারণাগতভাবে): “শেষ পর্যন্ত (◇)
pc_A = "done"” — safety property “কিছু খারাপ কখনো ঘটে না” থেকে ভিন্ন, liveness বলে “কিছু ভালো শেষ পর্যন্ত ঘটবেই” - আসল TLA+ শিখতে চাইলে TLA+ Toolbox বা VS Code extension দিয়ে এই স্পেসিফিকেশন প্রকৃত syntax-এ লিখে TLC চালিয়ে দেখুন
বাস্তব সিস্টেমে
AWS-এ TLA+ — কাল্পনিক নয়, documented বাস্তবতা
২০১৪ সালে Amazon-এর একটা দল (Chris Newcombe এবং সহকর্মীরা) Communications of the ACM-এ একটা paper প্রকাশ করে: “How Amazon Web Services Uses Formal Methods”। এটা শুধু একাডেমিক আগ্রহের বিষয় নয় — production system-এ formal methods ব্যবহারের সবচেয়ে ভালো-documented বাস্তব কেস স্টাডিগুলোর একটা।
মূল দাবি: DynamoDB, S3, EBS আর অন্যান্য কয়েকটা core AWS service-এ TLA+ দিয়ে এমন সব bug পাওয়া গেছে যেগুলো traditional testing খুঁজে পেত না — কারণ সেগুলো এমন rare interleaving বা failure-mode-এ ঘটত যা reproduce করা প্রায় অসম্ভব ছিল।
paper-এ বর্ণিত কিছু নির্দিষ্ট উদাহরণ:
- একটা replication protocol-এর bug যা শুধু তখনই প্রকাশ পেত যখন একটা নির্দিষ্ট node ঠিক একটা নির্দিষ্ট মুহূর্তে fail করত — এমন একটা timing window যা বছরের পর বছর production traffic-এও কখনো ঘটেনি, কিন্তু TLA+ model checking কয়েক মিনিটেই সেই exact trace খুঁজে বের করে।
- একাধিক subtle bug যা শুধু তখনই ঘটত যখন একাধিক rare event একসাথে ঘটত (network partition + node crash + একটা নির্দিষ্ট client retry pattern) — মানুষের পক্ষে সব combination কল্পনা করে test লেখা প্রায় অসম্ভব, কিন্তু exhaustive exploration সব combination স্বয়ংক্রিয়ভাবে ধরে ফেলে।
paper-এর আরেকটা গুরুত্বপূর্ণ পর্যবেক্ষণ: engineer-রা যারা TLA+ শিখে ব্যবহার করেছেন, তারা রিপোর্ট করেছেন যে শুধু স্পেসিফিকেশন লেখার প্রক্রিয়া-ই design-এর ফাঁক প্রকাশ করে দেয় — model checker চালানোর আগেই, কারণ একটা protocol-কে precise, ambiguity-মুক্ত ভাষায় লিখতে বাধ্য হলে “hand-wavy” অংশগুলো তাৎক্ষণিকভাবে ধরা পড়ে।
অন্যান্য জায়গায় formal methods
Raft consensus protocol। distributed-systems/raft-এ আমরা
Raft-এর Log Matching Property আর Leader Completeness-এর induction-ভিত্তিক
প্রমাণ দেখেছি। Diego Ongaro-র মূল PhD dissertation-এ এই একই
guarantee-গুলোর একটা সম্পূর্ণ TLA+ specification আছে, যা TLC
দিয়ে model-checked — Raft-কে শুধু “বোঝার জন্য সহজ” না, formally
verified বলেও দাবি করা যায়।
Chip design। Intel-এর ঐতিহাসিক Pentium FDIV bug (১৯৯৪, ভুল floating-point division টেবিলের একটা entry-র কারণে ভুল উত্তর)-এর পর, Intel floating-point unit ডিজাইনে formal verification (model checking-এর একটা রূপ) বাধ্যতামূলক করে দেয় — একটা bug-এর কারণে কোম্পানির ৪৭৫ মিলিয়ন ডলার recall খরচ হয়েছিল।
Aviation ও safety-critical software। DO-178C (বিমান software certification standard)-এর উচ্চতর assurance level formal methods-কে explicit ভাবে স্বীকৃতি দেয় testing-এর পরিপূরক হিসেবে।
Database transaction protocol। Two-phase commit, distributed lock manager-এর মতো protocol-এর নতুন optimization প্রায়ই TLA+ দিয়ে প্রথমে verify করা হয় প্রোডাকশনে যাওয়ার আগে — MongoDB, CockroachDB-র মতো কোম্পানি প্রকাশ্যে TLA+ specification প্রকাশ করেছে তাদের consensus/replication layer-এর জন্য।
যে ভুলগুলো সবাই করে
“Formal verification মানে প্রমাণ যে software-এ কোনো bug নেই।”
না — এটা প্রমাণ করে software আপনার লেখা specification মেনে চলে।
যদি specification-টাই অসম্পূর্ণ বা ভুল হয় (যেমন কোনো property লিখতেই ভুলে গেছেন, বা বাস্তব environment-এর একটা প্রয়োজনীয় শর্ত model-এ ধরা পড়েনি), তাহলে proof passing করলেও production bug থাকতে পারে।
“formal verification” মানে trusted base-টা code থেকে specification-এ সরে যায় — কিন্তু সম্পূর্ণ অদৃশ্য হয় না। এটাই কারণ CompCert বা seL4-এর মতো প্রজেক্টেও সতর্কতার সাথে বলা হয়: “আমরা প্রমাণ করেছি implementation specification মেনে চলে — specification নিজে ঠিক বাস্তব প্রয়োজনীয়তা ধরেছে কি না, সেটা আলাদা প্রশ্ন।”
“Model checking মানে সব সম্ভাব্য input চালিয়ে দেখা — এটা আসলে খুব ধীর exhaustive testing।”
সম্পূর্ণ ভিন্ন। Testing একটা নির্দিষ্ট input-এর জন্য program চালিয়ে দেখে ফলাফল ঠিক কি না। Model checking একটা abstract model-এর সব reachable state explore করে একটা property (যা অসীম সংখ্যক execution trace-কে cover করে) যাচাই করে।
একটা concurrent system-এ n input-এর জন্য test করলে হয়তো nটা
নির্দিষ্ট schedule-এই চলে; model checking সেই একই input-এর সব
সম্ভাব্য schedule (interleaving) দেখে — experiment section-এর
20টা interleaving সবগুলোই একই input নিয়ে, শুধু ভিন্ন timing।
Testing এই পার্থক্য কখনো ধরতে পারে না, কারণ একটা মাত্র schedule
চলে প্রতিটা রান-এ।
“Hoare logic শুধু toy academic example-এর জন্য — বাস্তব software-এ ব্যবহারযোগ্য নয়।”
আংশিক পুরনো ধারণা। সরাসরি হাতে Hoare-style proof লেখা বড় codebase-এ বিরল, ঠিক — কিন্তু এর নীতি সর্বত্র লুকিয়ে আছে:
- Dafny, F, Verus* — এই ভাষাগুলো প্রতিটা function-এর জন্য precondition/postcondition/loop invariant লেখাকে ভাষার অংশ করে দিয়েছে, আর compiler-ই automatically Hoare-style verification condition তৈরি করে SMT solver দিয়ে check করে।
assertstatement — বাস্তব production কোডে যে assertion লেখেন, সেগুলো আসলে informal Hoare precondition/postcondition, runtime-এ checked।- Rust-এর borrow checker — টাইপ-স্তরে ownership/lifetime নিয়ম চেক করা, formal reasoning-এরই একটা বিশেষায়িত, automated রূপ।
Hoare logic হাতে না লিখলেও, এর কাঠামোগত চিন্তা (“কী precondition লাগবে, কী invariant maintain হয়”) প্রতিটা careful engineer-এর মাথায় implicitly চলে।
বুঝেছেন কি না দেখুন
1Assignment axiom সরাসরি প্রয়োগ করে দেখান কেন
{n ≥ 0} i:=1; s:=0 {s = (i-1)·i/2 ∧ 1 ≤ i ≤ n+1} সত্য — sequence
rule আর assignment axiom ব্যবহার করে, পেছন থেকে সামনে।
প্রয়োগ
{n ≥ 0} i:=1; s:=0 {s = (i-1)·i/2 ∧ 1 ≤ i ≤ n+1} সত্য — sequence
rule আর assignment axiom ব্যবহার করে, পেছন থেকে সামনে।Postcondition I: s = (i-1)i/2 ∧ 1 ≤ i ≤ n+1 থেকে শুরু, পেছনের
দিকে যাই।
দ্বিতীয় statement (s:=0) -এর জন্য assignment axiom: I-তে
s-এর জায়গায় 0 বসাই:
প্রথম statement (i:=1) -এর জন্য assignment axiom: উপরের
predicate-এ i-এর জায়গায় 1 বসাই:
তাহলে sequence rule অনুযায়ী:
ঠিক যা প্রমাণ করতে চেয়েছিলাম। এই একই ফলাফল আমরা lesson-এর মূল
টেক্সটে “সামনে থেকে” (i=1, s=0 বসিয়ে সরাসরি চেক করে) পেয়েছিলাম —
দুইটা পথই মেলে, কারণ assignment axiom logically সমতুল্য দুইদিকেই
প্রয়োগযোগ্য। Backward application (এই উত্তরে) সবসময় নিরাপদ —
mechanically প্রয়োগ করা যায়; forward substitution (“বসিয়ে চেক করা”)
প্রায়ই দ্রুত কিন্তু সবসময় সরাসরি rule-এর রূপ নয়, rule of
consequence প্রয়োজন হতে পারে।
2Factorial-এর জন্য এই loop-টার সম্পূর্ণ correctness প্রমাণ করুন
Hoare logic দিয়ে (invariant বলুন, initialization ও maintenance
দেখান):
// precondition: n ≥ 0
k := 0;
p := 1;
while (k < n) do
k := k + 1;
p := p * k
// postcondition: p = n!
প্রয়োগ
// precondition: n ≥ 0
k := 0;
p := 1;
while (k < n) do
k := k + 1;
p := p * k
// postcondition: p = n!Invariant: I: p = k! ∧ 0 ≤ k ≤ n
এটা postcondition (p = n!)-এর সাধারণীকরণ, ঠিক sum-loop-এর মতো —
“এখন পর্যন্ত যতগুলো factor গুণ হয়েছে, p-তে তাদের factorial আছে।”
Initialization: {n ≥ 0} k:=0; p:=1 {I}
I-তে k=0, p=1 বসাই: 1 = 0! = 1 ✓, আর 0 ≤ 0 ≤ n ⟺ n ≥ 0 ✓
— ঠিক precondition। Sequence + assignment axiom দিয়ে formally ধাপে
ধাপে: I[1/p][0/k] সরল করলে n ≥ 0 আসে (Question 1-এর মতোই কৌশল)।
Maintenance: {I ∧ k < n} k:=k+1; p:=p*k {I}
পেছন থেকে: I-তে p-এর জায়গায় p*k বসাই না — প্রথমে শেষ statement
(p := p*k) থেকে শুরু। I-তে p-এর জায়গায় p*k বসাই:
এবার প্রথম statement (k := k+1) — এই predicate-এ k-এর জায়গায়
k+1 বসাই:
সরলীকরণ: (k+1)! = (k+1)·k!, তাই প্রথম অংশ ⟺ p = k! (যেহেতু
k+1 ≠ 0, দুই পাশ ভাগ করা যায়)। দ্বিতীয় অংশ ⟺ -1 ≤ k ≤ n-1।
তাহলে দেখাতে হবে: I ∧ (k < n) ⟹ (p = k! ∧ -1 ≤ k ≤ n-1)
I মানে p = k! (মিলে যায়) আর 0 ≤ k ≤ n, প্লাস k < n মানে
0 ≤ k ≤ n-1 — যা -1 ≤ k ≤ n-1-কে entail করে (দুর্বল করে) ✓
Rule of consequence দিয়ে maintenance প্রমাণিত। ∎
Loop শেষে: I ∧ ¬(k<n) = p=k! ∧ 0≤k≤n ∧ k≥n ⟹ k=n, তাই
p = n! — postcondition। ✓
Termination: variant n - k, প্রতি iteration-এ ঠিক ১ কমে,
k ≤ n থাকা অবস্থায় অ-ঋণাত্মক — well-ordering দিয়ে সসীম ধাপে থামে।
3কেন while true do skip-এর জন্য যেকোনো postcondition Q Hoare
logic-এ partial-correct প্রমাণ করা যায়? এটা কি formal methods-এর
একটা দুর্বলতা?
যুক্তি
while true do skip-এর জন্য যেকোনো postcondition Q Hoare
logic-এ partial-correct প্রমাণ করা যায়? এটা কি formal methods-এর
একটা দুর্বলতা?While rule দিয়ে I = true নিন। {true ∧ true} skip {true} তুচ্ছভাবে
সত্য (skip কিছু বদলায় না, আর true সবসময় সত্য)। While rule দেয়:
Postcondition true ∧ false = false। Logic-এ false থেকে যেকোনো
কিছু follow করে (ex falso quodlibet), আর rule of consequence
বলে যদি Q' ⟹ Q হয় তাহলে postcondition Q'-কে যেকোনো দুর্বল Q
দিয়ে replace করা যায়। যেহেতু false ⟹ Q (যেকোনো Q-এর জন্য
vacuously সত্য), তাই আমরা যেকোনো Q বসাতে পারি।
এটা দুর্বলতা নয় — এটা partial correctness-এর সংজ্ঞার সরাসরি
পরিণতি। Partial correctness বলে “যদি থামে, তাহলে Q”। এই loop
কখনো থামে না, তাই “যদি থামে” শর্তটা কখনো satisfy হয় না — পুরো
statement vacuously সত্য, ঠিক যেমন “যদি 2+2=5, তাহলে চাঁদ পনিরের
তৈরি” logically সত্য একটা statement (মিথ্যা premise থেকে)।
এই কারণেই lesson-এর মূল টেক্সটে জোর দেওয়া হয়েছে: partial correctness একা কখনো যথেষ্ট নয় — সবসময় আলাদাভাবে termination (variant function দিয়ে) প্রমাণ করতে হবে। যে কোনো serious verification tool (Dafny, F*) এই দুটোকে একসাথে বাধ্যতামূলক করে, ঠিক এই ফাঁকটা বন্ধ করতে।
4Experiment section-এ আমরা দেখেছি 20টা সম্ভাব্য interleaving-এর
মধ্যে 18টা race condition-এ ভোগে। যদি একজন engineer বলেন “আমরা
এই কোড হাজার বার production-এ চালিয়েছি, কখনো bug দেখিনি — তাই এটা
নিরাপদ”, এই যুক্তির সমস্যা কী, model checking-এর দৃষ্টিতে?
যুক্তি
20টা সম্ভাব্য interleaving-এর
মধ্যে 18টা race condition-এ ভোগে। যদি একজন engineer বলেন “আমরা
এই কোড হাজার বার production-এ চালিয়েছি, কখনো bug দেখিনি — তাই এটা
নিরাপদ”, এই যুক্তির সমস্যা কী, model checking-এর দৃষ্টিতে?সমস্যাটা দুই ভাগে।
প্রথমত, “কখনো দেখিনি” মানে “ঘটেনি না” — মানে “দেখা যায়নি”। Race condition-এর ফলাফল যদি silently ভুল হয় (যেমন counter একটু কম হওয়া, কিন্তু crash না করা), তাহলে bug detect না হয়ে চুপচাপ ভুল ডেটা তৈরি হতে পারে। Production-এ “কখনো দেখিনি” প্রায়ই মানে “কখনো লক্ষ্য করিনি” — একটা payment system-এ ৫০ টাকার discrepancy হয়তো মাসের রিপোর্টে চাপা পড়ে যায়, যতক্ষণ না accumulate হয়ে বড় হয়।
দ্বিতীয়ত, “হাজার বার চলেছে” একটা statistical sample, exhaustive
proof না। Production-এর thread scheduler বাস্তবে 20টা possible
interleaving-এর মধ্যে সব equally likely চালায় না — কিছু interleaving
(যেমন একটা thread সম্পূর্ণ শেষ করে আরেকটা শুরু হওয়া) অনেক বেশি সম্ভাব্য,
কারণ context switch ব্যয়বহুল আর scheduler কম করে। তাই bug-উৎপাদনকারী
বিরল interleaving হয়তো মিলিয়ন রান-এ একবার ঘটে — production-এ “হাজার
বার” এখনো সেই মিলিয়নের ধারেকাছেও না।
Model checking-এর মূল সুবিধা ঠিক এখানেই: এটা “সম্ভাবনার” উপর নির্ভর করে না। এটা প্রতিটা সম্ভাব্য interleaving structurally enumerate করে — rare হোক বা common, exhaustive exploration-এ সবগুলোই সমান গুরুত্ব পায়। এটাই AWS-এর case study-তে দেখা bug-গুলোর pattern: এমন rare timing window যা বছরের production traffic-এও ঘটেনি, কিন্তু TLC কয়েক মিনিটেই খুঁজে বের করেছে।
5আপনি একটা নতুন distributed lock service ডিজাইন করছেন। প্রথমে কোন
পদ্ধতিতে (Hoare logic, model checking, বা TLA+) verify করা শুরু
করবেন, আর কেন? তিনটার প্রতিটার ভূমিকা কোথায় হতে পারে একই project-এ?
ডিজাইন
তিনটাই আলাদা স্তরে, একসাথে ব্যবহারযোগ্য — কোনোটা “সেরা” না, প্রতিটার নিজস্ব জায়গা।
প্রথমে TLA+ দিয়ে protocol-level design। Lock service-এর মূল ঝুঁকি
distributed-systems-এর — network partition, node crash, message
reorder/loss, concurrent client request। এগুলো precisely এমন জায়গা
যেখানে TLA+ সবচেয়ে শক্তিশালী: পুরো protocol-কে state machine হিসেবে
লিখে, safety property (একই সময়ে দুইজন lock ধরতে পারবে না) আর
liveness property (lock request শেষ পর্যন্ত granted বা denied হবেই — চিরকাল ঝুলে থাকবে না) যাচাই করা। এটা implementation লেখার
আগে করা উচিত — design-এর ভুল তখন সস্তায় ধরা পড়ে, কোড লেখার পরে
নয়।
তারপর, নির্দিষ্ট critical algorithm-এর জন্য Hoare logic-স্টাইল reasoning। যেমন lock-এর internal state machine-এর একটা sequential অংশ (একটা single node-এর মধ্যে lease renewal logic) — এখানে explicit precondition/postcondition/invariant লেখা (হয়তো Dafny/F*-এর মতো tool দিয়ে, না হলে code comment + assertion হিসেবে) implementation-এর correctness নিশ্চিত করে, protocol-level design ঠিক থাকলেও।
সবশেষে, model checking implementation-এর কাছাকাছি স্তরে। যদি TLA+ spec-এর কাছাকাছি একটা executable model বানানো যায় (বা production code-এর উপর সরাসরি — যেমন Jepsen-এর মতো টুল, যেটা আসল distributed system চালিয়ে network fault inject করে আর observed behavior একটা formal consistency model-এর বিরুদ্ধে checks করে), সেটা implementation আসলেই spec মেনে চলছে কি না তার একটা empirical সেতু — কারণ TLA+ proof শুধু spec সম্পর্কে বলে, code spec সঠিকভাবে implement করেছে কি না তা আলাদা প্রশ্ন (এই ফাঁকটাই Misconception section-এর প্রথম দাবির মূল কথা)।
সংক্ষেপে: TLA+ → design-এর সবচেয়ে ঝুঁকিপূর্ণ (concurrent/distributed) অংশ প্রথমে যাচাই, Hoare-স্টাইল reasoning → sequential algorithm-এর বিস্তারিত সঠিকতা, model checking/testing tool (Jepsen) → code স্পেসিফিকেশন মেনে চলছে কি না তার empirical যাচাই। তিনটা মিলিয়ে trusted base যতটা সম্ভব ছোট করা যায়, একটাও একা সম্পূর্ণ নিশ্চয়তা দেয় না।
এরপর কী
আমরা দেখলাম কীভাবে program-কে state আর transition-এর মধ্য দিয়ে যুক্তি দেওয়া যায় — Hoare triple precondition/postcondition দিয়ে, model checking reachable state দিয়ে, TLA+ পুরো protocol-কে state machine হিসেবে।
কিন্তু একটা সম্পূর্ণ ভিন্ন দৃষ্টিভঙ্গি আছে computation বোঝার — যেখানে
কোনো “state” নেই, কোনো assignment নেই, শুধু function। পরের লেসনে
আমরা ফিরে যাব ১৯৩০-এর দশকে, Alonzo Church-এর কাছে, যিনি computation-কে
মাত্র তিনটা নিয়মে ধরে ফেলেছিলেন — lambda calculus। এটাই সেই
ভাষা যেখান থেকে আজকের প্রতিটা lambda keyword (Python), arrow
function (JavaScript), আর সম্পূর্ণ functional programming paradigm-এর
জন্ম।
আরও পড়ুন
- An Axiomatic Basis for Computer Programming — C. A. R. Hoare · 1969 — মূল paper, যেখানে {P} C {Q} নোটেশন আর inference rule-গুলো প্রথম প্রকাশিত হয়
- The Temporal Logic of Actions — Leslie Lamport · 1994, ACM TOPLAS — TLA+-এর গাণিতিক ভিত্তি
- How Amazon Web Services Uses Formal Methods — Chris Newcombe, Tim Rath, Fan Zhang, Bogdan Munteanu, Marc Brooker, Michael Deardeuff · Communications of the ACM, 2015 — DynamoDB, S3 আর অন্যান্য AWS system-এ TLA+ দিয়ে production-এর আগে পাওয়া সূক্ষ্ম bug-এর কেস স্টাডি