Foundationপ্রথম নীতি থেকে
LEVEL 5লেসন ১/১৫কঠিন৪৫ মিনিট

Syntax বনাম Semantics — লেখা, অর্থ, আর ভুল হওয়ার তিনটা সময়

Syntax vs Semantics

Syntax হলো অক্ষর-আর-টোকেনের গঠন, semantics হলো সেই গঠনের অর্থ — আর প্রোগ্রাম ভুল হতে পারে তিনটা আলাদা মুহূর্তে, তিন রকমভাবে, আর প্রতিটা মুহূর্ত ধরার দায়িত্ব ভাষার ভিন্ন ভিন্ন যন্ত্রাংশের।

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

  • Syntax আর semantics-এর পার্থক্য এমনভাবে ব্যাখ্যা করতে পারবেন যেন একটা নির্দিষ্ট error message দেখেই বলা যায় সেটা কোন স্তরের ব্যর্থতা
  • একটা প্রোগ্রাম ভুল হওয়ার তিনটা সময় — parse time, static check time, runtime — আলাদা করে, প্রতিটার জন্য একটা করে concrete উদাহরণ দিতে পারবেন
  • একই লেখা (same string) ভিন্ন ভাষায় বা একই ভাষায় ভিন্ন evaluation rule-এ ভিন্ন অর্থ নেয় — অন্তত দুইটা উদাহরণ trace করে দেখাতে পারবেন
  • কেন `x = x + 1` syntax-দৃষ্টিতে সম্পূর্ণ বৈধ কিন্তু গণিতের দৃষ্টিতে অসংগতি, আর imperative semantics ঠিক কোন নিয়ম যোগ করায় সেটা বৈধ হয় — তা ব্যাখ্যা করতে পারবেন
  • একটা ছোট expression evaluator-এ semantic rule বদলে ফলাফল বদলে ফেলতে পারবেন, syntax/AST এক ইঞ্চিও নড়ানো ছাড়া

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

আগে এটা বুঝি

Module-এর driving question-টা মনে আছে? Python আর Rust — দুটোই “programming language”, কিন্তু নিচে পার্থক্যটা ঠিক কোথায়? এই প্রশ্নের সবচেয়ে প্রথম, সবচেয়ে ভিত্তিগত উত্তরটা এত সরল যে বেশিরভাগ মানুষ প্রথমবার শুনলে হতাশ হন: একটা programming language আসলে দুইটা সম্পূর্ণ আলাদা জিনিসের প্যাকেজ — একটা হলো লেখার নিয়ম (কোন অক্ষর-সারি বৈধ?), আরেকটা হলো অর্থের নিয়ম (বৈধ লেখাটা চালালে ঠিক কী ঘটবে?)। প্রথমটার নাম syntax, দ্বিতীয়টার নাম semantics। Python বনাম Rust-এর পার্থক্যের বড় একটা অংশ আসলে semantics কতটা আগে আর কতটা কড়াভাবে পরীক্ষা করা হয়, সেই সিদ্ধান্তে।

এখানে একটা সূক্ষ্ম বিষয় আছে যেটা না ধরলে পুরো module-টা ঝাপসা থেকে যাবে। একটা প্রোগ্রাম ভুল হতে পারে একটা সময়ে না — তিনটা সময়ে, এবং তিনটা সময়ের ভুল তিন রকমভাবে প্রকাশ পায়। একটা ভুল ধরা পড়ে প্রোগ্রামটা চলার আগেই, লেখা পড়ার স্তরে। আরেকটা ধরা পড়ে চলার মাঝে, একটা নির্দিষ্ট লাইনে পৌঁছালে। আর সবচেয়ে ভয়ের কথা — তৃতীয় ধরনের ভুল কোনোদিন ধরাই পড়ে না: প্রোগ্রাম হাসিমুখে exit code 0 দিয়ে শেষ হয়, অথচ উত্তরটা ভুল।

আজকের প্রশ্ন তিনটা: (১) ঠিক কোন স্তরটা “syntax” আর কোনটা “semantics”? (২) একই লেখা কীভাবে ভিন্ন নিয়মে ভিন্ন অর্থ নিতে পারে? (৩) ভুল ধরার তিনটা সময় — parse time, check time, runtime — আসলে কী আলাদা করে, আর কোন ভুলটা কোনো যন্ত্রই ধরতে পারে না?

মূল ধারণা

দুইটা স্তর, দুইটা নিয়মের বই

প্রতিটা ভাষা আসলে দুইটা আলাদা specification:

SyntaxSemantics
মূল প্রশ্নএই অক্ষর-সারিটা কি বৈধ লেখা?বৈধ লেখাটা চালালে ঠিক কী হবে?
রূপGrammar — BNF/E-BNF production ruleEvaluation rule — expression থেকে মান বের হওয়ার নিয়ম
কে বহন করেLexer আর parserInterpreter বা compiled code নিজেই
ভুলের চেহারাSyntaxError, parse errorType error, exception, অথবা নীরব ভুল উত্তর

