Foundationপ্রথম নীতি থেকে
LEVEL 5লেসন ২/১৫রিসার্চ-লেভেল১ ঘণ্টা

Formal Semantics — ভাষার অর্থ গণিতে লেখা

Operational, Denotational & Axiomatic Semantics

ভাষার অর্থ prose-এ না লিখে গণিতে লেখার তিনটা ঐতিহ্য — operational (চলার ধাপগুলো), denotational (গাণিতিক function), axiomatic (assertion-এর চুক্তি) — আর language specification-গুলো বাস্তবে কোথায় কতটা এদের কাছে যায়।

এই লেসন শেষে আপনি পারবেন

  • Big-step আর small-step operational semantics-এর পার্থক্য ব্যাখ্যা করে একটা tiny arithmetic language-এর evaluation rule inference-rule আকারে পড়তে ও লিখতে পারবেন
  • Denotational semantics-এর compositionality ধারণা ব্যাখ্যা করে বলতে পারবেন expression কীভাবে mathematical function-এ অনুবাদ হয়, আর এটা operational দৃষ্টিভঙ্গি থেকে কোথায় আলাদা
  • Hoare triple `{P} C {Q}`-এর মানে পড়তে পারবেন, assignment axiom দিয়ে weakest precondition বের করতে পারবেন, আর loop rule-এ invariant-এর ভূমিকা বলতে পারবেন
  • C-এর undefined behavior আর Java-র fully-specified semantics-কে "formal semantics-এর presence বনাম absence" হিসেবে ব্যাখ্যা করতে পারবেন
  • একই tiny language-এর জন্য big-step evaluator আর small-step reducer দুটোই Python-এ লিখে দেখাতে পারবেন দুটোই একই ফল দেয়, আর তাতে `let` binding যোগ করতে পারবেন

আগে যা বোঝা থাকা দরকার

আগে এটা বুঝি

আগের লেসন শেষ হয়েছিল একটা অস্বস্তিতে: আমরা বললাম 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 [ n ] ρ=nE [ x ] ρ=ρ(x)\mathcal{E}\,[\,n\,]\,\rho = n \qquad\quad \mathcal{E}\,[\,x\,]\,\rho = \rho(x)

E [ e1+e2 ] ρ  =  E [ e1 ] ρ+E [ e2 ] ρ\mathcal{E}\,[\,e_1 + e_2\,]\,\rho \;=\; \mathcal{E}\,[\,e_1\,]\,\rho + \mathcal{E}\,[\,e_2\,]\,\rho

E [ if0 c then t else f ] ρ={E[t] ρযদি E[c] ρ=0E[f] ρঅন্যথায়\mathcal{E}\,[\,\text{if0 } c \text{ then } t \text{ else } f\,]\,\rho = \begin{cases} \mathcal{E}[t]\,\rho & \text{যদি } \mathcal{E}[c]\,\rho = 0 \\ \mathcal{E}[f]\,\rho & \text{অন্যথায়} \end{cases}

পড়ার নিয়ম: 𝓔[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        │
 └─────────────────┴─────────────────────┴─────────────────────┘
        │                   │                      │
        ▼                   ▼                      ▼
  "কীভাবে চলে"        "কী-এর সাথে সমান"       "কী প্রতিশ্রুতি টিকে"
তিন ঐতিহ্য — একই প্রোগ্রাম, তিন রকম 'অর্থ'-এর সংজ্ঞা, তিন ভিন্ন consumer।

ছবিতে

              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!)
Small-step reducer-এর হার্ট — প্রতিটা ধাপে ঠিক কোন sub-expression কমবে, context rule গুলো সেটাই ঠিক করে।

Flowchart-টা হুবহু এই লেসনের experiment-এর step() function-এর control flow — formal rule থেকে code পর্যন্ত দূরত্ব শূন্য। আর STUCK box-টা মনে রাখুন: লেসন ৩-এ টাইপ সিস্টেম যোগ করার পুরো লক্ষ্য এই box-এ কেউ না পৌঁছানো সেটার compile-time প্রমাণ।

নিজে চালিয়ে দেখুন

EXPERIMENT

একই ভাষার দুই evaluator — big-step আর small-step পাশাপাশি

Python 3.10+ (match লাগে না, tuple-ই যথেষ্ট)· ২০ মিনিট

~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

তিনটা পর্যবেক্ষণ:

  1. (1000 + 34) কখনো হিসাবই হলো না — trace-এ একবারও নেই। Rule বলেছে untaken branch বাদ; interpreter শুধু rule-এর echo।
  2. Trace-এর প্রতিটা লাইন আগের example section-এর হাতে-করা trace-এর সাথে হুবহু মেলে — formal rule আর code একই জিনিস।
  3. 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-এর অংশ।

নিজে বানান

BUILD IT

`let x = e₁ in e₂` — দুই evaluator-এই binding যোগ করুন

Python · ●●○○○
  1. AST-তে নতুন node যোগ করুন — (let, name, bound_expr, body)
  2. Big-step-এ B-Let rule implement করুন — environment extend করে
  3. Small-step-এ substitution-based S-Let rules implement করুন
  4. Shadowing test চালান — nested let একই নামে
  5. 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

তিনটা গভীর পর্যবেক্ষণ:

  1. Environment (big-step) বনাম substitution (small-step) — দুইটা binding-এর দুই বাস্তবায়ন: একটা lookup-table বহন করে, অন্যটা variable-কেই মান দিয়ে replace করে দেয়। Interpreter design-এ এই দুই স্টাইল আজও coexist।
  2. Shadowing-টা দুই স্টাইলে ভিন্নভাবে সমাধান — big-step-এ নতুন dict-entry পুরনোটাকে ঢেকে দেয়; small-step-এ subst-এর y == x special-case। Trace-এর step 0-এ দেখুন: বাইরের let substitute হয়েছে, কিন্তু ভেতরের let x = ...-এর body অক্ষত — নইলে উত্তর 36-এর বদলে ভুল হতো।
  3. Naive substitution-এর সীমা: function (lambda) যোগ করলেই capture problem ধরা পড়বে — variable-এর নাম দুই scope-এ টক্কর খায়। Real systems α-renaming বা de Bruijn index ব্যবহার করে; compilers module-এ এটা ফিরবে।

নিজে বাড়ান:

  1. Unbound variable দিয়ে চালান — let-ছাড়া bare 'var' node। Small-step কোথায় STUCK হয় trace-এ দেখুন; big-step-এ কী ঘটে (KeyError) তুলনা করুন।
  2. div operator যোগ করুন যার rule শুধু divisor ≠ 0-তে apply হয় — division-by-zero-কে stuck state হিসেবে লিখুন, exception না।
  3. চ্যালেঞ্জ: 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 করে ঠিক এই কারণে।

বুঝেছেন কি না দেখুন

1

Big-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 হবে?

প্রয়োগ

Trace:

((1 + 2) × (3 + 4))
(3 × (3 + 4))      ← S-Add (বাঁ operand ready)
(3 × 7)            ← S-Add-R (বাঁ value, ডাঁ কমল)
21                 ← S-Mul

3টা step। Big-step derivation-এ rule application: B-Add ×2 (ভেতরের দুই যোগ), B-Mul ×1 — মোট 3টা rule application। সংযোগটা সাধারণ না (step সংখ্যা order-dependent), কিন্তু এই সরল case-এ মিলে গেল — কারণ প্রতিটা rule-application এখানে ঠিক একটা reduction।

3

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 বদলায়।

4

Weakest precondition rule {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 চাই সেখানে কম।

6

Java-তে 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 এখান থেকে