Formal Semantics — ভাষার অর্থ গণিতে লেখা
Operational, Denotational & Axiomatic Semantics
ভাষার অর্থ prose-এ না লিখে গণিতে লেখার তিনটা ঐতিহ্য — operational (চলার ধাপগুলো), denotational (গাণিতিক function), axiomatic (assertion-এর চুক্তি) — আর language specification-গুলো বাস্তবে কোথায় কতটা এদের কাছে যায়।
আগে এটা বুঝি
আগের লেসন শেষ হয়েছিল একটা অস্বস্তিতে: আমরা বললাম semantics মানে “অর্থের নিয়মের বই” — কিন্তু সেই বইটা আমরা লিখলাম English prose-এ। “Assignment মানে environment-এ binding পালটানো” — শুনতে ভালো, কিন্তু prose-এর একটা পুরনো রোগ আছে: দ্ব্যর্থতা। আইনের দলিলে “সম্পত্তি” শব্দের মানে নিয়ে মামলা হয়। Programming language-এর specification-এ যদি এমন ফাঁক থাকে, তাহলে দুইটা compiler একই প্রোগ্রামকে দুই রকম চালাবে — এবং বাস্তবে দুটোই নিজের পক্ষে যুক্তি দেখাতে পারবে, কারণ spec-ই অস্পষ্ট।
গণিতের সমস্যা এটা না — গণিতে প্রতিটা symbol-এর মানে আগে থেকে ঠিক। তাহলে প্রশ্নটা natural: ভাষার অর্থ কি গণিতের ভাষায় লেখা যায়? উত্তর: হ্যাঁ, এবং তিনটা আলাদা ঐতিহ্যে লেখা হয়েছে, তিন রকম উদ্দেশ্যে। Operational semantics বলে “অর্থ মানে ঠিক কোন ধাপে কোন ধাপে কী ঘটে” — মেশিনের চোখে অর্থ। Denotational semantics বলে “অর্থ মানে একটা mathematical object-এ অনুবাদ” — গণিতবিদের চোখে অর্থ। Axiomatic semantics বলে “অর্থ মানে কোন assertion কোনটার আগে-পরে সত্য থাকে” — প্রমাণকারীর চোখে অর্থ।
আজকের প্রশ্ন তিনটা: (১) এই তিন ঐতিহ্য ঠিক কী আলাদা করে, আর কোনটা কোন কাজে লাগে? (২) একটা tiny language-এর rule-গুলো আসলে কেমন দেখতে — যথেষ্ট নিখুঁত যে দুইজন programmer একই rule থেকে একই interpreter বানাবে? (৩) আর বাস্তব ভাষার spec-গুলো — C, Java — এই গণিত থেকে ঠিক কতদূরে, আর সেই দূরত্বের দাম কে দেয়?
মূল ধারণা
খেলার মাঠ: একটা tiny arithmetic language
Formal semantics শেখার নিয়ম হলো সবচেয়ে ছোট ভাষা নিয়ে শুরু। আমাদের ভাষায় মাত্র চার রকম expression:
e ::= n (পূর্ণসংখ্যা literal)
| x (variable)
| e₁ + e₂ (যোগ)
| e₁ × e₂ (গুণ)
| if0 e₁ then e₂ else e₃if0 c then t else f মানে: c-র মান শূন্য হলে t, নইলে f।
এই চার construct-এ variable-ছাড়া যা আছে তা দিয়েই দেখা যায়
semantics লেখার তিন ঐতিহ্যের সবগুলো সিদ্ধান্ত।
Operational semantics — অর্থ মানে চলার ইতিহাস
Big-step (“এক ঢঙে ফলাফল”)
Big-step rule-গুলো বলে: একটা expression কোন condition-এ সরাসরি কোন final value-তে যায়। লেখার ধাঁচ — উপরে premise (যা ধরে নেওয়া হয়), নিচে conclusion (যা তখন দাবি করা যায়):
(B-Num) n যেকোনো পূর্ণসংখ্যা হলে
─────────────────────
⟨n, ρ⟩ ⇓ n
(B-Var) ρ(x) = v
─────────────────────
⟨x, ρ⟩ ⇓ v
⟨e₁, ρ⟩ ⇓ v₁ ⟨e₂, ρ⟩ ⇓ v₂
(B-Add) ─────────────────────────────────────
⟨e₁ + e₂, ρ⟩ ⇓ v₁ + v₂
⟨c, ρ⟩ ⇓ 0 ⟨t, ρ⟩ ⇓ v
(B-If-T) ──────────────────────────────────
⟨if0 c then t else f, ρ⟩ ⇓ v
⟨c, ρ⟩ ⇓ n ⟨f, ρ⟩ ⇓ v (n ≠ 0)
(B-If-F) ──────────────────────────────────
⟨if0 c then t else f, ρ⟩ ⇓ vপড়ার নিয়ম: ⟨e, ρ⟩ ⇓ v মানে “environment ρ-তে expression e-র
ফল v”। B-Add rule পড়ুন নিচ থেকে উপরে: “দাবি — e₁ + e₂-র ফল
v₁+v₂; এটা ধরা যায় যদি দুইটা ছোট দাবি সত্যি হয় — e₁-এর ফল
v₁ আর e₂-এর ফল v₂।” প্রতিটা rule একটা justified step;
পুরো derivation হলো এই rule-গুলো গাছের মতো জড়ানো — আর সেই গাছটাই
interpreter-এর recursion tree-র হুবহু mirror (একটু পরেই কোডে দেখবেন)।
Small-step (“এক ইঞ্চি এক ইঞ্চি”)
Small-step বদলে বলে: value নয়, expression থেকে expression-এ
এক ধাপে যাওয়া। লেখা হয় e → e′ দিয়ে:
(S-Add) n₁ + n₂ → n₃ যেখানে n₃ = n₁ + n₂
(S-Add-L) e₁ → e₁′
───────────────────────
e₁ + e₂ → e₁′ + e₂
(S-Add-R) e₂ → e₂′
─────────────────────── (যখন e₁ একটা value)
e₁ + e₂ → e₁ + e₂′
(S-If-T) if0 0 then t else f → t
(S-If-F) if0 n then t else f → f (n ≠ 0)S-Add-L/S-Add-R-এর কাজ খেয়াল করুন: এরা নিজেরা কিছু “হিসাব” করে না — এরা শুধু বলে কোন operand আগে কমাতে বৈধ। অর্থাৎ evaluation order নিজেই এই rule-বইয়ের অংশ — আগের লেসনের build section-এ যে evaluation-order-সিদ্ধান্তের কথা বলেছিলাম, formal semantics সেটাকে এভাবেই লেখে।
Denotational semantics — অর্থ মানে একটা function
তৃতীয় দৃষ্টিভঙ্গি ধাপের কথা ভুলে যায়। প্রতিটা expression-কে সরাসরি একটা mathematical function-এ অনুবাদ করা হয়:
পড়ার নিয়ম: 𝓔[e]ρ মানে “environment ρ দিলে expression e যে
সংখ্যায় translate হয়”। লক্ষ্য করুন ডানপাশের + আর বাঁপাশের +
ভিন্ন: বাঁটা আমাদের ভাষার syntax, ডানটা পূর্ণসংখ্যার গাণিতিক
যোগ। Semantics মানে এই অনুবাদ-niti-ই।
এই ঐতিহ্যের প্রাণ compositionality: composite expression-এর
denotation শুধু তার অংশগুলোর denotation দিয়েই গঠিত — অংশগুলোর
syntax আর দরকার নেই। এই এক property-ই compiler optimizer-এর
local reasoning সম্ভব করে: a*2 + b*2 দেখে 2*(a+b)-এ transform
নিরাপদ কি না ভাবতে হয় কেবল দুই subtree-র meaning, বাকি প্রোগ্রাম
নয়।
ভেতরে কী ঘটছে
Stuck state — “no rule applies”-এর ভয়ংকর মানে
Small-step জগতে একটা প্রশ্ন আসে: যদি কোনো rule-ই apply না হয়?
যেমন True + 3 — কোনো numeric rule নেই, কিন্তু expression-ও
value না। এই অবস্থার নাম stuck state, আর formal semantics
এটাকে একটা সুনির্দিষ্ট জায়গা দেয়: runtime type error মানে
stuck state-এ আটকে যাওয়া। এটা প্রথমবারের মতো “error” শব্দটাকে
নিখুঁত সংজ্ঞা দেয় — আগের লেসনের “চিৎকার করা” বিমূর্ত ছিল, এটা
গাণিতিক।
এই সংজ্ঞাটা থেকেই type theory-র সবচেয়ে সুন্দর theorem গুলোর একটা জন্ম হয় (লেসন ৩-এ পুরোটা): টাইপ যোগ করলে প্রমাণ করা যায় well-typed program কখনো stuck হয় না — Milner-এর বিখ্যাত বাক্য, “well-typed programs cannot go wrong”। Type system আসলে এই লেসনের rule-গুলোর উপর বসানো একটা gate-প্রমাণ, নতুন কিছু না।
Derivation গাছ = recursion গাছ
Big-step rule-টা আরেকবার দেখুন:
⟨e₁, ρ⟩ ⇓ v₁ ⟨e₂, ρ⟩ ⇓ v₂
(B-Add) ─────────────────────────────────────
⟨e₁ + e₂, ρ⟩ ⇓ v₁ + v₂এখন মনে করুন যেকোনো interpreter-এর recursive call:
return evaluate(e1, env) + evaluate(e2, env)
# └── premise ১ ──┘ └── premise ২ ──┘
# └──────────── conclusion ────────────────┘Rule-এর প্রতিটা premise একটা recursive call; conclusion হলো return। Inference rule আর interpreter একই জিনিসের দুই ভাষা। তাই “formal semantics পড়া” আর “interpreter লেখা” একই skill-এর দুই exercise — আর এই কারণেই একটা rule-set যথেষ্ট নিখুঁত যে দুইজন স্বাধীন engineer একই behavior-এর implementation বানাবে। এটাই spec-এর মানদণ্ড।
Axiomatic semantics — অর্থ মানে assertion-এর চুক্তি
তৃতীয় ঐতিহ্যটা statement-দের জগতে (assignment, sequence, loop)। Tony Hoare 1969-এ প্রস্তাব করলেন: প্রোগ্রামের অর্থ বোঝার সবচেয়ে কাজের রূপ একটা triple:
{P} C {Q}পড়া হয়: “P সত্য হলে C চালানো শেষ হলে (হলেই!) Q সত্য।”
এটা contract — P হলো precondition, Q postcondition। Assignment-এর
জন্য axiom টাই সবচেয়ে মজার, কারণ সেটা উল্টো দিকে চলে:
(Assign) {Q [ x ↦ E ]} x := E {Q}Q[x ↦ E] মানে Q-র ভেতরে প্রতিটা x-এর জায়গায় E বসানো।
উদাহরণ — আগের লেসনের x = x + 1-এর formal সংস্করণ:
{x + 1 = 6} x := x + 1 {x = 6}postcondition {x = 6} চাইলে আগে থেকে যা দরকার তা বের করতে
Q-তে x-এর জায়গায় x+1 বসাতে হয় — দাঁড়াল {x + 1 = 6}, অর্থাৎ
{x = 5}। এই উল্টো-হিসাবকে বলে weakest precondition — এটাই
Dijkstra-র guard command আর modern verifier-এর (Dafny, F*, ESC/Java)
engine। Loop-এর জন্য দরকার হয় একটা মৌলিক ধারণা:
Loop invariant — এমন একটা assertion যা loop-এর প্রতিটা iteration-এর শুরুতে সত্য থাকে। While-rule বলে: যদি invariant আর loop-condition মিলে body-র পরেও invariant টিকে থাকে, তাহলে loop শেষে invariant + exit-condition সত্য:
(I ∧ B) body (I) (loop শেষে ¬B)
──────────────────────────────────────────
{I} while B do body {I ∧ ¬B}এই rule-ই আমাদের প্রথম যন্ত্র যা infinite loop-কেও হাতে ধরে — big-step/small-step দুটোই loop-এর সামনে অসহায় (একটা কখনো ফেরে না, আরেকটা চিরকাল চলে), কিন্তু axiomatic rule বলে দেয় “যদি কখনো শেষ হয়, তবে কী সত্য থাকবে” — termination-কে আলাদা প্রশ্ন করে।
উদাহরণ
একটা expression, তিন ক্যামেরায়
Expression নিন:
if0 (2 + (-2)) then (10 × (3 + 4)) else (1000 + 34)ক্যামেরা ১ — big-step derivation (গাছ)
⟨2, ρ⟩ ⇓ 2 ⟨-2, ρ⟩ ⇓ -2 ⟨3, ρ⟩ ⇓ 3 ⟨4, ρ⟩ ⇓ 4
────────────────────────── ────────────────────────
⟨2+(-2), ρ⟩ ⇓ 0 ⟨3+4, ρ⟩ ⇓ 7
────────────────────────── ────────────────────────
⟨10×(3+4), ρ⟩ ⇓ 70
──────────────────────────────────────────────────────────
⟨if0 (2+(-2)) then (10×(3+4)) else (1000+34), ρ⟩ ⇓ 70নিচ থেকে উপরে পড়ুন — leaves থেকে root। B-If-T branch নিল, B-If-F branch-টা (1000+34) কখনোই derive হয়নি — lazy না, এটা rule-এর premise-ই শুধু একটা branch চায়।
ক্যামেরা ২ — small-step trace (video)
step 0: if0 (2 + -2) then (10 × (3 + 4)) else (1000 + 34)
step 1: if0 0 then (10 × (3 + 4)) else (1000 + 34)
step 2: 10 × (3 + 4)
step 3: 10 × 7
step 4: 70 ← value; থেমে গেললক্ষ্য করুন step 2: S-If-T এক ধাপে পুরো untaken branch মুছে দিল —
এবং (1000 + 34) কখনো হিসাবই হলো না। এই trace-এর প্রতিটা লাইন
debugger-এ single-step করলে যা দেখেন তার formal সংস্করণ।
ক্যামেরা ৩ — denotation
𝓔[ if0 c then t else f ] ρ = 𝓔[t]ρ (কারণ 𝓔[c]ρ = 𝓔[2+(-2)]ρ = 0)
= 𝓔[10 × (3+4)]ρ
= 𝓔[10]ρ × 𝓔[(3+4)]ρ
= 10 × (3+4) = 70এখানে “কোন branch নিল” কথাটাই নেই — শুধু এক chain of equalities, শেষে এক সংখ্যা। তিনটা ক্যামেরা, একটা উত্তর: ৭০।
সোর্স প্রোগ্রাম
│
▼
┌─────────────────┬─────────────────────┬─────────────────────┐
│ OPERATIONAL │ DENOTATIONAL │ AXIOMATIC │
│ অর্থ = ধাপগুলো │ অর্থ = function │ অর্থ = চুক্তি │
│ │ │ │
│ e → e′ → e″ … │ 𝓔[e] : Env → Z │ {P} e {Q} │
│ ⟨e,ρ⟩ ⇓ v │ │ │
│ │ │ │
│ কাজে লাগে: │ কাজে লাগে: │ কাজে লাগে: │
│ interpreter, │ optimizer, │ verifier, │
│ debugger, │ equivalence proof, │ correctness proof, │
│ profiler │ reasoning tool │ Dafny/F*/ESC │
└─────────────────┴─────────────────────┴─────────────────────┘
│ │ │
▼ ▼ ▼
"কীভাবে চলে" "কী-এর সাথে সমান" "কী প্রতিশ্রুতি টিকে"ছবিতে
current expression e
│
▼
┌─────────────────────────┐
│ e কি value? (num) │──হ্যাঁ──▶ থেমে গেল — ফল = e
└───────────┬─────────────┘
│ না
▼
┌─────────────────────────┐
│ top-level rule আছে? │──হ্যাঁ──▶ apply (যেমন n₁+n₂ → n₃)
└───────────┬─────────────┘
│ না — তাহলে context rule
▼
┌─────────────────────────┐
│ বাঁ operand value না? │──হ্যাঁ──▶ e = e₁ ⊕ e₂
└───────────┬─────────────┘ বাঁ দিকে recurse (S-⊕-L)
│ না (বাঁটা value)
▼
┌─────────────────────────┐
│ ডাঁ operand value না? │──হ্যাঁ──▶ ডাঁ দিকে recurse (S-⊕-R)
└───────────┬─────────────┘
│ না
▼
STUCK STATE
(কোনো rule নেই — এটাই
runtime type error!)Flowchart-টা হুবহু এই লেসনের experiment-এর step() function-এর
control flow — formal rule থেকে code পর্যন্ত দূরত্ব শূন্য। আর
STUCK box-টা মনে রাখুন: লেসন ৩-এ টাইপ সিস্টেম যোগ করার পুরো লক্ষ্য
এই box-এ কেউ না পৌঁছানো সেটার compile-time প্রমাণ।
নিজে চালিয়ে দেখুন
একই ভাষার দুই evaluator — big-step আর small-step পাশাপাশি
~80 লাইনে পুরো setup — AST builder, দুই evaluator, pretty-printer, আর side-by-side চালানো:
# tiny.py — ভাষা: n | x | e1 + e2 | e1 * e2 | if0 c then t else f
def num(n): return ('num', n)
def add(a, b): return ('add', a, b)
def mul(a, b): return ('mul', a, b)
def if0(c, t, f): return ('if0', c, t, f)
def show(e):
tag = e[0]
if tag == 'num': return str(e[1])
if tag == 'add': return f"({show(e[1])} + {show(e[2])})"
if tag == 'mul': return f"({show(e[1])} × {show(e[2])})"
if tag == 'if0':
return f"(if0 {show(e[1])} then {show(e[2])} else {show(e[3])})"
raise ValueError(tag)
# ── Big-step: ⟨e, ρ⟩ ⇓ v ────────────────────────────────────
def big_step(e, env=None):
env = {} if env is None else env
tag = e[0]
if tag == 'num':
return e[1] # B-Num
if tag == 'var':
return env[e[1]] # B-Var
if tag == 'add':
return big_step(e[1], env) + big_step(e[2], env) # B-Add
if tag == 'mul':
return big_step(e[1], env) * big_step(e[2], env) # B-Mul
if tag == 'if0':
c = big_step(e[1], env)
return big_step(e[2], env) if c == 0 else big_step(e[3], env)
raise ValueError(f"stuck (big-step): {e}")
# ── Small-step: e → e′ ──────────────────────────────────────
def is_value(e):
return e[0] == 'num'
def step(e):
tag = e[0]
if tag in ('add', 'mul'):
left, right = e[1], e[2]
if not is_value(left):
return (tag, step(left), right) # S-⊕-L
if not is_value(right):
return (tag, left, step(right)) # S-⊕-R
if tag == 'add':
return ('num', left[1] + right[1]) # S-Add
return ('num', left[1] * right[1]) # S-Mul
if tag == 'if0':
cond = e[1]
if not is_value(cond):
return ('if0', step(cond), e[2], e[3]) # S-If-C
return e[2] if cond[1] == 0 else e[3] # S-If-T / S-If-F
raise ValueError(f"STUCK: {e} — কোনো rule apply হয় না")
def run_small(e):
trace = [show(e)]
while not is_value(e):
e = step(e)
trace.append(show(e))
return e[1], trace
# ── চালান — একই প্রোগ্রাম, দুই ক্যামেরা ─────────────────────
P = if0(add(num(2), num(-2)),
mul(num(10), add(num(3), num(4))),
add(num(1000), num(34)))
print("program:", show(P))
print("big-step result:", big_step(P))
value, trace = run_small(P)
print("\nsmall-step trace:")
for i, term in enumerate(trace):
print(f" step {i}: {term}")
print("small-step result:", value)
assert big_step(P) == value, "দুই evaluator একমত নয়!"
print("✔ big-step == small-step")প্রত্যাশিত output:
program: (if0 (2 + -2) then (10 × (3 + 4)) else (1000 + 34))
big-step result: 70
small-step trace:
step 0: (if0 (2 + -2) then (10 × (3 + 4)) else (1000 + 34))
step 1: (if0 0 then (10 × (3 + 4)) else (1000 + 34))
step 2: (10 × (3 + 4))
step 3: (10 × 7)
step 4: 70
small-step result: 70
✔ big-step == small-stepতিনটা পর্যবেক্ষণ:
(1000 + 34)কখনো হিসাবই হলো না — trace-এ একবারও নেই। Rule বলেছে untaken branch বাদ; interpreter শুধু rule-এর echo।- Trace-এর প্রতিটা লাইন আগের example section-এর হাতে-করা trace-এর সাথে হুবহু মেলে — formal rule আর code একই জিনিস।
big_step(add(...))-এ দুই premise-এর order (e1আগে নাe2) ফল বদলায় না, কিন্তুrun_small-এর order rule-এ লেখা। Big-step-এ order implicit, small-step-এ explicit — এটাই দুই স্টাইলের সবচেয়ে practical পার্থক্য।
একই tiny language-এর big-step evaluator আর small-step reducer হুবহু একই final value দেয় — operational semantics-এর দুই স্টাইল একই অর্থের দুই encoding, আর small-step trace দেখায় evaluation order rule-এর অংশ।
নিজে বানান
`let x = e₁ in e₂` — দুই evaluator-এই binding যোগ করুন
- AST-তে নতুন node যোগ করুন — (let, name, bound_expr, body)
- Big-step-এ B-Let rule implement করুন — environment extend করে
- Small-step-এ substitution-based S-Let rules implement করুন
- Shadowing test চালান — nested let একই নামে
- Unbound variable দিয়ে stuck-state error দেখুন
নতুন construct:
e ::= ... | let x = e₁ in e₂মানে: environment-এ x := e₁-র মান bind করে e₂ হিসাব করো।
দুই evaluator-এ দুই ভিন্ন কৌশল লাগবে — আর সেই পার্থক্যটাই শিক্ষণীয়।
def let(x, bound, body):
return ('let', x, bound, body)
# ── Big-step: environment extend করে (dynamic approach) ──────
def big_step_v2(e, env=None):
env = {} if env is None else env
tag = e[0]
if tag == 'let':
v = big_step_v2(e[2], env) # B-Let premise ১: e₁ ⇓ v₁
return big_step_v2(e[3], {**env, e[1]: v}) # premise ২: নতুন ρ-তে e₂
# ... বাকি cases আগের big_step-এর মতোই
return big_step(e, env)
# ── Small-step: substitution দিয়ে (static approach) ──────────
def subst(e, x, val):
"""e-র ভেতরে ('var', x)-এর সব occurrence-এর জায়গায় val বসাও"""
tag = e[0]
if tag == 'num': return e
if tag == 'var': return val if e[1] == x else e
if tag in ('add', 'mul'):
return (tag, subst(e[1], x, val), subst(e[2], x, val))
if tag == 'if0':
return ('if0', subst(e[1], x, val),
subst(e[2], x, val), subst(e[3], x, val))
if tag == 'let':
_, y, bound, body = e
if y == x: # ভেতরের let একই নাম —
return ('let', y, subst(bound, x, val), body) # shadowing: ভেতরের body স্পর্শ নয়
return ('let', y, subst(bound, x, val),
subst(body, x, val))
raise ValueError(tag)
def step_v2(e):
tag = e[0]
if tag == 'let':
_, x, bound, body = e
if not is_value(bound):
return ('let', x, step_v2(bound), body) # S-Let-Bound
return subst(body, x, bound) # S-Let: substitute
# ... বাকি cases আগের step-এর মতোই
return step(e)
# ── pretty-printer: let সহ (experiment-এর show-এর extension) ──
def show_v2(e):
tag = e[0]
if tag == 'num':
return str(e[1])
if tag == 'var':
return e[1]
if tag in ('add', 'mul'):
op = ' + ' if tag == 'add' else ' × '
return f"({show_v2(e[1])}{op}{show_v2(e[2])})"
if tag == 'if0':
return f"(if0 {show_v2(e[1])} then {show_v2(e[2])} else {show_v2(e[3])})"
if tag == 'let':
return f"(let {e[1]} = {show_v2(e[2])} in {show_v2(e[3])})"
raise ValueError(tag)
# ── Test: shadowing ──────────────────────────────────────────
# let x = 5 in (let x = x + 1 in (x × x))
P2 = let('x', num(5),
let('x', add(('var', 'x'), num(1)),
mul(('var', 'x'), ('var', 'x'))))
print("big-step:", big_step_v2(P2)) # 36 — ভেতরের x = 6
value, trace = [], []
e = P2
while not is_value(e):
e = step_v2(e)
trace.append(show_v2(e)) # প্রতিটা intermediate term
print("small-step:", e[1]) # 36 — একই উত্তর
for i, t in enumerate(trace):
print(f" step {i}: {t}")প্রত্যাশিত output:
big-step: 36
small-step: 36
step 0: (let x = (5 + 1) in (x × x))
step 1: (let x = 6 in (x × x))
step 2: (6 × 6)
step 3: 36তিনটা গভীর পর্যবেক্ষণ:
- Environment (big-step) বনাম substitution (small-step) — দুইটা binding-এর দুই বাস্তবায়ন: একটা lookup-table বহন করে, অন্যটা variable-কেই মান দিয়ে replace করে দেয়। Interpreter design-এ এই দুই স্টাইল আজও coexist।
- Shadowing-টা দুই স্টাইলে ভিন্নভাবে সমাধান — big-step-এ নতুন
dict-entry পুরনোটাকে ঢেকে দেয়; small-step-এ subst-এর
y == xspecial-case। Trace-এর step 0-এ দেখুন: বাইরের let substitute হয়েছে, কিন্তু ভেতরেরlet x = ...-এর body অক্ষত — নইলে উত্তর 36-এর বদলে ভুল হতো। - Naive substitution-এর সীমা: function (lambda) যোগ করলেই capture problem ধরা পড়বে — variable-এর নাম দুই scope-এ টক্কর খায়। Real systems α-renaming বা de Bruijn index ব্যবহার করে; compilers module-এ এটা ফিরবে।
নিজে বাড়ান:
- Unbound variable দিয়ে চালান —
let-ছাড়া bare'var'node। Small-step কোথায় STUCK হয় trace-এ দেখুন; big-step-এ কী ঘটে (KeyError) তুলনা করুন। divoperator যোগ করুন যার rule শুধু divisor ≠ 0-তে apply হয় — division-by-zero-কে stuck state হিসেবে লিখুন, exception না।- চ্যালেঞ্জ:
while-এর জন্য small-step rule লিখুন (while b do s → if b then (s; while b do s) else unit) — এবং দেখুন non-terminating program-এ big-step কখনো ফেরে না কিন্তু small-step চিরকাল step নেয়।
বাস্তব সিস্টেমে
Formal semantics যেখানে বাস্তবে দেখা যায়
C-এর undefined behavior — absence-এর স্বাক্ষর। C standard-এ
~200 ধরনের প্রোগ্রাম আচরণ সংজ্ঞায়িতই নয়: signed overflow,
null dereference, out-of-bounds access। Formal ভাষায়: ওই input-গুলোর
জন্য language-এর transition relation-ই define করা নেই। Compiler
writers এটাকে সুবিধা নেয় — “UB হতে পারে ধরে নিয়ে optimize”
(i < i+1-কে সরাসরি true করে দেওয়া, কারণ signed overflow UB)।
দাম: ভুল প্রোগ্রাম security vulnerability — আর এটাই CVE-গুলোর একটা
স্থায়ী উৎস।
Java Language Specification — presence-এর প্রচেষ্টা। JLS প্রায়
সবকিছু define করে: int overflow মানে wrap-around (silently), array
bounds সবসময় check হয়, floating-point-এর প্রতিটা bit even cross-
platform same (একসময় strictfp keyword দিয়ে enforce হতো)। Prose-
আকারে হলেও এটা operational semantics-এর দিকে হাঁটা — এবং Wadler-রা
JLS-কে formalize-ও করেছেন (Featherweight Java)।
WebAssembly — প্রথম-শ্রেণির industrial formal semantics। Wasm-এর official spec-এ executable formal semantics আছে — একটা reference interpreter যা spec-এর অংশ, OCaml-এ লেখা। Browser vendors এটাকে test-oracle হিসেবে চালায়: তাদের engine আর reference interpreter ভিন্ন ফল দিলে কারো কোনোটা ভুল — spec নিজেই referee।
CompCert — formal semantics থেকে verified compiler। Xavier Leroy আর team-এর CompCert C compiler Coq-এ প্রমাণিত: source semantics থেকে assembly semantics-এ প্রতিটা transformation correctness-preserving। NASA, Airbus জাতীয় জায়গায় এটা ব্যবহৃত — কারণ “compiler ভুল করলে সঠিক প্রোগ্রামও ভুল চলে” আর সেই ঝুঁকি প্রমাণ-দিয়ে মুছে ফেলা যায়।
Verifier-রা axiomatic ঐতিহ্যের product। Dafny, F*, ESC/Java2, Amazon-এর internal tools — সবগুলোর engine weakest-precondition calculation: ঠিক এই লেসনের assignment axiom-এর scaled-up সংস্করণ। AWS TLA+-এ distributed protocol প্রমাণ করা — সেই সংস্কৃতির আরেক branch।
K Framework আর ভাষা-as-spec। C, Java, JavaScript, Solidity — K-তে executable semantics লিখে দেওয়া হয়েছে, যেখান থেকে parser + interpreter + verifier auto-generated হয়। Ethereum-জগতে Solidity-র formal semantics-এর কাজ smart-contract audit industry-র ভিত্তি।
যে ভুলগুলো সবাই করে
“Formal semantics মানে ভাষাটা অবশ্যই functional বা academic — বাস্তব ভাষার এটা লাগে না।”
JavaScript, C, WebAssembly, JVM — সবগুলোর formal semantics লেখা হয়েছে, এবং সবগুলোরই industrial কারণ ছিল: JS-এর semantics (ES5 formalization) browser interop bug মেটাতে; Wasm-এরটা multi-vendor trust-এর ভিত্তি; CompCert-এরটা safety-critical compilation-এর। Formal semantics কোনো ভাষা-শৈলীর বিষয় না — specification quality-র বিষয়।
“Big-step আর small-step দুইটা ভিন্ন semantics — একটা বেছে নিলে অন্যটা বাতিল।”
দুটোই একই deterministic language-এর একই relation-এর দুই presentation — experiment-এ assert দিয়ে মিলিয়েছেন। বাস্তব practice-এ মানুষ দুটোই রাখে: small-step debugger/profiler/progress-reasoning-এর জন্য, big-step induction-proof-এর জন্য। Non-determinism বা divergence আলোচনায় পার্থক্য গুরুত্বপূর্ণ হয়ে ওঠে (infinite loop-এ big-step নিঃশব্দ, small-step সাক্ষী) — তাও “বাতিল” না, “কোন প্রশ্নে কোনটা বলে”।
“Undefined behavior মানে 'random ফল দেবে' বা 'crash করবে'।”
UB-র formal মানে কঠোরতর: ওই situation-এ কোনো constraint-ই নেই —
standard কোনো behavior promise করে না, তাই compiler ওই assumption
দিয়ে backward reasoning করতে পারে। if (p != NULL) return *p;-এ
deref পরে থাকায় compiler বাজি ধরতে পারে p null হতে পারে না এবং
check-টাই মুছে দিতে পারে। Random crash-এরা অন্তত চিৎকার করে; UB
নীরবে আপনার check মুছে দেয় — আগের লেসনের নীরব-ভুল শ্রেণির extreme
রূপ।
“Hoare triple-এর {P} C {Q} মানে C চলে — নাহলে partial correctness আর total correctness-এর পার্থক্য জানা নেই।”
Classic triple-টা partial correctness: “যদি P সত্য হয় এবং C terminate করে, তবে Q সত্য।” Infinite loop হলে triple ভাঙে না — vacuously সত্য। Termination-সহ দাবিকে বলে total correctness, আর সেটা প্রমাণে আলাদা যন্ত্র লাগে (variant/decreasing measure — loop প্রতি iteration-এ যে quantity কমে)। Verifier-রা এই দুই claim আলাদা report করে ঠিক এই কারণে।
বুঝেছেন কি না দেখুন
1Big-step আর small-step operational semantics-এর মধ্যে পার্থক্য এক
বাক্যে লিখুন, আর বলুন কোন স্টাইলটা infinite loop-এর ক্ষেত্রে কোন
তথ্য দেয়।
স্মরণ
Big-step এক derivation-এ expression সরাসরি final value-তে যায়
(⟨e,ρ⟩ ⇓ v); small-step expression থেকে expression-এ এক-ধাপে
যায় (e → e′)। Infinite loop-এ: big-step কোনো ফলই দেয় না —
derivation কখনো complete হয় না, কোনো observation নেই। Small-step
প্রতিটা ধাপে নতুন term দেয় — প্রোগ্রাম “চলছে, এখন এই state-এ আছে”
বলা যায়; debugger আর progress monitoring তাই small-step-এর জগৎ।
2(1 + 2) × (3 + 4)-এর small-step trace হাতে লিখুন (value হওয়া
পর্যন্ত)। মোট কয়টা step? আর big-step derivation-এ কয়টা B-Add/B-Mul
rule apply হবে?
প্রয়োগ
(1 + 2) × (3 + 4)-এর small-step trace হাতে লিখুন (value হওয়া
পর্যন্ত)। মোট কয়টা step? আর big-step derivation-এ কয়টা B-Add/B-Mul
rule apply হবে?Trace:
((1 + 2) × (3 + 4))
(3 × (3 + 4)) ← S-Add (বাঁ operand ready)
(3 × 7) ← S-Add-R (বাঁ value, ডাঁ কমল)
21 ← S-Mul3টা step। Big-step derivation-এ rule application: B-Add ×2 (ভেতরের দুই যোগ), B-Mul ×1 — মোট 3টা rule application। সংযোগটা সাধারণ না (step সংখ্যা order-dependent), কিন্তু এই সরল case-এ মিলে গেল — কারণ প্রতিটা rule-application এখানে ঠিক একটা reduction।
3if0 c then t else f-এর B-If-T rule-এ শুধু t-র derivation চাওয়া
হয়, f-এর না। যদি কেউ rule এমন লেখে যে দুই branch-ই derive
করতে হয় তারপর একটা বাছাই হয় — কোন বাস্তব প্রোগ্রামিং আচরণটা সেটা
simulate করবে, আর কোনটা হারাবে?
যুক্তি
if0 c then t else f-এর B-If-T rule-এ শুধু t-র derivation চাওয়া
হয়, f-এর না। যদি কেউ rule এমন লেখে যে দুই branch-ই derive
করতে হয় তারপর একটা বাছাই হয় — কোন বাস্তব প্রোগ্রামিং আচরণটা সেটা
simulate করবে, আর কোনটা হারাবে?দুই branch derive = strict/eager both-sides evaluation — যেমন
function-call arguments সবসময় হিসাব হওয়া (Python-এ f(g(), h())-এ
g আর h দুটোই চলে)। হারাবে: short-circuiting — and/or-এর
second operand না-চলা, conditional-এর untaken branch-এর side-effect
না-হওয়া। বাস্তব প্রমাণ: if x != 0 and 100/x > 5:-type কোড
zero-division থেকে বাঁচে শুধু one-branch-only rule-এর কারণে। অর্থাৎ
evaluation strategy নিজেই semantic decision — rule-এর premise
কয়টা, তাতেই ভাষার character বদলায়।
4Weakest precondition rule {Q[x ↦ E]} x := E {Q} ব্যবহার করে বের
করুন: (a) {?} x := x × 2 {x ≥ 10} (b) {?} x := x − 1 {x = 0 ∧ y = y₀}
প্রয়োগ
{Q[x ↦ E]} x := E {Q} ব্যবহার করে বের
করুন: (a) {?} x := x × 2 {x ≥ 10} (b) {?} x := x − 1 {x = 0 ∧ y = y₀}(a) Q-তে x-এর জায়গায় x × 2: {x × 2 ≥ 10} — অর্থাৎ {x ≥ 5}।
যেকোনো x ≥ 5 থেকে শুরু করলে doubling-এর পরে x ≥ 10 গ্যারান্টি।
(b) Q-তে x-এর জায়গায় x − 1: {x − 1 = 0 ∧ y = y₀} — অর্থাৎ
{x = 1 ∧ y = y₀}। Assignment y-কে ছুঁয়েছে না, তাই Q-র y-part
অপরিবর্তিত থেকে যায় — এটাই rule-এর automatic bookkeeping: আপনাকে
y-এর কথা ভাবতেই হয়নি।
5আপনাকে একটা DSL-এর spec লিখতে হবে যেটা তিন vendor আলাদা implement
করবে। কোন স্তরে কতটা formality বেছে নেবেন — (a) grammar, (b) core
evaluation rules, (c) performance characteristics? প্রতিটার জন্য
prose/E-BNF/inference-rule/reference-interpreter থেকে বাছুন আর
justify করুন।
ডিজাইন
(a) Grammar: E-BNF — context-free grammar এতটাই established notation যে prose লেখা redundant; ambiguity এখানেই বেশি ক্ষতিকর (parser disagreement মানে vendor-রা ভিন্ন program parse করবে)।
(b) Core evaluation: inference rules + reference interpreter দুটোই। Inference rules দেয় নিখুঁত, reviewable definition (dispute হলে rule cite হয়); reference interpreter দেয় executable oracle — conformance test suite-এর ভিত্তি। Wasm-এর spec ঠিক এই combination — proven formula।
(c) Performance: prose-এ non-binding guidance (“expected O(log n)”)। Performance-কে normative করলে implementation-এর হাত বাঁধা পড়ে — JVM JIT-এর মতো innovation অসম্ভব হত। তাই timing/asymptotics spec-এ hint, contract না।
সারকথা: formality যেখানে dispute হয় সেখানে বেশি, যেখানে freedom চাই সেখানে কম।
6Java-তে integer overflow wrap করে (defined); C-তে signed overflow
undefined। দুই সিদ্ধান্তের formal-semantics-দৃষ্টিতে মানে কী, আর
compiler optimizer-এর জন্য পার্থক্যটা কোথায় দেখা যায়?
যুক্তি
Formal দৃষ্টিতে: Java-র transition relation overflow-input-এও
define আছে (mod 2³² arithmetic); C-এর relation ওই point-এ define
নেই — domain hole। Optimizer-এর জন্য পার্থক্য মৌলিক: C compiler
x + 1 > x-কে নিরাপদে true বানাতে পারে, কারণ যদি overflow হত,
প্রোগ্রামের আচরণ already unconstrained — যে-কোনো transformation
legal। Java compiler-এ এই transform illegal: overflow-এ x + 1 > x
হতেই পারে false (wrap করে)। অর্থাৎ spec-এর hole-টা optimization-এর
fuel — C দ্রুত কিন্তু ভুল-প্রোগ্রামে বিস্ফোরক; Java predictable
কিন্তু কিছু transform নিষিদ্ধ। কোনোটা free না — আগের লেসনের
trade-off সুরটা formal semantics-এও বাজে।
এরপর কী
এরপর কী
আজ অর্থের বইটা গণিতে লিখলাম — তিন ঐতিহ্যে, আর দেখলাম rule থেকে
interpreter-এর দূরত্ব শূন্য। কিন্তু খেয়াল করুন কী ঘটল: আমাদের tiny
language-এ True + 3 লিখলে small-step machine নীরবে STUCK-এ বসে
থাকে — error টা তখনই জানা গেল যখন চালালাম। প্রশ্ন ওঠাই স্বাভাবিক:
চালানোর আগেই প্রমাণ করা যায় কি প্রোগ্রাম কখনো stuck হবে না?
পরের লেসনে — Type System — ঠিক এটাই: টাইপ মানে values-এর set আর legal operations-এর তালিকা; static/dynamic মানে প্রমাণটা কখন হয়; strong/weak মানে প্রমাণ ভাঙলে চিৎকার হয় না নীরব bit-reinterpretation। আর দেখব Milner-এর প্রমিত দাবি — well-typed programs cannot go wrong — কীভাবে এই লেসনের stuck state-কে compile-time-এ নিষিদ্ধ করে।
আরও পড়ুন
- Types and Programming Languages, Chapters 3–5 — Untyped Arithmetic Expressions — Benjamin C. Pierce · Small-step আর big-step operational semantics-এর canonical textbook treatment — এই লেসনের inference rule-গুলোর সরাসরি model
- The Formal Semantics of Programming Languages — Glynn Winskel · Operational, denotational আর axiomatic — তিন ঐতিহ্যেরই সমতুল্য-গভীর আলোচনা
- "An Axiomatic Basis for Computer Programming" — C. A. R. Hoare · 1969-এর মূল পেপার — Hoare triple আর assignment axiom এখান থেকে