Syntax সম্পূর্ণ syntactic — অর্থ থেকে সম্পূর্ণ স্বাধীন। if x { কুৎসিত, অর্থহীন, কিন্তু তার syntax ভুল কি না সেটা বিচারে অর্থ বুঝতেই হয় না — শুধু নিয়মের বইয়ের production rule-গুলোর সাথে মেলাতে হয়। ঠিক যেমন বাংলা ব্যাকরণে “চাঁদ ফুটল সন্ধ্যা বিকেলে” বাক্যের অর্থ না-ভেবেই বলা যায় এটা ব্যাকরণসম্মত — subject আছে, verb আছে, গঠন ঠিক আছে; বাক্যটা অর্থবহ কি না সেটা সম্পূর্ণ আলাদা প্রশ্ন।

Syntax-ও একটা নয়, তিনটা স্তরে ভাগ হয়

বাস্তব ভাষার বইগুলো (Sebesta-র chapter 3-ই যা করে) syntax-কে তিন স্তরে ভাগ করে, আর এই ভাগটাই ভুল-ধরার সময়গুলোর সাথে সরাসরি মেলে:

  1. Lexical structure — কোন অক্ষর-ক্রম একটা token বানায়। while, x, 123, <= — প্রতিটা একটা token। 123abc lexical স্তরেই প্রশ্নবিদ্ধ।
  2. Grammatical structure — token-ক্রমটা grammar-এর production rule মেনে চলে কি? if x { — token-রা সবই বৈধ, ক্রমটা incomplete।
  3. Contextual structure — grammar-মেনে-চলা লেখাটা প্রসঙ্গে বৈধ কি না: প্রতিটা variable declared কি না, argument সংখ্যা মিলছে কি না। অনেক বইয়ে এটার নাম static semantics — আর ঠিক এই স্তরটাই syntax আর semantics-এর সীমানায় বসে, কারণ এটা অর্থ ছুঁয়ে দেখে, তবে run না করেই।

Semantics তখন বাকি থাকে: বৈধ লেখাটা চলতে দিলে ঠিক কী ঘটবে। আর এখানেই আসে এই লেসনের সবচেয়ে ছোট, সবচেয়ে পরিচিত, সবচেয়ে বোঝা-উঁচু উদাহরণটা।

x = x + 1 — ছয় অক্ষরে পুরো বিভাজন

x = x + 1

Syntax দৃষ্টিতে এটা নিখুঁত: identifier, assignment operator, expression — grammar-এর প্রতিটা rule পূরণ। কোনো parser-ই এর বিরুদ্ধে একটা শব্দও বলবে না।

গণিতের দৃষ্টিতে এটা অসংগতি: কোনো সংখ্যা x-এর জন্য x = x + 1 সত্য নয় (উভয়পক্ষ থেকে x বাদ দিলে 0 = 1)। গণিতের = হলো equality — একটা সম্পর্ক, যা সত্য বা মিথ্যা, কখনো “ঘটানো” যায় না।

কিন্তু প্রোগ্রাম চালালে কোনো বিদ্রূপ নেই — x এক বেড়ে যায়। কীভাবে? কারণ imperative ভাষার semantics-এ = চিহ্নটা equality না — সেটা assignment: “বর্তমান environment-এ x-এর binding সরিয়ে রাখো, নতুন মান বসাও ডানপাশের expression-এর ফল।” একই অক্ষর, দুইটা সম্পূর্ণ ভিন্ন semantic rule, দুইটা সম্পূর্ণ ভিন্ন জগৎ। পরের লেসনে এই rule-টা গণিতের ভাষায় লিখেই দেখব — {x = 5} অবস্থায় x := x + 1 চালালে {x = 6} — কিন্তু আজ শুধু ধরে নিন: meaning আসে rule থেকে, অক্ষর থেকে নয়।

একই লেখা, ভিন্ন ভাষা, ভিন্ন অর্থ

Parser যা বানায় সেটা abstract syntax tree (AST) — লেখার whitespace ইত্যাদি ঝেড়ে ফেলে অবশিষ্ট থাকে খাঁটি গঠন। AST-ই syntax আর semantics-এর সীমানাপথ: তার আগে সব লেখা-নিয়ে, তার পরে সব অর্থ-নিয়ে। আর একই AST ভিন্ন ভাষায় ভিন্ন অর্থ নেয় — এটা বিমূর্ত কথা না, প্রতিদিনের বাস্তবতা:

ExpressionPythonJavaScriptC (int)Java
1 + "1"TypeError (runtime)"11" (string concat)pointer arithmetic — compile হয়, ফল আবর্জনাcompile error
1 / 20.50.50 (integer division)0
-7 / 2-3.5-3.5-3 (শূন্যর দিকে truncate)-3

একই অক্ষর, চার ভাষা, চার ভাগ্য। পার্থক্য এক জায়গাতেই: কোন ভাষার semantic rule-book-এ / বা +-এর জন্য কোন টাইপ-জুটির কী নিয়ম লেখা আছে। C-এর নিয়মবই এত উদার যে 1 + "1" বৈধ খাটে — string literal মানে pointer, pointer+int বৈধ operation। Java-র নিয়মবই এত কড়া যে জুটিটা লেখার সময়েই বারণ করে।

ভেতরে কী ঘটছে

ভুল হওয়ার তিনটা সময় — ঠিক কোন যন্ত্রাংশ কখন চিৎকার করে

সময় ১ — Parse time: লেখাই ভাঙা

Lexer token বানায়, parser grammar মিলিয়ে AST বানায়। যে-কোনো স্তরে ব্যর্থতা = SyntaxError। সবচেয়ে গুরুত্বপূর্ণ বৈশিষ্ট্য: এই ভুল প্রোগ্রামের একটা লাইনও চলার আগেই ধরা পড়ে — AST ছাড়া চালানোর কিছুই নেই।

$ python3 broken.py
  File "broken.py", line 2
    for n in numbers
                    ^
SyntaxError: expected ':'

ভুলটা line 2-তে, অথচ line 1-এর statement-ও চলেনি। Parse-time ব্যর্থতার এটাই স্বাক্ষর — পুরো ফাইলটাই অচল।

সময় ২ — Static check time: লেখা ঠিক, প্রতিশ্রুতি ভাঙা

AST তৈরি — গঠন বৈধ। এবার checker হাঁটে: variable declared? operation-এর operand-এর টাইপ নিয়মমতো? এই স্তর run ছাড়াই চলতে পারে, তবে syntax থেকে আলাদা কারণ এটাকে অর্থ পড়তে হয়।

TypeError: unsupported operand type(s) for +=: 'int' and 'str'

Python-এ এই ভুল runtime traceback দিয়ে আসে — কারণ Python-এ check স্তরটা runtime-এর ভেতরে বাস করে (dynamic typing — লেসন ৩-এ পুরো হিসাব আছে)। C বা Rust-এ একই ভুল compile টেবিলেই ধরা পড়ত। ভুলটা এক; ধরা-পড়ার সময় ভাষার ডিজাইন সিদ্ধান্ত।

সময় ৩ — Runtime পার হয়ে যাওয়া: কোনো চিৎকার নেই

তৃতীয় ধরনটা সবচেয়ে নীরব। লেখা বৈধ, টাইপও নিয়মমতো — কিন্তু অর্থটাই আপনি ভুল বুঝেছেন:

average = total // len(data)   # // লিখে ফেলেছি, মানে ছিল গড়
# কোনো error নেই। exit code 0। উত্তর 25, সঠিক হওয়ার ছিল 25.25

কোনো যন্ত্রাংশই এই ভুল ধরতে পারে না — কারণ যন্ত্র জানেই না আপনি কী চেয়েছিলেন। ভাষার কাছে শুধু rule আছে, intention-এর কোনো model নেই। এই স্তরের একমাত্র প্রতিষেধক test আর assertion — অর্থাৎ intention-টা আপনি নিজেই কোথাও মেশিনপাঠ্য করে রাখুন।

একটা প্রোগ্রামের জীবনে তিনটা gate
  1. Parse timelexer + parser — লেখা গঠনগতভাবে বৈধ কি না; ব্যর্থ হলে একটা লাইনও চলে না
  2. Static checkdeclarations, types, arity — লেখা বৈধ কিন্তু প্রতিশ্রুতি ভাঙা; Python-এ এটা runtime-এর ভেতরে বাস করে
  3. Evaluationsemantic rule মেনে চলা — এখান থেকে ভুল হলে হয় exception, নয়তো নীরব ভুল উত্তর

Python নিজেও আসলে compile করে — শুধু তাড়াতাড়ি করে

প্রচলিত ভুল ধারণা: Python “লাইনে-লাইনে পড়ে চলে”। সত্য না। CPython প্রতিটা module পুরোপুরি parse করে bytecode-এ compile করে, তারপর VM সেই bytecode চালায় — __pycache__/.pyc ফাইলগুলো ঠিক সেইজন্যই। তাই ফাইলের শেষ লাইনে syntax ভুল থাকলে প্রথম লাইনও চলে না:

import dis
code = compile("x = 1\ny = x + 1", "<demo>", "exec")
dis.dis(code)
  1           0 LOAD_CONST               0 (1)
              2 STORE_NAME               0 (x)

  2           4 LOAD_NAME                0 (x)
              6 LOAD_CONST               0 (1)
              8 BINARY_OP                0 (+)
             10 STORE_NAME               1 (y)

compile() সফল — parse আর bytecode-generation শেষ। dis দেখাচ্ছে VM যা চালাবে। Parse-time আর runtime-এর মাঝে bytecode নামের একটা পুরো জগৎ আছে, আর BINARY_OP-এর ভেতরেই লুকিয়ে আছে সেই semantic rules: operand-দুইটার টাইপ দেখে int-যোগ হবে না string-concat — কোনোটাই না মিললে তার ভেতর থেকেই TypeError ছুটে আসে।

উদাহরণ

একটা কাজ, তিন মৃত্যু — পাশাপাশি

একই কাজ — সংখ্যার তালিকার গড় — তিন রকম ভাঙা কপি:

মৃত্যু ১ — parse time (একটা লাইনও চলেনি):

def average(numbers):
    total = 0
    for n in numbers        # ← colon নেই
        total += n
    return total / len(numbers)

print("side effect")         # ← এই লাইনও চলবে না!
print(average([10, 20, 30, 41]))
SyntaxError: expected ':' — ফাইলের কিছুই চলেনি

মৃত্যু ২ — check/execution time (অর্ধেক পথে, চিৎকার করে):

def average(numbers):
    total = 0
    for n in numbers:
        total += n
    return total / len(numbers)

data = [10, "20", 30, 41]    # ← একটা string ঢুকে গেছে
print("side effect")
print(average(data))
side effect
TypeError: unsupported operand type(s) for +=: 'int' and 'str'
# loop আসলে চলতে শুরু করেছিল — total = 10 পর্যন্ত হয়ে গেছে —
# তারপর মাঝপথে আটকেছে

মৃত্যু ৩ — নীরব (কিছুই ধরে না, ভুল উত্তর দিয়ে সুখে শেষ):

def average(numbers):
    total = 0
    for n in numbers:
        total += n
    return total // len(numbers)   # ← গড় না, পূর্ণসংখ্যা-ভাগ

print("side effect")
print(average([10, 20, 30, 41]))   # 25 ছাপে — সঠিক হওয়ার ছিল 25.25
print("done")
side effect
25
done

তিন ক্ষেত্রেই ক্ষতি এক না। প্রথমটা সবচেয়ে সস্তা — কিছুই চলেনি, message সরাসরি জায়গা দেখায়। দ্বিতীয়টা মাঝামাঝি — কিছু কাজ নষ্ট, তবে চিৎকার আছে। তৃতীয়টা সবচেয়ে দামি — ভুল উত্তর report-এ, bank balance-এ, medical dose-এ চলে যেতে পারে, কেউ জানবেও না কোথা থেকে এল। এখান থেকেই software engineering-এর পুরনো শিক্ষা: fail fast — ভুল যত আগে ধরা পড়ে তত সস্তা। পুরো type-systems জগৎটা (তিন লেসন পরে) আসলে এই একটা লাইনের প্রকৌশলী রূপ: যত বেশি ভুল check-time-এ টানা যায়, তত কম নীরব বিপদ।

ছবিতে

                 ┌──────────────────┐
 সোর্স লেখা ──▶ │ Gate 1: PARSE    │ ──▶ AST ───────────────┐
 (অক্ষর)         │ lexer + parser   │                        │
                 └──────────────────┘                        ▼
                       ║                          ┌─────────────────────┐
                 SyntaxError                      │ Gate 2: CHECK       │ ──▶ বৈধ প্রোগ্রাম
                 (কোনো লাইন                       │ declarations/types  │     (চালানোর জন্য প্রস্তুত)
                  চলে না)                         └─────────────────────┘
                                                        ║
                                             CompileError / TypeError
                                             (ভাষাভেদে এখানে বা runtime-এ)
                                                        ║
                                                        ▼
                                          ┌───────────────────────────┐
                                          │ Gate 3: EVALUATE          │
                                          │ semantic rule মেনে চলা   │
                                          └───────────────────────────┘
                                                 ║             ║
                                        exception (চিৎকার)    নীরব ভুল উত্তর
                                                              (কোনো gate নেই!)
একটা সোর্স ফাইলের যাত্রাপথ — তিনটা gate, প্রতিটা এক ধরনের ভুল আটকায়। তৃতীয় gate পেরোনো ভুল আর কোথাও আটকায় না।

ডায়াগ্রামের সবচেয়ে গুরুত্বপূর্ণ অংশ ডানদিকের নিচে: নীরব ভুল উত্তরের কোনো gate নেই। Gate 1 আর Gate 2 যা ধরে তা কেবল লেখা-নিয়ম আর declaration-নিয়ম; “আপনি যা চেয়েছিলেন” — কোনো gate-এ সেই নিয়ম লেখা নেই, কারণ লেখার উপায়ই নেই। Assertion আর test হলো আপনার নিজের হাতে-বানানো চতুর্থ gate — ভাষা দেয় না, আপনাকেই বানাতে হয়।

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

EXPERIMENT

একই প্রোগ্রামে তিন ধরনের ব্যর্থতা — কখন কী ধরা পড়ে, exit code সহ

যেকোনো OS, Python 3.8+· ১৫ মিনিট

একটাই কাজ — গড় — তিন রকম ভাঙা কপি। আলাদা ফাইলে রাখুন, একে একে চালান, আর output-এর সাথে shell-এর exit code-ও দেখুন:

# v1_syntax.py — ভুল: colon নেই
def average(numbers):
    total = 0
    for n in numbers
        total += n
    return total / len(numbers)

print("side effect: এই লাইন কি চলবে?")
print(average([10, 20, 30, 41]))
# v2_type.py — ভুল: তালিকায় একটা string
def average(numbers):
    total = 0
    for n in numbers:
        total += n
    return total / len(numbers)

print("side effect: loop শুরুর আগে এই লাইন চলবে")
print(average([10, "20", 30, 41]))
# v3_silent.py — ভুল: // দিয়ে গড় করা
def average(numbers):
    total = 0
    for n in numbers:
        total += n
    return total // len(numbers)

print("side effect: এই লাইনও চলবে")
result = average([10, 20, 30, 41])
print("গড়:", result)
print("exit হচ্ছে — কোনো সমস্যাই নেই, তাই না?")
python3 v1_syntax.py; echo "exit=$?"
python3 v2_type.py;   echo "exit=$?"
python3 v3_silent.py; echo "exit=$?"

প্রত্যাশিত ফল:

v1: SyntaxError — "side effect" লাইনটা একবারও ছাপায়নি।     exit=1
v2: side effect ছাপা হয়েছে, তারপর TypeError।                exit=1
v3: side effect, "গড়: 25", "exit হচ্ছে..." — সব ছাপা।       exit=0

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

  1. v1-এ প্রথম print-ও চলেনি — parse পুরো ফাইলে হয়; execution শুরুর আগেই থেমে গেছে (hood section-এর bytecode কথাটা মনে করুন)।
  2. v2 dynamic checking-এর চোখের সামনের প্রদর্শনী: বিস্ফোরণ তখনই, যখন চলতে-চলতে ঠিক সেই +=-এ পৌঁছাল। একই ভুল Rust-এ ওই লাইনে পৌঁছানোর আগেই compile আটকে দিত।
  3. v3-এ exit code 0 — মেশিনের দৃষ্টিতে সফল প্রোগ্রাম। ভুলটা ধরার একমাত্র উপায় সঠিক উত্তর আগে থেকে জানা। assert result == 25.25 যোগ করুন — v3 এখন exit=1 দেবে। আপনি সম্প্রতি নিজের হাতে চতুর্থ gate বানালেন।
এটা কী প্রমাণ করে

Syntax error প্রথম লাইন চলার আগেই পুরো ফাইল আটকায়; টাইপ error চলতে-চলতে নির্দিষ্ট লাইনে ধরা পড়ে; আর semantic ভুল কোনোদিনই ধরা পড়ে না — exit code 0 সহ সফলভাবে শেষ হয়।

নিজে বানান

BUILD IT

Semantic Rule Swapper — একই AST, তিন সেট evaluation rule

Python · ●●○○○
  1. Tokenizer লিখুন — লেখাকে token-এ ভাগ করবে (সংখ্যা, identifier, operator)
  2. Recursive-descent parser লিখুন — token থেকে precedence মেনে AST
  3. AST node dataclass হিসেবে সাজান — Num, Var, BinOp
  4. evaluate() লিখুন যেটা rule-dictionary প্যারামিটার হিসেবে নেয়
  5. তিনটা rule-set define করুন — true division, floor division, C-style truncation
  6. একই expression, একই environment, তিন rule-set-এ চালিয়ে ফল তুলনা করুন

লক্ষ্য: syntax আর AST একবারও না ছুঁয়ে শুধু semantic rule বদলে একই প্রোগ্রামের উত্তর বদলে ফেলা — অর্থ আসে rule থেকে, লেখা থেকে নয়, হাতে-কলমে প্রমাণ।

import re
import math
from dataclasses import dataclass
from operator import add, sub, mul, truediv, floordiv

# ── ধাপ ১: tokenizer — লেখা → token (lexical structure) ──────
TOKEN_RE = re.compile(r"\d+(?:\.\d+)?|[A-Za-z_]\w*|\*\*|[+\-*/%()]")

def tokenize(text):
    return TOKEN_RE.findall(text)

# ── AST nodes ────────────────────────────────────────────────
@dataclass
class Num:
    value: float

@dataclass
class Var:
    name: str

@dataclass
class BinOp:
    op: str
    left: object
    right: object

# ── ধাপ ২: recursive-descent parser — token → AST ────────────
# Grammar (syntax!):
#   expr   := term (('+'|'-') term)*
#   term   := factor (('*'|'/'|'%') factor)*
#   factor := '-' factor | power
#   power  := atom ('**' factor)?
#   atom   := NUMBER | NAME | '(' expr ')'

class Parser:
    def __init__(self, tokens):
        self.toks = tokens
        self.i = 0

    def peek(self):
        return self.toks[self.i] if self.i < len(self.toks) else None

    def eat(self):
        tok = self.peek()
        self.i += 1
        return tok

    def expr(self):
        node = self.term()
        while self.peek() in ('+', '-'):
            node = BinOp(self.eat(), node, self.term())
        return node

    def term(self):
        node = self.factor()
        while self.peek() in ('*', '/', '%'):
            node = BinOp(self.eat(), node, self.factor())
        return node

    def factor(self):
        if self.peek() == '-':
            self.eat()
            return BinOp('-', Num(0), self.factor())   # unary minus = desugar
        return self.power()

    def power(self):
        node = self.atom()
        if self.peek() == '**':
            return BinOp(self.eat(), node, self.factor())  # right-assoc
        return node

    def atom(self):
        tok = self.eat()
        if tok == '(':
            node = self.expr()
            assert self.eat() == ')'
            return node
        if tok.replace('.', '', 1).isdigit():
            return Num(float(tok))
        return Var(tok)

def parse(text):
    p = Parser(tokenize(text))
    node = p.expr()
    assert p.peek() is None, f"অবশিষ্ট token: {p.peek()} — parse error"
    return node

# ── ধাপ ৩: evaluator — rule-dictionary প্যারামিটার নেয় ───────
def evaluate(node, env, rules):
    if isinstance(node, Num):
        return node.value
    if isinstance(node, Var):
        if node.name not in env:
            raise NameError(f"unbound variable: {node.name}")
        return env[node.name]
    if isinstance(node, BinOp):
        lv = evaluate(node.left, env, rules)    # লক্ষ্য: evaluation
        rv = evaluate(node.right, env, rules)   # order নিজেও একটা
        return rules[node.op](lv, rv)           # semantic সিদ্ধান্ত!
    raise TypeError(f"unknown node: {node!r}")

# ── ধাপ ৪: তিনটা semantic rule-set — syntax এক, অর্থ তিন ─────
RULES_TRUE_DIV = {'+': add, '-': sub, '*': mul, '/': truediv}

RULES_FLOOR = {'+': add, '-': sub, '*': mul,
               '/': floordiv,                          # নিচের পূর্ণসংখ্যায়
               '%': lambda a, b: a - floordiv(a, b) * b}

RULES_C_TRUNC = {'+': add, '-': sub, '*': mul,
                 '/': lambda a, b: float(math.trunc(a / b)),  # C: শূন্যর দিকে
                 '%': lambda a, b: a - math.trunc(a / b) * b}  # remainder-এর sign = dividend

# ── ধাপ ৫: একই লেখা, তিন ভাগ্য ───────────────────────────────
env = {'price': 100.0, 'qty': 3.0, 'total': 7.0}

for expr_text in ["price * qty / total", "-7 / 2"]:
    ast = parse(expr_text)          # ← একবারই parse — একই AST
    print(f"{expr_text:>22}")
    for name, rules in [("true-div ", RULES_TRUE_DIV),
                        ("floor-div", RULES_FLOOR),
                        ("C-trunc  ", RULES_C_TRUNC)]:
        print(f"    {name}: {evaluate(ast, env, rules)}")

প্রত্যাশিত output:

   price * qty / total
    true-div : 42.857142857142854
    floor-div: 42.0
    C-trunc  : 42.0
              -7 / 2
    true-div : -3.5
    floor-div: -4.0
    C-trunc  : -3.0

-7 / 2-এর তিনটা উত্তর দেখুন — -3.5, -4, -3। Tokenizer একবার চলেছে, parser একবার, AST-এর প্রতিটা node একবারই তৈরি — বদলেছে শুধু / চিহ্নের পেছনের function। আপনি নিজের হাতে দেখলেন: C আর Python একই লেখা পেয়ে ভিন্ন উত্তর দেয়, কারণ rule-book আলাদা — আর সেই rule-book-ই semantics।

নিজে বাড়ান:

  1. চতুর্থ rule-set যোগ করুন — saturating arithmetic (audio DSP-তে ব্যবহৃত): ফল 0–255-এর বাইরে গেলে সীমায় আটকায়। 200 + 100 কী দেয় দেখুন — overflow-ও একটা semantic সিদ্ধান্ত।
  2. evaluate-এ counter বসিয়ে মাপুন প্রতিটা rule-check কতবার চলে — dynamic checking-এর runtime খরচের প্রথম স্বাদ (লেসন ৩-এর মূল বিষয়)।
  3. চ্যালেঞ্জ: evaluate-কে right-to-left operand evaluation করান — pure expression-এ ফল বদলাবে না (কেন?), কিন্তু Var lookup-এ log বসালে ক্রমের পার্থক্য দেখা যাবে। Side effect থাকলে evaluation order নিজেই semantics — পরের লেসনের formal rules-এ এটা স্পষ্ট হবে।

বাস্তব সিস্টেমে

Syntax বনাম semantics যেখানে বাস্তবে দেখা যায়

Compiler error message-এর মানের যুদ্ধ। Rust-এর জনপ্রিয়তার একটা বড় কারণ তার error message — আর সেগুলো ভালো হওয়ার কারণ Rust team স্পষ্টভাবে আলাদা করে বোঝে কোন ভুল parse-time (গঠন নিয়ে কথা) আর কোনটা borrow-check-time (semantics-নিয়ম নিয়ে কথা)। ভালো tooling- এর প্রথম শর্তই স্তরগুলো চিনে কথা বলা।

Linters আর formatters শুধু syntax জগতের বাসিন্দা। Black, Prettier, gofmt — কোনোটাই প্রোগ্রামের অর্থ নিয়ে কাজ করে না; AST বানিয়ে, AST ঘুরিয়ে, AST থেকে আবার লেখা বানায়। এই কারণেই formatting বদলালে behavior বদলানোর কথা নয় — বদলালে formatter-এ bug, এবং template engine-এর HTML-এর মতো জায়গায় সেই bug CVE পর্যন্ত হয়েছে।

HTML parser-এর error recovery। Browser ভাঙা HTML reject করে না — WHATWG spec-এ exact recovery algorithm লেখা: <b><i></b></i> দেখলে ঠিক কোন tree বানাবে তা ধাপে ধাপে সংজ্ঞায়িত। Syntax-ভুলের জন্যও semantics-এর নিয়ম — web টিকিয়ে রাখার সবচেয়ে বড় pragmatic সিদ্ধান্তগুলোর একটা।

JSON vs YAML। JSON-এর syntax ছোট — duplicate key বা trailing comma প্রায় সব parser-এ একইভাবে আচরণ করে। YAML-এর syntax বিশাল, আর Norway problem (no → boolean false)-জাতীয় ঘটনা দেখায়: লেখার নিয়ম জটিল হলে একই লেখার অর্থ parserভেদে বদলায়। Infrastructure-as- code ভাঙার ক্লাসিক উৎস এখানেই।

TypeScript-এর পুরো business model। TypeScript JavaScript-এর syntax-এ annotation যোগ করে আর check-time স্তরটা runtime-এর আগে টেনে আনে — তারপর annotation মুছে খাঁটি JS ফেলে দেয়। এই লেসনের ভাষায় তার productivity-দাবি: “নীরব ভুলের বড় অংশকে check-time gate দিয়ে আটকাও।”

__pycache__ আর .pyc। প্রতিদিনের অভিজ্ঞতাতেই বিভাজন বাস করে — প্রথম import-এর পর দ্বিতীয়বার দ্রুত লোড, কারণ parse-এর খরচ cached bytecode-এ সাশ্রয় হয়ে গেছে।

যে ভুলগুলো সবাই করে

“Syntax error সবচেয়ে গুরুতর ধরনের ভুল।”

ঠিক উল্টো — তিন-মৃত্যু উদাহরণে দেখুন: syntax ভুল সবচেয়ে নিরীহ, কারণ সবচেয়ে আগে, সবচেয়ে সস্তায় ধরা পড়ে, কোনো ক্ষতি করতে পারে না। সবচেয়ে বিপজ্জনক নীরব semantic ভুল — exit code 0 সহ সুন্দরভাবে চলে, ভুল উত্তর দিয়ে যায়। Error-এর গুরুত্বের মাপকাঠি “কতটা রাগী দেখাচ্ছে” না, “কত দেরিতে ধরা পড়ল”।

“Semantics মানে documentation বা comments — ভাষা 'আসলে' শুধু syntax।”

Documentation মানুষের জন্য; semantics মেশিনের জন্য binding নিয়ম — কোন rule মানলে কী ঘটবে তার পূর্ণ বিবরণ, যার উপর ecosystem দাঁড়িয়ে। sorted() stable কি না, -7 // 2 কত, NaN != NaN কি না — এসব documentation-এর সৌজন্য না, semantic specification-এর অংশ; ভাঙলে লাখ লাখ প্রোগ্রাম ভাঙে। Python-এর language reference-এর অর্ধেকের বেশিই pure semantics — syntax-এর চেয়ে অনেক বড়।

“Compile-time বনাম runtime ধরা-পড়া ভাষার inherent বৈশিষ্ট্য — Python-এর ভুল সবসময় runtime-এই ধরা পড়বে।”

ধরা-পড়ার সময় ভাষার না, implementation-এর সিদ্ধান্ত — এবং বদলায়। mypy/pyright Python সোর্সেই টাইপ ভুল ধরে, run-এর আগে; Cython Python কোড C-তে compile করে; আবার C-এর জন্যও interpreter আছে (Ch, পুরনো CINT)। Static/dynamic বিভাজন আসলে “check টা কখন হয়” প্রশ্ন — toolchain বদলালে উত্তর বদলায়। এই সূক্ষ্মতাই লেসন ৩-এর static/dynamic axis-এর ভিত্তি।

“একই expression-এর ভিন্ন ভাষায় ভিন্ন মান হওয়া মানে কোনো একটা ভাষা 'ভুল'।”

1/2 কি 0.5 না 0 — এর absolute উত্তর নেই; আছে ভিন্ন design trade-off: C-এর integer division hardware-এ এক cycle, embedded জগতে অমূল্য; Python-এর true division surprise কমায়। Semantic rule হলো সিদ্ধান্তের সেট, সত্যের নয়। বাধ্যতামূলক শুধু একটা: নিয়মটা সুনির্দিষ্ট থাকা — আর সেই নিখুঁতকরণই পরের লেসনের বিষয়: অর্থকে গণিতের ভাষায় লেখা।

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

1

পাঁচটা Python অবস্থা — প্রতিটা কোন শ্রেণির ব্যর্থতা: lexical, grammatical, contextual/static, runtime evaluation, নাকি logical?

(a) x = 10 লেখার পর print(x1) (b) if True print(1) (c) total = total + "a" যেখানে total = 0 (d) [1, 2][5] (e) গড় বের করতে গিয়ে age // count ব্যবহার করা

স্মরণ

(a) Contextual — x1 কোথাও declared নয় (NameError চলার সময় আসে, তবে ভুলের প্রকৃতি declaration-context-এর)।

(b) Grammatical — token-রা সবই বৈধ, কিন্তু if statement-এর grammar colon চায়। SyntaxError।

(c) Runtime evaluation (Python-এ) — +-এর semantic rule টাইপ দেখে ব্যর্থ: TypeError। Static-checked ভাষায় একই ভুল check-time-এ ধরা পড়ত।

(d) Runtime evaluation — list-এ indexing বৈধ operation, মানটাই সীমার বাইরে: IndexError।

(e) Logical — কোনো স্তরই ধরবে না; ভুলটা intention বনাম rule-এর ফাঁকে। নীরব ভুল উত্তরের শ্রেণি।

2

x = x + 1: (a) কোন অংশটা syntax-নির্ধারিত? (b) গণিতের = আর imperative assignment-এর semantic পার্থক্য এক বাক্যে লিখুন। (c) Pure functional ভাষায় assignment-জাতীয় কিছু কি সম্ভব — কেন বা কেন না?

প্রয়োগ

(a) Token-ক্রম: NAME = NAME + NUMERAL — grammar rule assign := NAME '=' expr পূরণ; AST গঠনটাও syntax-নির্ধারিত।

(b) গণিতের = একটা সম্পর্ক (দুই মান সমান কি না — সত্য/মিথ্যা, সময়-নিরপেক্ষ); assignment একটা ক্রিয়া (environment-এ binding পালটানো — পুরনো অবস্থা ধ্বংস করে নতুন গড়ে)। সম্পর্ক ঘটানো যায় না, ক্রিয়া ঘটানো যায়।

(c) Mutable assignment নেই, তবে উদ্দেশ্য recursion দিয়ে হয়: loop x = loop (x + 1) — “নতুন x” পরের recursive কলের parameter; পুরনো binding কখনো পালটায় না, নতুন binding তৈরি হয়। Environment পালটানোর বদলে নতুন scope — semantics ভিন্ন, ফল অনুরূপ।

3

C-তে 1 + "1" compile হয়, Java-তে হয় না — দুই ভাষার কোন স্তরের কোন সিদ্ধান্তের ফল? প্রতিটা সিদ্ধান্তের সুবিধা-খরচ কী?

যুক্তি

সিদ্ধান্তটা contextual/static-check স্তরের: C-এর type system pointer+integer যোগকে বৈধ মানে (pointer arithmetic — array walk-এর মূল যন্ত্র), আর string literal decay করে pointer হয়। Java-র system জুটিটা reject করে — String আর int-এর + কেবল নির্দিষ্ট overload-এ বৈধ।

C-এর সুবিধা: system ছোট, compiler সরল, low-level offset-হিসাবে বাধাহীনতা। খরচ: ভুল লেখা নীরবে চলে যায় — buffer overflow পর্যন্ত। Java-র সুবিধা: বিপুল ভুল check-time-এ আটকায়। খরচ: system জটিল, কিছু বৈধ low-level কৌশল express করা কষ্টকর। কোনোটাই “ভুল” না — ভিন্ন domain-এর ভিন্ন trade-off।

4

Non-programmer-রাও যে config ভাষা লিখবে সেটা ডিজাইন করছেন। তিনটা gate-এর আলোকে বলুন: কোন ধরনের ভুল কোন gate-এ আটকাবেন? Syntax error-এ hard-fail করবেন নাকি browser-এর HTML-এর মতো best-effort recovery?

ডিজাইন

যুক্তিসঙ্গত ডিজাইন:

  • Lexical/grammatical: hard-fail, exact location সহ। Config ভুল পড়ে আধা-চালু সিস্টেম চালানোর চেয়ে না-চালু নিরাপদ (fail fast)। Non-programmer-দের জন্য message-টাই product: “line 12-এ : নেই” জাতীয় suggestion।

  • Contextual: config-এর প্রধান বিপদ — timeout = 30 বনাম timeout = "30", unknown key। Schema validation দিয়ে check-time-এ টানুন; কড়া মোডে fail, migration-মোডে warn।

  • Semantic/logical: ভাষা ধরতে পারে না, তবে সাহায্য করতে পারে — unit-সচেতন লেখা (timeout = 30ms), range validation, sane defaults।

Recovery-পক্ষের মূল যুক্তি browser-এর backward-compat; বিপক্ষে যুক্তি config-এ ভুল-চালু-অবস্থার খরচ ভাঙা layout-এর চেয়ে অনেক বেশি। সিদ্ধান্ত domain-নির্ভর, কিন্তু স্তরভিত্তিক ভাবনাটা সবখানে একই থাকে।

5

নিচের Python ফাইলে তিনটা আলাদা ভুল আছে, তিন স্তরে। চালালে সবচেয়ে আগে কোনটা ধরা পড়বে, কেন? সেটা ঠিক করে আবার চালালে তারপর কোনটা?

import statistics
print("start")
data = [10, 2o, 30]
mean = statistics.mean(dat)
print(mean)
প্রয়োগ

প্রথমে line 3-এর lexical ভুল: 2o — digit-এর পরে letter; কোনো token-ই গঠন হয় না, tokenizer আটকে যায় → SyntaxError (invalid literal)। যেহেতু parse পুরো ফাইলে হয়, line 2-এর print("start")-ও চলে না।

ঠিক করে চালালে দ্বিতীয়ে line 4-এর contextual ভুল: dat declared নয় (data লিখতে হত) — লেখার গঠন বৈধ, নাম lookup ব্যর্থ → NameError। লক্ষ্য করুন এটা তখনই আসবে যখন execution ওই লাইনে পৌঁছাবে; ততক্ষণে start ছাপা হয়ে গেছে।

(তৃতীয়টা কোথায়? 2o-কে 20 করলে আসলে দুইটা ভুলই মিলে যায়; যদি data = [10, "20", 30] হত, তাহলে statistics.mean-এ TypeError — runtime check স্তর।)

6

এই লেসনের build section-এ একই AST তিন rule-set-এ -7 / 2-কে -3.5, -4, -3 দিয়েছে। এই তিনটা ফল কি বোঝায় গণিতে division-এর “সঠিক উত্তর” তিনটা? না হলে ঠিক কী বোঝায়?

যুক্তি

না — গণিতে -7 ÷ 2 = -3.5 একটাই সত্য। তিনটা ফল বোঝায় তিনটা ভিন্ন specification একই syntactic form-কে ভিন্নভাবে interpret করেছে: true-div গণিতের ভাগকে যথাযথ বহন করে; floor-div “ভাগফল = নিচের পূর্ণসংখ্যা” নিয়ম বেছেছে (Python-এর //, modulo-র সাথে সামঞ্জস্যের জন্য); C-trunc “ভাগফল = শূন্যর দিকে কাটা” নিয়ম বেছেছে (hardware division instruction-এর natural আচরণ)। শিক্ষাটা: ভাষার semantic rule গণিতের সত্যের অনুবাদ না — গণিতের একটা বাছাই-করা অংশের উপর দাঁড়ানো সিদ্ধান্তের সেট। তাই দুই ভাষার ফল মেলাতে গিয়ে spec পড়তে হয়, intuition নয়।

এরপর কী

এরপর কী

আজ আমরা দেখলাম ভাষা দুইটা বই — লেখার বই আর অর্থের বই — নিয়ে দাঁড়িয়ে, আর ভুল তিন সময়ে তিন রকম ধরা পড়ে (বা পড়ে না)। কিন্তু একটা অস্বস্তি রয়ে গেল: আমরা semantics-এর কথা বললাম English prose-এ — “assignment মানে environment-এ binding পালটানো” — যেভাবে আইনজীবী আইন লেখেন। Prose-এর সমস্যা: দ্ব্যর্থতা। “একটা element বাদ দাও” — কোনটা, কোন নিয়মে, কোন edge case-এ?

পরের লেসনে — Formal Semantics — আমরা অর্থের বইটা গণিতের ভাষায় লিখব: big-step আর small-step operational semantics-এর inference rule, denotational semantics-এর mathematical function, আর Hoare logic-এর {P} C {Q} চুক্তি। আর দেখব কেন C-এর standard-এ “undefined behavior” নামের একটা ভয়ংকর ধারা আসলে formal semantics-এর absence-এর স্বাক্ষর।

আরও পড়ুন

  • Concepts of Programming Languages, Chapter 3 — Describing Syntax and Semantics — Robert W. Sebesta · Syntax আর semantics-এর ধ্রুপদি পাঠ্যবই-ব্যাখ্যা — BNF grammar থেকে operational semantics-এর পরিচয় পর্যন্ত
  • Types and Programming Languages, Chapter 1 — Introduction — Benjamin C. Pierce · "Syntax বনাম semantics" বিভাজনটা যে টাইপ সিস্টেমেরও ভিত্তি — তার সংক্ষিপ্ত, নিখুঁত আলোচনা
  • Fundamental Concepts in Programming Languages — Christopher Strachey · 1967-এর এই বক্তৃতাতেই modern PL vocabulary-র বেশিরভাগ শব্দ — expression/statement/command-এর অর্থ-ভিত্তিক ভাগসহ — প্রথম সাজানো হয়