অটোমাটা থিওরি বাস্তবে — কম্পাইলার ও টেক্সট প্রসেসিং
এই পাঠে যা শিখবেন
- M2-M8-এর অটোমাটা তত্ত্ব বাস্তবের তিনটি প্রধান জায়গায় (লেক্সিং, পার্সিং, টেক্সট সার্চ) কীভাবে ব্যবহৃত হয়
- এই কোর্সের প্রতিটি প্রাসঙ্গিক পাঠ (L06, L15-16, L20, L24-25, L10-11) কোন বাস্তব ব্যবহারের সাথে যুক্ত তা একটি সংশ্লেষণ টেবিলে দেখা
- ক্লিনির থিওরেমের রেগেক্স → NFA → DFA পাইপলাইন পুনর্ব্যবহার করে একটি সত্যিকারের, কার্যকর টেক্সট-সার্চ টুল বাস্তবায়ন
- কেন DFA-ভিত্তিক সার্চ লিনিয়ার-টাইম এবং কেন এটি বাস্তব regex ইঞ্জিনের ভিত্তি (L54-এর কেস স্টাডির প্রিভিউ)
১ · M2-M8 বাস্তবের কোথায় কাজে লাগে
এই কোর্সের M2-M8 জুড়ে আমরা অটোমাটার ফরমাল সংজ্ঞা, ইকুইভ্যালেন্স প্রমাণ, ও সীমাবদ্ধতা প্রমাণ করেছি — এবার সেই
বিমূর্ত তত্ত্ব বাস্তবের কোন কংক্রিট টুলে রূপ নেয় তা সরাসরি দেখা যাক, ../programming-languages-compilers/
কোর্সের সাথে সংক্ষিপ্তভাবে ক্রস-রেফারেন্স করে (সেই কোর্স ইতিমধ্যে ব্যবহারিক লেক্সার/পার্সার-নির্মাণ কভার করেছে —
এখানে আমরা পুনরায় তৈরি করছি না, শুধু এই কোর্সের তত্ত্ব সেই ব্যবহারিক কাজের ভিত্তি কীভাবে তা দেখাচ্ছি)।
বাস্তব কম্পাইলার/ইন্টারপ্রেটার DFA (M2/L06) ব্যবহার করে টোকেনাইজ করে — কারণ DFA-এর প্রমাণিত প্রপার্টি (L06-এর টোটালিটি গ্যারান্টি, L15-16-এর মিনিমাইজেশন থিওরেম) এদের দ্রুত, সিঙ্গল-পাস টোকেনাইজিং-এর জন্য আদর্শ করে তোলে (cross-ref
../programming-languages-compilers/-এর M4)।LL/LR পার্সার নির্দিষ্টভাবে ডিটারমিনিস্টিক কনটেক্সট-ফ্রি ল্যাঙ্গুয়েজে (M5/L25-এর DCFL ধারণা) কাজ করে — এই কোর্সের M4-M6 তত্ত্ব (CNF/L20, PDA-CFG ইকুইভ্যালেন্স/L24, ডিটারমিনিস্টিক-বনাম-নন-ডিটারমিনিস্টিক PDA/L25) ঠিক সেই গাণিতিক ভিত্তি যার উপর ব্যবহারিক পার্সিং অ্যালগরিদম দাঁড়িয়ে (cross-ref সেই কোর্সের M5)।
আধুনিক টেক্সট এডিটর/grep-স্টাইল টুল একটি regex প্যাটার্নকে একটি অটোমাটায় কম্পাইল করে (ক্লিনির থিওরেম, M2/L10-11) দ্রুত, লিনিয়ার-টাইম স্ট্রিং সার্চের জন্য — L54-এর কেস স্টাডিতে দেখা যাবে বাস্তব regex ইঞ্জিন কোথায় এই বিশুদ্ধ তত্ত্ব থেকে বিচ্যুত হয়।
২ · সংশ্লেষণ টেবিল — এই কোর্স ও বাস্তবের সংযোগ
নিচের কোড সেলের প্রথম অংশে এই তিনটি বাস্তব প্রয়োগকে এই কোর্সের নির্দিষ্ট পাঠের সাথে সরাসরি সংযুক্ত করে একটি রেফারেন্স টেবিল তৈরি করা হয়েছে — যাতে বিমূর্ত তত্ত্ব থেকে পরিচিত, বাস্তব টুলে যাওয়ার পথটা স্পষ্ট থাকে।
৩ · সত্যিকারের রেগেক্স-টু-NFA-টু-DFA টেক্সট সার্চ
L10-এ আমরা regex-কে নেস্টেড টাপল হিসেবে (base case: ('sym', a), inductive case:
('union', R, S), ('concat', R, S), ('star', R)) সংজ্ঞায়িত করেছিলাম, আর
L11-এ ক্লিনির থিওরেম অনুযায়ী সেই regex-কে একটি ε-NFA-তে (থম্পসন-স্টাইল কনস্ট্রাকশন) ও তারপর L08-এর সাবসেট
কনস্ট্রাকশন দিয়ে একটি DFA-তে রূপান্তর করেছিলাম। নিচের কোড সেল এই সম্পূর্ণ পাইপলাইনটি পুনর্ব্যবহার করে
একটি বাস্তব, কার্যকর টেক্সট-সার্চ টুল বানাবে — regex (a∪b)*a ("a বা b-এর যেকোনো সিকোয়েন্স, শেষে a")
একটি DFA-তে কম্পাইল করে, তারপর একটি দীর্ঘ টেক্সট স্ট্রিং-এ প্রতিটি শুরু-অবস্থান থেকে সেই DFA দিয়ে ম্যাচ খুঁজে বের
করবে — ঠিক যেভাবে একটি বাস্তব regex ইঞ্জিন কাজ করে (কম্পাইল একবার, তারপর প্রতিটি অবস্থানে দ্রুত রান)।
# অংশ ১ -- এই কোর্সের তত্ত্ব ও বাস্তব প্রয়োগের সংশ্লেষণ টেবিল
applications = {
"লেক্সিক্যাল অ্যানালাইসিস": {
"theoretical_foundation": "DFA (M2), মিনিমাইজেশন থিওরেম (M3)",
"this_courses_lesson_reference": "L06, L15-L16",
"practical_benefit": "দ্রুত, সিঙ্গল-পাস, প্রেডিক্টেবল টোকেনাইজিং",
},
"পার্সিং": {
"theoretical_foundation": "CNF, PDA-CFG ইকুইভ্যালেন্স, DCFL",
"this_courses_lesson_reference": "L20, L24, L25",
"practical_benefit": "LL/LR পার্সার -- নির্ভরযোগ্য, ব্যাকট্র্যাকিং-মুক্ত পার্সিং",
},
"টেক্সট সার্চ": {
"theoretical_foundation": "ক্লিনির থিওরেম -- regex ⇔ ফাইনাইট অটোমাটা",
"this_courses_lesson_reference": "L10, L11",
"practical_benefit": "লিনিয়ার-টাইম প্যাটার্ন ম্যাচিং",
},
}
print(f"{'বাস্তব প্রয়োগ':22s} | {'তাত্ত্বিক ভিত্তি':38s} | {'এই কোর্সের পাঠ'}")
print("-" * 90)
for app, info in applications.items():
print(f"{app:22s} | {info['theoretical_foundation']:38s} | {info['this_courses_lesson_reference']}")
print()
# অংশ ২ -- L10-L11-এর regex-to-NFA-to-DFA পাইপলাইন পুনর্ব্যবহার করে একটি সত্যিকারের টেক্সট-সার্চ টুল
class Counter:
def __init__(self):
self.n = 0
def new_state(self):
s = self.n
self.n += 1
return s
def build_nfa(ast, counter, transitions):
# regex AST-এর প্রতিটি recursive case-এর জন্য থম্পসন-স্টাইল ε-NFA fragment তৈরি (L11)
kind = ast[0]
if kind == 'eps':
s, a = counter.new_state(), counter.new_state()
transitions.setdefault(s, []).append((None, a))
return s, a
if kind == 'sym':
ch = ast[1]
s, a = counter.new_state(), counter.new_state()
transitions.setdefault(s, []).append((ch, a))
return s, a
if kind == 'union':
s1, a1 = build_nfa(ast[1], counter, transitions)
s2, a2 = build_nfa(ast[2], counter, transitions)
s, a = counter.new_state(), counter.new_state()
transitions.setdefault(s, []).extend([(None, s1), (None, s2)])
transitions.setdefault(a1, []).append((None, a))
transitions.setdefault(a2, []).append((None, a))
return s, a
if kind == 'concat':
s1, a1 = build_nfa(ast[1], counter, transitions)
s2, a2 = build_nfa(ast[2], counter, transitions)
transitions.setdefault(a1, []).append((None, s2))
return s1, a2
if kind == 'star':
s1, a1 = build_nfa(ast[1], counter, transitions)
s, a = counter.new_state(), counter.new_state()
transitions.setdefault(s, []).extend([(None, s1), (None, a)])
transitions.setdefault(a1, []).extend([(None, s1), (None, a)])
return s, a
raise ValueError("অজানা regex node")
def regex_to_nfa(ast):
counter = Counter()
transitions = {}
start, accept = build_nfa(ast, counter, transitions)
return {'start': start, 'accept': {accept}, 'transitions': transitions}
def epsilon_closure(transitions, states):
# L09-এর ফিক্সড-পয়েন্ট ε-closure অ্যালগরিদম
stack = list(states)
closure = set(states)
while stack:
s = stack.pop()
for (sym, t) in transitions.get(s, []):
if sym is None and t not in closure:
closure.add(t)
stack.append(t)
return closure
def subset_construction(nfa, alphabet):
# L08-এর সাবসেট কনস্ট্রাকশন -- NFA-কে সমতুল্য DFA-তে রূপান্তর
trans = nfa['transitions']
start_set = frozenset(epsilon_closure(trans, {nfa['start']}))
dfa_states = {start_set}
dfa_transitions = {}
unmarked = [start_set]
accept_nfa = nfa['accept']
while unmarked:
S = unmarked.pop()
for ch in alphabet:
move = set()
for s in S:
for (sym, t) in trans.get(s, []):
if sym == ch:
move.add(t)
if not move:
continue
T = frozenset(epsilon_closure(trans, move))
dfa_transitions[(S, ch)] = T
if T not in dfa_states:
dfa_states.add(T)
unmarked.append(T)
dfa_accept = {S for S in dfa_states if S & accept_nfa}
return {'start': start_set, 'transitions': dfa_transitions, 'accept': dfa_accept}
def dfa_accepts(dfa, string):
state = dfa['start']
for ch in string:
key = (state, ch)
if key not in dfa['transitions']:
return False
state = dfa['transitions'][key]
return state in dfa['accept']
def search_pattern(dfa, text):
# প্রতিটি সম্ভাব্য শুরু-অবস্থান থেকে DFA দিয়ে ম্যাচ খোঁজা -- বাস্তব regex ইঞ্জিনের core loop-এর সরলীকৃত সংস্করণ
matches = []
n = len(text)
for i in range(n):
state = dfa['start']
for j in range(i, n):
key = (state, text[j])
if key not in dfa['transitions']:
break
state = dfa['transitions'][key]
if state in dfa['accept']:
matches.append((i, j + 1, text[i:j + 1]))
return matches
# regex (a|b)*a -- L10-এর ঠিক একই উদাহরণ, "শেষে a"-যুক্ত a/b স্ট্রিং
regex_ast = ('concat', ('star', ('union', ('sym', 'a'), ('sym', 'b'))), ('sym', 'a'))
nfa = regex_to_nfa(regex_ast)
dfa = subset_construction(nfa, ['a', 'b'])
# পুরো-স্ট্রিং অ্যাকসেপ্টেন্স যাচাই -- L10-এর বর্ণনার সাথে মিলছে কি না
for s in ["", "a", "b", "aab", "bba"]:
expected = len(s) > 0 and s[-1] == 'a'
print(f"পুরো-স্ট্রিং: {s!r:6s} -> DFA={dfa_accepts(dfa, s)} (প্রত্যাশিত={expected})")
assert dfa_accepts(dfa, s) == expected
# এবার আসল সার্চ -- একটি দীর্ঘ টেক্সটে প্যাটার্নের সব ম্যাচ খুঁজে বের করা
text = "baabab"
matches = search_pattern(dfa, text)
print(f"\nটেক্সট {text!r}-এ '(a|b)*a' প্যাটার্নের সব ম্যাচ:")
for start, end, matched in matches:
print(f" পজিশন [{start},{end}) -> {matched!r}")
print(f"\nমোট ম্যাচ পাওয়া গেছে: {len(matches)}টি")
search_pattern প্রতিটি শুরু-অবস্থানে DFA-এর start state থেকে আবার শুরু
করে এবং প্রতিটি ধাপে বর্তমান অবস্থা accept-এ আছে কি না চেক করে — এভাবে একটি একক পজিশন থেকে একাধিক দৈর্ঘ্যের ম্যাচ
(nested matches) পাওয়া সম্ভব হয়। যেহেতু DFA-এর প্রতিটি ট্রানজিশন $O(1)$ (একটি ডিকশনারি লুকআপ), আর টেক্সটের প্রতিটি
অবস্থান থেকে সর্বোচ্চ টেক্সটের বাকি অংশ পর্যন্ত স্ক্যান করা হয় — এটাই কেন DFA-ভিত্তিক সার্চ এত দ্রুত ও প্রেডিক্টেবল,
L06-এর DFA-এর টোটালিটি ও ডিটারমিনিজম গ্যারান্টির সরাসরি ব্যবহারিক পরিণতি।
M2-M8-এর প্রতিটি ফরমাল প্রমাণ — DFA-এর টোটালিটি (L06), মিনিমাইজেশন (L15-16), ক্লিনির থিওরেম (L11), PDA-CFG ইকুইভ্যালেন্স (L24) — বিমূর্ত গণিত নয়, বরং প্রতিদিনের ব্যবহৃত কম্পাইলার ও টেক্সট-এডিটরের নির্ভরযোগ্যতা ও গতির সরাসরি গাণিতিক ভিত্তি। উপরের কোড সেল প্রমাণ করেছে regex থেকে NFA থেকে DFA পর্যন্ত পুরো পাইপলাইনটি সত্যিই কাজ করে — শুধু তত্ত্বে নয়, একটি বাস্তব, চলমান টেক্সট-সার্চ টুল হিসেবেও।
ভাবনার প্রশ্ন
প্রতিটি প্রশ্ন নিজে কিছুক্ষণ ভাবুন — তারপর "→ উত্তর" চাপুন।
প্র ০১ কম্পাইলার লেক্সিং-এর জন্য কেন NFA সরাসরি না ব্যবহার করে DFA-তে রূপান্তর করে ব্যবহার করা হয়?
একটি NFA সিমুলেট করতে হলে প্রতিটি ধাপে সম্ভাব্য একাধিক অবস্থার সেট ট্র্যাক করতে হয় (L07), যা প্রতিটি ইনপুট সিম্বলে অতিরিক্ত কাজ যোগ করে। একবার L08-এর সাবসেট কনস্ট্রাকশন দিয়ে DFA-তে রূপান্তর করা হলে, রানটাইমে প্রতিটি ধাপ মাত্র একটি ডিকশনারি লুকআপ ($O(1)$) — রূপান্তরের এককালীন খরচ (compile-time) দিয়ে প্রতিটি রান-টাইম লুকআপকে দ্রুততম সম্ভব করে তোলা হয়, যা একটি কম্পাইলারে লক্ষ লক্ষ বার চালানো একটি লেক্সারের জন্য অত্যন্ত গুরুত্বপূর্ণ।
প্র ০২
উপরের কোডে search_pattern কেন প্রতিটি শুরু-অবস্থানে DFA-কে আবার start state থেকে শুরু করায়, আগের অবস্থান থেকে চালিয়ে যায় না কেন?
কারণ একটি ম্যাচ কোনো নির্দিষ্ট অবস্থানে শুরু হতে পারে যা আগের কোনো ম্যাচের সাথে সম্পর্কিত নয় — DFA-এর "মেমোরি" (তার বর্তমান state) শুধু "এই নির্দিষ্ট শুরু-অবস্থান থেকে এখন পর্যন্ত কী পড়া হয়েছে" তা এনকোড করে, ভিন্ন শুরু-অবস্থানের তথ্য বহন করে না। তাই প্রতিটি সম্ভাব্য শুরুতে fresh শুরু করাটাই সঠিক আচরণ — এটি ঠিক কীভাবে একটি একক-অটোমাটা matcher একাধিক ওভারল্যাপিং সম্ভাব্য ম্যাচ পজিশন পরীক্ষা করে (বাস্তব regex ইঞ্জিনগুলো এটি আরও অপ্টিমাইজড উপায়ে করে, কিন্তু মূল নীতি একই)।
প্র ০৩ পার্সিং কেন শুধু CFG (M4) নয়, নির্দিষ্টভাবে ডিটারমিনিস্টিক CFL (M5/L25)-এর উপর নির্ভর করে?
LL/LR-এর মতো ব্যবহারিক পার্সিং অ্যালগরিদম প্রতিটি ধাপে ব্যাকট্র্যাকিং ছাড়াই পরবর্তী পদক্ষেপ ঠিক করতে চায় (কর্মক্ষমতার জন্য) — এটি সম্ভব শুধু তখনই যখন গ্রামারটি ডিটারমিনিস্টিক (L25-এর DPDA দিয়ে recognizable) হয়, যেখানে প্রতিটি মুহূর্তে ঠিক একটি বৈধ পরবর্তী পদক্ষেপ থাকে। সাধারণ (নন-ডিটারমিনিস্টিক) CFG-র জন্য পার্সিং অনেক বেশি ব্যয়বহুল (ব্যাকট্র্যাকিং বা একাধিক সম্ভাবনা একসাথে ট্র্যাক করা প্রয়োজন) — তাই বাস্তব কম্পাইলার ডিজাইনাররা প্রায়ই ইচ্ছাকৃতভাবে তাদের ভাষার গ্রামারকে ডিটারমিনিস্টিক-ফ্রেন্ডলি রাখেন।
অনুশীলন
-
চিন্তা করুন: সংশ্লেষণ টেবিলে তিনটি বাস্তব প্রয়োগের প্রতিটির জন্য, ভেবে দেখুন যদি সংশ্লিষ্ট তাত্ত্বিক গ্যারান্টি (DFA টোটালিটি, DCFL ডিটারমিনিজম, ক্লিনির থিওরেমের ইকুইভ্যালেন্স) না থাকতো, তাহলে ব্যবহারিক টুলটির কী সমস্যা হতো?
DFA টোটালিটি ছাড়া লেক্সার অসংজ্ঞায়িত ইনপুটে ক্র্যাশ করতে পারত। DCFL ডিটারমিনিজম ছাড়া পার্সার প্রতিটি ধাপে একাধিক সম্ভাবনা ট্র্যাক করতে বাধ্য হতো (ব্যাকট্র্যাকিং, ধীরগতি)। ক্লিনির থিওরেমের ইকুইভ্যালেন্স ছাড়া regex ইঞ্জিন নিশ্চিত হতে পারত না যে অটোমাটায় কম্পাইল করা প্যাটার্নটি আসল regex-এর সাথে ঠিক একই ভাষা চেক করছে কি না — প্রতিটি ক্ষেত্রেই তত্ত্বীয় গ্যারান্টি ব্যবহারিক নির্ভরযোগ্যতার ভিত্তি।
-
পরীক্ষা করুন: উপরের কোড সেলে
regex_astবদলে('concat', ('sym','a'), ('star', ('sym','b')))(অর্থাৎ regexab*) বানিয়ে Run চাপুন — নতুনtext = "abbba"-তে কতগুলো ম্যাচ পাওয়া যায় হাতে গুনে যাচাই করুন।ab*মানে একটি 'a' তারপর শূন্য বা তার বেশি 'b'।"abbba"-তে পজিশন ০ থেকে "a", "ab", "abb", "abbb" — এই ৪টি ম্যাচ শুরু হয় (প্রতিটি বৈধ, কারণ 'a' দিয়ে শুরু ও শুধু 'b' অনুসরণ করে), এবং পজিশন ৪-এ (শেষ 'a') আরেকটি ম্যাচ "a" — মোট ৫টি ম্যাচ প্রত্যাশিত। কোড চালিয়ে এই সংখ্যা মিলছে কি না যাচাই করুন।
আরও পড়ুন · ABCL TECH-এ আপনার পরবর্তী পদক্ষেপ
- কোর্সের সম্পূর্ণ সিলেবাস দেখুন ৫৬টি পাঠ পরবর্তী পাঠ — কম্পিউটেবিলিটি ও ক্রিপ্টোগ্রাফি — M12-এর দ্বিতীয় বাস্তব-প্রয়োগ সেতু-পাঠ।
- Programming Languages & Compiler Design কোর্স সঙ্গী কোর্স লেক্সার, পার্সার ও regex-ম্যাচার বাস্তবে কীভাবে নির্মাণ করা হয় তা সেই কোর্সের ব্যবহারিক অধ্যায়ে দেখুন — এই পাঠ তাদের গাণিতিক ভিত্তি দেখিয়েছে।
- পাঠ ৫০ — NP-হার্ডনেস মোকাবিলা পূর্ববর্তী পাঠ M11-এর কমপ্লেক্সিটি তত্ত্ব বাস্তবে কীভাবে অ্যাপ্রক্সিমেশন ও হিউরিস্টিক-এ রূপ নেয়।