সাবপ্রোগ্রাম, অ্যাক্টিভেশন রেকর্ড ও কল স্ট্যাক
এই পাঠে যা শিখবেন
- সাবপ্রোগ্রাম ও অ্যাক্টিভেশন রেকর্ড কী ধরে রাখে
- কল স্ট্যাক কীভাবে push (কল) ও pop (রিটার্ন) ডিসিপ্লিনে চলে
- রিকার্শন কীভাবে এই একই মডেল থেকে স্বাভাবিকভাবে বেরিয়ে আসে
- একটি বাস্তব
CallStackক্লাস দিয়েmain()->f()->g()কল-চেইন সিমুলেট ও ট্রেস করা
১ · সাবপ্রোগ্রাম কী
সাবপ্রোগ্রামSubprogramফাংশন/প্রসিডিউর/মেথড — একটি নাম-দেওয়া, একাধিকবার INVOKE করা যায় এমন কোড ব্লক। (ফাংশন/প্রসিডিউর/মেথড — একটি সাধারণ ছাতা-টার্ম) হলো একটি নাম-দেওয়া, পুনঃব্যবহারযোগ্য কোড ব্লক যা একাধিক জায়গা থেকে কল/ইনভোক করা যায়। এই কোর্সে এই ধারণা ইতিমধ্যে অনানুষ্ঠানিকভাবে বহুবার ব্যবহৃত হয়েছে — M2/L06-এর ইম্পারেটিভ প্রসিডিউর থেকে শুরু করে M5/L22-এর পার্সার ফাংশন পর্যন্ত।
২ · অ্যাক্টিভেশন রেকর্ড — একটি কলের সব তথ্য
অ্যাক্টিভেশন রেকর্ডActivation Record (Stack Frame)একটি সাবপ্রোগ্রাম প্রতিবার কল হলে অ্যালোকেট হওয়া একটি মেমরি ব্লক — সেই নির্দিষ্ট ইনভোকেশনের জন্য প্রয়োজনীয় সবকিছু ধরে রাখে। (স্ট্যাক ফ্রেম) হলো একটি সাবপ্রোগ্রাম প্রতিবার কল হলে অ্যালোকেট হওয়া একটি মেমরি ব্লক — সেই নির্দিষ্ট ইনভোকেশনের জন্য দরকারি সবকিছু ধরে রাখে:
কল শেষ হলে ক্যালারের ঠিক কোথায় ফিরে যেতে হবে।
এই নির্দিষ্ট কলে পাস করা আর্গুমেন্টের মান/রেফারেন্স।
L39-এর stack-dynamic অ্যালোকেশনের বাস্তবায়ন।
চেইন ধরে হাঁটা যায় — L43-এর স্ট্যাক-আনওয়াইন্ডিং-এ পুনর্ব্যবহৃত।
৩ · কল স্ট্যাক — push/pop ডিসিপ্লিন
কল স্ট্যাকCall Stackবর্তমানে সক্রিয় সব অ্যাক্টিভেশন রেকর্ডের একটি স্ট্যাক (LIFO)। হলো বর্তমানে-সক্রিয় সব অ্যাক্টিভেশন রেকর্ডের একটি স্ট্যাক — একটি CALL একটি নতুন অ্যাক্টিভেশন রেকর্ড push করে; একটি RETURN সেটি pop করে, ক্যালারের কনটেক্সট ফিরিয়ে দেয়। এটাই L39-এর "stack-dynamic" স্টোরেজ ক্যাটাগরির সুনির্দিষ্ট বাস্তবায়ন — ঠিক এই push-on-call/pop-on-return ডিসিপ্লিন দিয়েই।
৪ · রিকার্শনের সাথে সম্পর্ক
অ্যাক্টিভেশন-রেকর্ড দৃষ্টিকোণ থেকে দেখলে, একটি রিকার্সিভ কল মোটেও বিশেষ কিছু নয় — এটি স্রেফ আরেকটি সাধারণ কল, যা স্ট্যাকে আরেকটি নতুন অ্যাক্টিভেশন রেকর্ড push করে। ফাংশনটি টেক্সট হিসেবে "একই" হলেও, প্রতিটি কল সম্পূর্ণ আলাদা, নিজস্ব লোকাল ভ্যারিয়েবলসহ একটি আলাদা অ্যাক্টিভেশন রেকর্ড পায় — এজন্যই রিকার্শন একে অপরের লোকাল স্টেটে হস্তক্ষেপ না করেই সঠিকভাবে কাজ করে (M11/L50-এ এই মডেল দিয়ে রিকার্শনের গভীরতা সরাসরি ট্র্যাক করা হবে)।
৫ · কোড: main() → f() → g() কল-চেইন সিমুলেশন
নিচের কোড সেলে একটি বাস্তব CallStack ক্লাস দিয়ে একটি ছোট কল-চেইন সিমুলেট করা হয়েছে —
প্রতিটি push/pop-এর পরে সম্পূর্ণ স্ট্যাক প্রিন্ট করে বৃদ্ধি ও সংকোচন দুটোই দেখানো হয়েছে।
class CallStack:
def __init__(self):
self.frames = []
self._next_return_address = 100 # সিমুলেটেড ইনস্ট্রাকশন-ঠিকানা, শুধু ট্রেসিংয়ের জন্য
def call(self, function_name, params=None, locals_=None):
frame = {
"function_name": function_name,
"return_address": self._next_return_address,
"params": dict(params or {}),
"locals": dict(locals_ or {}),
}
self.frames.append(frame)
self._next_return_address += 10
return frame
def return_from_call(self):
if not self.frames:
raise RuntimeError("খালি call stack থেকে return করা যায় না")
return self.frames.pop()
def print_stack(self, label=""):
print(f"--- কল স্ট্যাক [{label}] (গভীরতা={len(self.frames)}) ---")
if not self.frames:
print(" (খালি)")
for depth, frame in enumerate(reversed(self.frames), start=1):
print(f" #{depth} {frame['function_name']} params={frame['params']} "
f"locals={frame['locals']} return_addr={frame['return_address']}")
stack = CallStack()
stack.call("main", params={}, locals_={"x": 5})
stack.print_stack("main() কল হওয়ার পর")
stack.call("f", params={"n": 5}, locals_={})
stack.print_stack("f() কল হওয়ার পর")
stack.call("g", params={"n": 4}, locals_={})
stack.print_stack("g() কল হওয়ার পর -- সর্বোচ্চ গভীরতা")
assert len(stack.frames) == 3
popped = stack.return_from_call()
print(f"\ng() থেকে return -- pop হলো: {popped['function_name']}")
stack.print_stack("g() থেকে return-এর পর")
popped = stack.return_from_call()
print(f"\nf() থেকে return -- pop হলো: {popped['function_name']}")
stack.print_stack("f() থেকে return-এর পর")
popped = stack.return_from_call()
print(f"\nmain() থেকে return -- pop হলো: {popped['function_name']}")
stack.print_stack("main() থেকে return-এর পর")
assert len(stack.frames) == 0
print("\nস্ট্যাক সম্পূর্ণ খালি -- growth ও shrink দুটোই সঠিকভাবে সম্পন্ন হয়েছে।")
return_from_call() সবসময় স্ট্যাকের একদম শেষে যোগ হওয়া ফ্রেমটিই
pop করে (Python-এর list.pop() ডিফল্টভাবে শেষ এলিমেন্ট সরায়) — এটাই কল স্ট্যাকের
LIFO (Last-In, First-Out) আচরণ। g() সবার শেষে push হয়েছে বলে সবার আগে pop
হয়, ঠিক যেমন আসলেই ঘটে: সবচেয়ে সাম্প্রতিক কলটিই সবার আগে রিটার্ন করে।
একটি সাবপ্রোগ্রাম কল মানে "একটি অ্যাক্টিভেশন রেকর্ড push করা"; একটি রিটার্ন মানে "সবচেয়ে উপরের অ্যাক্টিভেশন রেকর্ডটি pop করা"। এই সরল, নিয়মবদ্ধ push/pop ডিসিপ্লিনই — কোনো জাদু ছাড়াই — নেস্টেড কল, রিটার্ন-অ্যাড্রেস ট্র্যাকিং, এবং (M11/L50-এ দেখা যাবে) রিকার্শনকে সঠিকভাবে কাজ করায়।
ভাবনার প্রশ্ন
প্রতিটি প্রশ্ন নিজে কিছুক্ষণ ভাবুন — তারপর "→ উত্তর" চাপুন।
প্র ০১
উপরের সিমুলেশনে g() কল হওয়ার পর স্ট্যাকের গভীরতা কত ছিল, এবং কেন g() সবার আগে return করলো?
গভীরতা ছিল 3 (main, f, g — তিনটি সক্রিয় ফ্রেম)। g()
সবার আগে return করলো কারণ কল স্ট্যাক LIFO — সবচেয়ে সাম্প্রতিক push (g()) সবার উপরে থাকে,
এবং return_from_call() সবসময় সবচেয়ে-উপরেরটিই pop করে। main() ততক্ষণ pop
হবে না যতক্ষণ না তার উপরের সব ফ্রেম (এখানে f ও g) আগে pop হয়ে গেছে।
প্র ০২
যদি g() কল হওয়ার আগেই ভুলবশত return_from_call() চারবার কল করা হতো, তাহলে কী হতো?
প্রথম দুইবার সঠিকভাবে f ও main pop হয়ে যেত (যেহেতু তখন push হওয়া ছিল
main ও f)। তৃতীয়বার কল করার সময় স্ট্যাক ইতিমধ্যে খালি — কোডে দেখুন,
return_from_call() স্পষ্টভাবে if not self.frames: raise RuntimeError(...)
চেক করে এই অবস্থা ধরে ফেলে, চুপচাপ ভুল আচরণ (যেমন একটি খালি লিস্ট থেকে pop করে ক্র্যাশ করা বা None
রিটার্ন করা) না করে স্পষ্ট এরর দেয়।
প্র ০৩ অ্যাক্টিভেশন রেকর্ডে "ক্যালারের রেকর্ডের রেফারেন্স" রাখা কেন দরকারি, যদি কল স্ট্যাক নিজেই ইতিমধ্যে একটি ক্রম বজায় রাখে?
কল স্ট্যাক (এই লেসনের self.frames লিস্টের মতো) সাধারণত একটি ইমপ্লিমেন্টেশন ডিটেইল —
বাস্তব সিস্টেমে সবসময় এভাবে একটি সরল লিস্টে দেখা যায় না। প্রতিটি ফ্রেমে সরাসরি তার ক্যালারের ফ্রেমের
রেফারেন্স রাখলে (একটি লিংকড-লিস্ট-এর মতো চেইন), M9/L44-এর এক্সেপশন হ্যান্ডলিং-এর মতো কোনো মেকানিজম
দ্রুত "উপরের দিকে হেঁটে" ক্যালার-চেইন ধরে খুঁজতে পারে (যেমন: কোন ফ্রেমে try/except আছে) — কল স্ট্যাকের
গ্লোবাল গঠন না জেনেই, শুধু স্থানীয় "আমার ক্যালার কে" তথ্য দিয়ে।
অনুশীলন
-
পরীক্ষা করুন: উপরের কোডে
g()-এর পরে আরেকটি ধাপ যোগ করুন —stack.call("h", params={"n": 3}, locals_={})— এবংprint_stackচালিয়ে দেখুন গভীরতা কত হয়।গভীরতা হবে 4 (
main, f, g, h)।print_stack-এর আউটপুটেhসবার উপরে (#1 হিসেবে) দেখাবে, কারণ এটি সবার শেষে push হয়েছে — এবং এখন এটিকেই সবার আগেreturn_from_call()দিয়ে pop করতে হবে। -
চিন্তা করুন:
stack.call("f", ...)-কে একটি লুপে ৫ বার কল করলে (প্রতিবার আগেরটি return না করেই), স্ট্যাকে কয়টি আলাদাfফ্রেম থাকবে — এবং এরা কি একে অপরেরlocalsশেয়ার করবে?৫টি সম্পূর্ণ আলাদা অ্যাক্টিভেশন রেকর্ড push হবে — প্রতিটির নিজস্ব, স্বাধীন
localsডিকশনারি থাকবে (কোডে প্রতিবারdict(locals_ or {})দিয়ে একটি নতুন dict তৈরি হয়)। এরা কখনো locals শেয়ার করবে না — এটাই ঠিক সেই নীতি যা এই লেসনের §৪-এ বলা হয়েছে এবং যা রিকার্শনকে (M11/L50) সঠিকভাবে কাজ করায়।
আরও পড়ুন · ABCL TECH-এ আপনার পরবর্তী পদক্ষেপ
- কোর্সের সম্পূর্ণ সিলেবাস দেখুন ৫৮টি পাঠ পরের পাঠে — প্যারামিটার আসলে কীভাবে এই অ্যাক্টিভেশন রেকর্ডের প্যারামিটার স্লটে বসে।
- Data Structures & Algorithms কোর্স সঙ্গী কোর্স কল স্ট্যাক আসলে stack ডেটা স্ট্রাকচারেরই একটি বাস্তব প্রয়োগ — LIFO push/pop ডিসিপ্লিন এই কোর্সে বিস্তারিত।
- Computer Architecture & Digital Logic কোর্স সহোদর কোর্স হার্ডওয়্যার-লেভেলে stack pointer রেজিস্টার ও কল/রিটার্ন ইনস্ট্রাকশন ঠিক এই একই মডেল বাস্তবায়ন করে।