চূড়ান্ত প্রকল্প — একটি মিনি ইন্টারপ্রেটার বানানো
এই পাঠে যা শিখবেন
- আগের মডিউলগুলোর টুকরো টুকরো ধারণা কীভাবে একটি একক, সুসংগত পাইপলাইনে জোড়া লাগে
- একটি সম্পূর্ণ, কার্যকরী লেক্সার-পার্সার-সিম্বল-টেবিল-ইভালুয়েটর পাইপলাইন বাস্তবায়ন
- একটি বাস্তব ছোট প্রোগ্রাম টোকেন থেকে চূড়ান্ত আউটপুট পর্যন্ত সম্পূর্ণভাবে ট্রেস করা
- কেন একটি ইন্টারপ্রেটারের সিম্বল টেবিল প্রয়োজন, এমনকি আলাদা কোনো স্ট্যাটিক টাইপ-চেকিং ছাড়াই
১ · এই কোর্সের সবকিছু, একসাথে
এই পাঠ নতুন কোনো তত্ত্ব শেখায় না — এটি একটি সংশ্লেষণ। আমরা একটি MiniInterpreter
বানাব যা M4/L20-এর লেক্সার, M5/L22-এর পার্সার, M6/L28-L29-এর সিম্বল টেবিল, এবং M6/L30 ও M9/L41-এর
ইভালুয়েশন ধারণা — সবগুলো একটি একক, কার্যকরী পাইপলাইনে জোড়া দেয়। ভাষাটি ছোট কিন্তু বাস্তব — সংখ্যা, আইডেন্টিফায়ার,
+ - * /, বন্ধনী, অ্যাসাইনমেন্ট (=), এবং print সমর্থন করে।
২ · লেক্সার — M4/L20-এর স্টাইল পুনর্ব্যবহার
একটি হাতে-লেখা, চরিত্র-বাই-চরিত্র লেক্সার — সংখ্যা, আইডেন্টিফায়ার/কীওয়ার্ড (print), অপারেটর
(+ - * /), বন্ধনী, =, এবং নিউলাইন (স্টেটমেন্ট বিভাজক হিসেবে) চেনে। M4/L16-এর
"ম্যাক্সিমাল মাঞ্চ" নীতি অনুযায়ী একাধিক সংখ্যা/অক্ষর একসাথে একটি টোকেনে জমা হয়।
৩ · পার্সার — M5/L22-এর গ্রামার পুনর্ব্যবহার
M5/L22-এর বাম-রিকার্শন-মুক্ত এক্সপ্রেশন গ্রামার (expr, term, factor)
হুবহু পুনর্ব্যবহার করা হয়েছে, উপরে একটি সরল স্টেটমেন্ট-লেভেল গ্রামার যোগ করে —
statement ::= IDENT '=' expr NEWLINE | 'print' '(' expr ')' NEWLINEexpr ::= term expr_tail,expr_tail ::= ('+'|'-') term expr_tail | εterm ::= factor term_tail,term_tail ::= ('*'|'/') factor term_tail | εfactor ::= NUMBER | IDENT | '(' expr ')'
৪ · সিম্বল টেবিল ও ইভালুয়েটর — M6/L28-L30 পুনর্ব্যবহার
একটি SymbolTable ক্লাস স্কোপের একটি স্ট্যাক রাখে (M6/L29-এর স্কোপ রেজোলিউশন ধারণা) —
lookup সবচেয়ে ভেতরের স্কোপ থেকে বাইরের দিকে খুঁজে, না পেলে একটি স্পষ্ট এরর তোলে। ইভালুয়েটর
AST-কে বটম-আপ ট্রি-ওয়াক করে (M6/L30) — প্রতিটি এক্সপ্রেশন নোড রিকার্সিভভাবে মূল্যায়িত হয়ে একটি সংখ্যায়
পরিণত হয়, প্রতিটি স্টেটমেন্ট নোড হয় সিম্বল টেবিল আপডেট করে (অ্যাসাইনমেন্ট) নয়তো real আউটপুট তৈরি করে
(print)।
লেক্সার টেক্সটকে টোকেনে ভাঙে (M4) → পার্সার টোকেনকে একটি গঠনবদ্ধ AST-তে সাজায় (M5) → সিম্বল টেবিল নামের
অর্থ ট্র্যাক করে (M6) → ইভালুয়েটর সেই গঠনকে প্রকৃত মান ও পার্শ্ব-প্রতিক্রিয়ায় (side effect, এখানে
print) রূপান্তর করে (M6/M9) — এটাই ঠিক L01-এর ভূমিকায় দেখানো পাইপলাইন, এখন সম্পূর্ণভাবে বাস্তবায়িত।
৫ · বাস্তবায়ন — সম্পূর্ণ পাইপলাইন, একটি বাস্তব প্রোগ্রামে
নিচের কোড সেলে সম্পূর্ণ MiniInterpreter বাস্তবায়ন করা হয়েছে, এবং একটি ছোট প্রোগ্রাম চালানো
হয়েছে — x = 3 + 4, y = x * 2, তারপর print(y) ও print(x + y)।
টোকেন স্ট্রিম, সম্পূর্ণ AST, এবং প্রকৃত এক্সিকিউটেড আউটপুট — সব ধাপ আলাদাভাবে দেখানো হয়েছে।
# ---------- ১) লেক্সার (M4/L20 স্টাইল) ----------
class Token:
def __init__(self, type_, value):
self.type = type_
self.value = value
def __repr__(self):
return f"Token({self.type},{self.value!r})"
KEYWORDS = {"print"}
def tokenize(source):
tokens = []
i, n = 0, len(source)
while i < n:
ch = source[i]
if ch in " \t":
i += 1; continue
if ch == "\n":
tokens.append(Token("NEWLINE", "\\n")); i += 1; continue
if ch.isdigit():
start = i
while i < n and source[i].isdigit():
i += 1
tokens.append(Token("NUMBER", int(source[start:i]))); continue
if ch.isalpha() or ch == "_":
start = i
while i < n and (source[i].isalnum() or source[i] == "_"):
i += 1
word = source[start:i]
tokens.append(Token("PRINT", word) if word in KEYWORDS else Token("IDENT", word))
continue
simple = {"+": "PLUS", "-": "MINUS", "*": "STAR", "/": "SLASH",
"(": "LPAREN", ")": "RPAREN", "=": "ASSIGN"}
if ch in simple:
tokens.append(Token(simple[ch], ch)); i += 1; continue
raise ValueError(f"লেক্সিক্যাল এরর: অজানা ক্যারেক্টার {ch!r}, পজিশন {i}")
tokens.append(Token("EOF", None))
return tokens
# ---------- ২) স্কোপযুক্ত সিম্বল টেবিল (M6/L28-L29 স্টাইল) ----------
class SymbolTable:
def __init__(self):
self.scopes = [{}] # স্কোপের স্ট্যাক -- এখানে শুধু গ্লোবাল স্কোপ ব্যবহৃত হচ্ছে
def assign(self, name, value):
for scope in reversed(self.scopes):
if name in scope:
scope[name] = value
return
self.scopes[-1][name] = value # নতুন ভ্যারিয়েবল -- বর্তমান স্কোপে ঘোষিত হয়
def lookup(self, name):
for scope in reversed(self.scopes):
if name in scope:
return scope[name]
raise NameError(f"অঘোষিত ভ্যারিয়েবল: {name!r}")
# ---------- ৩) রিকার্সিভ ডিসেন্ট পার্সার (M5/L22 গ্রামার) ----------
class ParseError(Exception):
pass
class Parser:
def __init__(self, tokens):
self.tokens = tokens
self.pos = 0
def peek(self):
return self.tokens[self.pos]
def advance(self):
tok = self.tokens[self.pos]; self.pos += 1; return tok
def expect(self, type_):
tok = self.peek()
if tok.type != type_:
raise ParseError(f"{type_} প্রত্যাশিত, পাওয়া গেছে {tok.type}")
return self.advance()
def skip_newlines(self):
while self.peek().type == "NEWLINE":
self.advance()
def parse_program(self):
statements = []
self.skip_newlines()
while self.peek().type != "EOF":
statements.append(self.parse_statement())
self.skip_newlines()
return ("program", statements)
def parse_statement(self):
tok = self.peek()
if tok.type == "IDENT":
name = self.advance().value
self.expect("ASSIGN")
expr = self.parse_expr()
if self.peek().type == "NEWLINE": self.advance()
return ("assign", name, expr)
if tok.type == "PRINT":
self.advance(); self.expect("LPAREN")
expr = self.parse_expr()
self.expect("RPAREN")
if self.peek().type == "NEWLINE": self.advance()
return ("print", expr)
raise ParseError(f"স্টেটমেন্টের শুরুতে অপ্রত্যাশিত টোকেন: {tok.type}")
def parse_expr(self):
node = self.parse_term()
while self.peek().type in ("PLUS", "MINUS"):
op = self.advance().type
rhs = self.parse_term()
node = ("binop", "+" if op == "PLUS" else "-", node, rhs)
return node
def parse_term(self):
node = self.parse_factor()
while self.peek().type in ("STAR", "SLASH"):
op = self.advance().type
rhs = self.parse_factor()
node = ("binop", "*" if op == "STAR" else "/", node, rhs)
return node
def parse_factor(self):
tok = self.peek()
if tok.type == "NUMBER":
self.advance(); return ("num", tok.value)
if tok.type == "IDENT":
self.advance(); return ("var", tok.value)
if tok.type == "LPAREN":
self.advance()
node = self.parse_expr()
self.expect("RPAREN")
return node
raise ParseError(f"factor-এ অপ্রত্যাশিত টোকেন: {tok.type}")
# ---------- ৪) ট্রি-ওয়াকিং ইভালুয়েটর (M6/L30, M9/L41 স্টাইল) ----------
class MiniInterpreter:
def __init__(self):
self.symtab = SymbolTable()
self.output = []
def run(self, source):
tokens = tokenize(source)
ast = Parser(tokens).parse_program()
self.exec_node(ast)
return tokens, ast, self.output
def exec_node(self, node):
kind = node[0]
if kind == "program":
for stmt in node[1]:
self.exec_node(stmt)
elif kind == "assign":
_, name, expr = node
self.symtab.assign(name, self.eval_expr(expr))
elif kind == "print":
_, expr = node
value = self.eval_expr(expr)
self.output.append(value)
print(f" >>> print আউটপুট: {value}")
else:
raise ValueError("অজানা স্টেটমেন্ট নোড: " + kind)
def eval_expr(self, node):
kind = node[0]
if kind == "num":
return node[1]
if kind == "var":
return self.symtab.lookup(node[1]) # M6/L28-এর lookup -- নাম-রেজোলিউশন
if kind == "binop":
_, op, l, r = node
lv, rv = self.eval_expr(l), self.eval_expr(r)
if op == "+": return lv + rv
if op == "-": return lv - rv
if op == "*": return lv * rv
if op == "/": return lv // rv
raise ValueError("অজানা এক্সপ্রেশন নোড: " + kind)
# ---------- সম্পূর্ণ পাইপলাইন চালানো ----------
source = "x = 3 + 4\ny = x * 2\nprint(y)\nprint(x + y)\n"
print("সোর্স প্রোগ্রাম:")
print(source)
interp = MiniInterpreter()
tokens, ast, output = interp.run(source)
print("== ধাপ ১: টোকেন স্ট্রিম (M4) ==")
for t in tokens:
print(" ", t)
print("\n== ধাপ ২: AST (M5) ==")
print(" ", ast)
print("\n== ধাপ ৩: এক্সিকিউশন (উপরে print আউটপুট ইতিমধ্যে দেখানো হয়েছে) ==")
print("সংগ্রহ করা print আউটপুট:", output)
print("\n== চূড়ান্ত যাচাই ==")
print("x =", interp.symtab.lookup("x"), " y =", interp.symtab.lookup("y"))
assert interp.symtab.lookup("x") == 7
assert interp.symtab.lookup("y") == 14
assert output == [14, 21]
print("PASS -- x=7, y=14, print(y)->14, print(x+y)->21 -- সবকিছু সঠিক")
print("\n== অঘোষিত ভ্যারিয়েবলের এরর হ্যান্ডলিং (M5/L27, M6/L28 স্টাইল) ==")
try:
MiniInterpreter().run("print(z)\n")
except NameError as e:
print("সঠিকভাবে ধরা পড়েছে:", e)
tokenize সত্যিই সোর্স টেক্সট থেকে টোকেন তৈরি
করেছে, Parser সত্যিই সেই টোকেন থেকে একটি AST বানিয়েছে (M5/L22-এর গ্রামারের সাথে হুবহু মিলিয়ে
যাচাইযোগ্য), এবং MiniInterpreter সত্যিই সেই AST ট্রি-ওয়াক করে সিম্বল টেবিল আপডেট করেছে ও
print-এর real আউটপুট তৈরি করেছে। x = 3 + 4 = 7, y = 7 * 2 = 14,
print(y) = 14, print(x + y) = print(7 + 14) = 21 — প্রতিটি ধাপ কোড নিজে গণনা
করেছে, আমরা শুধু হাতে-যাচাই করেছি ফলাফল প্রত্যাশিত মানের সাথে মেলে।
একটি ইন্টারপ্রেটার — যত ছোটই হোক না কেন — এই কোর্সের প্রতিটি মডিউলের একটি বাস্তব প্রয়োগ। লেক্সিং, পার্সিং, সিম্বল টেবিল, ইভালুয়েশন — কোনোটিই বিচ্ছিন্ন তত্ত্ব নয়, প্রতিটি একটি বড় যন্ত্রের অপরিহার্য একটি অংশ। আপনি যদি এই পাঠ পর্যন্ত পৌঁছে থাকেন — অভিনন্দন, আপনি এখন জানেন সোর্স কোডের একটি লাইন কীভাবে ধাপে ধাপে একটি চলমান প্রোগ্রামে পরিণত হয়, শুরু থেকে শেষ পর্যন্ত।
ভাবনার প্রশ্ন
প্রতিটি প্রশ্ন নিজে কিছুক্ষণ ভাবুন — তারপর "→ উত্তর" চাপুন।
প্র ০১
এই ইন্টারপ্রেটারের কোনো আলাদা স্ট্যাটিক টাইপ-চেকিং ধাপ নেই (M7-এর অর্থে), তবুও এটি একটি সিম্বল টেবিল (M6/L28) ব্যবহার করে কেন? print(z)-এ z অঘোষিত হলে কী ঘটবে?
সিম্বল টেবিল দরকার নাম রেজোলিউশনের জন্য — একটি ভ্যারিয়েবল নামকে তার প্রকৃত মানের সাথে
যুক্ত করা — এটি টাইপ-চেকিং থেকে সম্পূর্ণ আলাদা প্রশ্ন। যখন eval_expr একটি ("var",
"z") নোড মূল্যায়ন করে, তখন symtab.lookup("z") কল হয়; যদি "z" কখনো
কোনো স্কোপে অ্যাসাইন করা না হয়ে থাকে, lookup সঠিকভাবে NameError তোলে — উপরের
কোড সেলের শেষে ঠিক এই আচরণটাই দেখানো হয়েছে। এটি প্রমাণ করে সিম্বল টেবিল টাইপ-চেকিং না থাকলেও প্রয়োজনীয়
— যেকোনো ভাষায়, নাম রেজোলিউশন একটি মৌলিক, পৃথক প্রয়োজন।
প্র ০২
যদি সোর্স প্রোগ্রামে y = x * 2-এর আগে x = 3 + 4 না থাকত (ক্রম উল্টে দেওয়া হতো), তাহলে কী হতো?
y = x * 2 এক্সিকিউট হওয়ার সময় eval_expr(("var", "x")) কল হতো, যা
symtab.lookup("x") কল করত — কিন্তু x তখনো কোনো স্কোপে অ্যাসাইন করা হয়নি, তাই
NameError: অঘোষিত ভ্যারিয়েবল: 'x' উঠত। এটি দেখায় এই ইন্টারপ্রেটার সোর্স কোডের
ক্রম অনুযায়ী এক্সিকিউট করে (M9/L41-এর evaluation order ধারণা) — একটি ভ্যারিয়েবল
ব্যবহারের আগেই তার অ্যাসাইনমেন্ট এক্সিকিউট হয়ে যেতে হবে।
প্র ০৩ এই ইন্টারপ্রেটারের পার্সার M5/L22-এর গ্রামার হুবহু পুনর্ব্যবহার করেছে। যদি মূল গ্রামারে বাম-রিকার্শন এলিমিনেট করা না থাকত, তাহলে এই ক্যাপস্টোনে কী সমস্যা হতো?
M5/L22-এ দেখানো হয়েছিল একটি বাম-রিকার্সিভ গ্রামার (expr ::= expr + term | term) দিয়ে সরাসরি
রিকার্সিভ ডিসেন্ট পার্সার লিখলে parse_expr নিজেকে কোনো ইনপুট consume না করেই আবার কল করত —
অসীম রিকার্শন, একটি প্রকৃত স্ট্যাক ওভারফ্লো (M11/L50-এর "stack overflow" ধারণার সরাসরি বাস্তব প্রকাশ)।
এই ক্যাপস্টোনের parse_expr/parse_term ফাংশন তাই ডান-রিকার্সিভ/ইটারেটিভ রূপ
(while লুপ, M5/L22-এর expr' রূপান্তরেরই একটি বাস্তব প্রয়োগ) ব্যবহার করে, যা
নিরাপদে টার্মিনেট করে।
অনুশীলন
-
চিন্তা করুন: উপরের কোড সেলের
sourceভ্যারিয়েবলে যদি একটি নতুন লাইনz = (x + y) / 3যোগ করা হয়, z-এর চূড়ান্ত মান কত হবে বলে আপনি হাতে গণনা করেন?x=7, y=14 হওয়ায়
z = (7 + 14) / 3 = 21 / 3 = 7(পূর্ণসংখ্যা ভাগ, যেহেতু ইভালুয়েটরে/অপারেটর//ব্যবহার করে বাস্তবায়িত)। বন্ধনী থাকায়parse_factorপ্রথমে ভেতরেরx + yএক্সপ্রেশন সম্পূর্ণ মূল্যায়ন করবে (M5/L22-এর গ্রামারের'(' expr ')'নিয়ম অনুযায়ী), তারপর ৩ দিয়ে ভাগ হবে। -
পরীক্ষা করুন: উপরের কোড সেলে
source-এর শেষেprint(x - y)যোগ করে Run চেপে দেখুন — নতুন AST ও আউটপুটে কী পরিবর্তন হয়?টোকেন স্ট্রিমে
PRINT, LPAREN, IDENT('x'), MINUS, IDENT('y'), RPAREN, NEWLINEনতুন যুক্ত হবে। AST-তেprogram-এর statement তালিকায় একটি নতুন("print", ("binop", "-", ("var","x"), ("var","y")))নোড যুক্ত হবে। এক্সিকিউশনে নতুন একটিprint আউটপুট: -7দেখা যাবে (x - y = 7 - 14 = -7), এবংoutputতালিকা হবে[14, 21, -7]।
আরও পড়ুন · ABCL TECH-এ আপনার পরবর্তী পদক্ষেপ
- কোর্সের সম্পূর্ণ সিলেবাস আবার দেখুন ৫৮টি পাঠ শুরু থেকে যেকোনো পাঠ আবার দেখতে চাইলে — সম্পূর্ণ ৫৮-পাঠের সিলেবাস এখানে।
- Computer Architecture & Digital Logic কোর্স সহোদর কোর্স এই কোর্স সোর্স কোডকে মেশিন কোডে রূপান্তর করা শেখায়; সেই কোর্স সেই মেশিন কোড আসলে কীভাবে হার্ডওয়্যারে চলে তা শেখায় — M12-এর কোড জেনারেশন ঠিক সেই সেতুবন্ধন।
- Data Structures & Algorithms কোর্স সঙ্গী কোর্স এই ক্যাপস্টোনের পার্সার ও সিম্বল টেবিল সরাসরি stack ও tree ডেটা স্ট্রাকচার ব্যবহার করেছে — এই কোর্সে তাদের ভিত্তি শেখানো হয়েছে।