পাঠ ১৬ · ৫৮-এর মধ্যে · মডিউল ৪
Home / Courses / Concepts of Programming Languages & Compiler Design / টোকেন ও লেক্সিম

টোকেন, লেক্সিম ও প্যাটার্ন

Tokens, lexemes & patterns
৭ মিনিট পড়া মধ্যম · Intermediate Python কোডসহ সম্পূর্ণ বাংলায়

এই পাঠে যা শিখবেন

  • টোকেন বনাম লেক্সিম-এর সুনির্দিষ্ট পার্থক্য — L01-এর তোকেনাইজার এই ধারণা ইতিমধ্যেই implicitly ব্যবহার করেছিল
  • প্যাটার্ন ও অ্যাট্রিবিউটের ভূমিকা — এবং কেন এগুলো পরবর্তী কম্পাইলার ধাপে দরকার হয়
  • Maximal munch নীতি — কেন একটি লেক্সার সবসময় দীর্ঘতম সম্ভাব্য লেক্সিম বেছে নেয়
  • Python দিয়ে একটি পূর্ণাঙ্গ Token কাঠামো ও IDENTIFIER সাপোর্ট বাস্তবায়ন

১ · টোকেন বনাম লেক্সিম

L01-এর টোকেনাইজার ইতিমধ্যেই (kind, value) জোড়া তৈরি করেছিল — যেমন ("NUMBER", "42")। এই পাঠে এই দুটো অংশের নাম ও সংজ্ঞা সুনির্দিষ্ট করা হচ্ছে —

টোকেনTokenএকটি লেক্সিক্যাল ইউনিটের ক্যাটেগরি/টাইপ -- সোর্স টেক্সটের একটি অংশ কোন "শ্রেণির" তা বোঝায়।
একটি ক্যাটেগরি/টাইপ — যেমন NUMBER, IDENTIFIER, PLUS, KEYWORD। বিমূর্ত "শ্রেণি," নির্দিষ্ট কোনো ক্যারেক্টার নয়।
লেক্সিমLexemeসোর্স টেক্সটের সেই আসল ক্যারেক্টার-সিকোয়েন্স যা একটি নির্দিষ্ট টোকেন-টাইপের প্যাটার্নের সাথে মিলে যায়।
সোর্স টেক্সটের আসল ক্যারেক্টার-সিকোয়েন্স — যেমন লেক্সিম "total" টোকেন-টাইপ IDENTIFIER-এর সাথে মেলে, লেক্সিম "42" টোকেন-টাইপ NUMBER-এর সাথে।

অর্থাৎ — একটি টোকেন-টাইপের অসংখ্য সম্ভাব্য লেক্সিম থাকতে পারে (NUMBER-এর জন্য "42", "7", "1000" — সবই বৈধ লেক্সিম), কিন্তু একটি নির্দিষ্ট লেক্সিম ঠিক একটি (অথবা কয়েকটি সম্ভাব্য) টোকেন-টাইপের সাথে মেলে।

২ · প্যাটার্ন ও অ্যাট্রিবিউট

প্যাটার্নPatternএকটি নিয়ম যা বর্ণনা করে ঠিক কোন কোন লেক্সিম একটি নির্দিষ্ট টোকেন-টাইপের অন্তর্ভুক্ত। হলো সেই নিয়ম যা বলে দেয় কোন কোন লেক্সিম একটি নির্দিষ্ট টোকেন-টাইপের অন্তর্ভুক্ত — যেমন NUMBER-এর প্যাটার্ন "এক বা তার বেশি ডিজিট"। এই পাঠে আমরা প্যাটার্নগুলো সরাসরি Python কোডে (isdigit()-এর মতো) লিখব, কিন্তু L17-এ দেখব কীভাবে এই একই প্যাটার্নগুলো একটি ফরমাল নোটেশনে (রেগুলার এক্সপ্রেশন) নির্দিষ্ট করা যায়।

অ্যাট্রিবিউটAttributeএকটি টোকেনের সাথে জুড়ে দেওয়া বাড়তি তথ্য, শুধু type ও lexeme ছাড়াও -- পরবর্তী কম্পাইলার ধাপে কাজে লাগে। হলো একটি টোকেনের সাথে জুড়ে দেওয়া বাড়তি তথ্য, শুধুমাত্র type ও lexeme ছাড়াও — যেমন একটি NUMBER টোকেনের অ্যাট্রিবিউট হতে পারে তার আসল সংখ্যাসূচক মান (lexeme="42" হলে attribute=42, স্ট্রিং নয় বরং integer হিসেবে) — এই parsed মানটি পরবর্তী ধাপে (M6-M12) সরাসরি কাজে লাগে, প্রতিবার লেক্সিম থেকে পুনরায় parse করার দরকার হয় না।

সোর্স টেক্সট: "total_sum" লেক্সিম extraction (maximal munch) প্যাটার্ন ম্যাচ → টোকেন-টাইপ নির্ণয় অ্যাট্রিবিউট সংযুক্তি Token(type=IDENTIFIER, lexeme="total_sum", attribute=None)
প্রতিটি টোকেন তৈরির প্রক্রিয়া চারটি ধাপে ভাঙা যায় — নিচের কোড সেল এই পুরো প্রক্রিয়া বাস্তবে চালাবে।

৩ · Maximal munch — সবসময় দীর্ঘতম লেক্সিম

Maximal munchMaximal Munchযখন ইনপুটের একাধিক prefix বৈধ টোকেন হতে পারে, লেক্সার সবসময় সবচেয়ে দীর্ঘ সম্ভাব্য prefix-টি একটি টোকেন হিসেবে গ্রহণ করে। নীতি বলে — ইনপুটের যদি একাধিক prefix বৈধ লেক্সিম হতে পারে, লেক্সার সবসময় সবচেয়ে দীর্ঘ সম্ভাব্য prefix-টি বেছে নেবে। L01-এর তোকেনাইজার ইতিমধ্যেই এই নীতি সঠিকভাবে মেনে চলেছিল — while i < len(expr) and expr[i].isdigit(): i += 1 লুপটি যতক্ষণ সম্ভব ডিজিট জমা করতেই থাকে, একটি ডিজিট পেয়েই থেমে যায় না। এই পাঠের কোড সেলে আমরা এই একই নীতি IDENTIFIER-এর জন্যও প্রয়োগ করব — যেমন ইনপুট "total_sum"-কে একটি একক IDENTIFIER টোকেন হতে হবে, ৯টি আলাদা এক-অক্ষরের টোকেন নয়।

Python
# L01-এর তোকেনাইজারকে একটি পূর্ণাঙ্গ Token(type, lexeme, attribute) কাঠামোতে সম্প্রসারণ
# নতুন সংযোজন: IDENTIFIER টোকেন-টাইপ (letter/underscore দিয়ে শুরু) + maximal munch

from collections import namedtuple

Token = namedtuple("Token", ["type", "lexeme", "attribute"])

def tokenize(source):
    tokens = []
    i = 0
    n = len(source)
    while i < n:
        ch = source[i]
        if ch.isspace():
            i += 1
            continue
        elif ch.isdigit():
            start = i
            while i < n and source[i].isdigit():      # maximal munch -- যতক্ষণ সম্ভব ডিজিট জমা করো
                i += 1
            lexeme = source[start:i]
            tokens.append(Token("NUMBER", lexeme, int(lexeme)))  # অ্যাট্রিবিউট = parsed সংখ্যাসূচক মান
        elif ch.isalpha() or ch == "_":
            start = i
            while i < n and (source[i].isalnum() or source[i] == "_"):  # maximal munch -- IDENTIFIER-এর জন্য
                i += 1
            lexeme = source[start:i]
            tokens.append(Token("IDENTIFIER", lexeme, None))    # কোনো সংখ্যাসূচক অ্যাট্রিবিউট নেই
        elif ch in "+-*/()":
            tokens.append(Token("OP", ch, None))
            i += 1
        else:
            raise ValueError(f"অজানা ক্যারেক্টার: {ch!r}")
    return tokens

source = "total_sum + 42"
print(f"সোর্স টেক্সট: {source!r}\n")
print(f"{'TYPE':12s} {'LEXEME':12s} ATTRIBUTE")
print("-" * 38)
for tok in tokenize(source):
    print(f"{tok.type:12s} {tok.lexeme:12s} {tok.attribute}")

# --- maximal munch স্পষ্টভাবে যাচাই ---
ident_tokens = [t for t in tokenize(source) if t.type == "IDENTIFIER"]
print(f"\n'total_sum' কতগুলো IDENTIFIER টোকেনে ভাঙল: {len(ident_tokens)}")
print(f"সেই একমাত্র টোকেনের লেক্সিম সম্পূর্ণ কিনা: {ident_tokens[0].lexeme == 'total_sum'}")

    
লক্ষ্য করুন tokenize ফাংশন এখন প্রতিটি টোকেনের জন্য তিনটি পৃথক তথ্য বহন করছে — type (NUMBER/IDENTIFIER/OP), lexeme (আসল সোর্স টেক্সট), এবং attribute (NUMBER-এর ক্ষেত্রে parsed integer, বাকিদের জন্য None)। L01-এর সরল (kind, value) জোড়ার তুলনায় এটি অনেক বেশি সম্পূর্ণ — এই Token কাঠামোটিই এখন থেকে M4-এর বাকি পাঠগুলোতে (L17-L20) এবং M5-এর পার্সারে ব্যবহৃত হবে।
মূল কথা · Key takeaway

টোকেন হলো ক্যাটেগরি, লেক্সিম হলো আসল টেক্সট, প্যাটার্ন হলো নিয়ম যা তাদের সংযুক্ত করে, আর অ্যাট্রিবিউট হলো বাড়তি তথ্য যা পরবর্তী ধাপে কাজে লাগে। Maximal munch নিশ্চিত করে একটি লেক্সার সবসময় সঠিক, সম্পূর্ণ লেক্সিম খুঁজে বের করে — কোনো multi-character টোকেনকে ভুল করে ছোট ছোট টুকরোয় ভাঙে না। পরের পাঠে (L17) আমরা দেখব কীভাবে এই "প্যাটার্ন" ধারণাটিকে একটি ফরমাল নোটেশন — রেগুলার এক্সপ্রেশন — দিয়ে নির্দিষ্ট করা যায়।

ভাবনার প্রশ্ন

প্রতিটি প্রশ্ন নিজে কিছুক্ষণ ভাবুন — তারপর "→ উত্তর" চাপুন।

প্র ০১ "টোকেন" ও "লেক্সিম" — এই দুটো শব্দ প্রায়ই মিলিয়ে ফেলা হয়। একটি বাক্যে এই দুটোর পার্থক্য স্পষ্ট করুন।

টোকেন একটি বিমূর্ত ক্যাটেগরি (যেমন NUMBER), আর লেক্সিম সেই ক্যাটেগরির একটি নির্দিষ্ট বাস্তব উদাহরণ — সোর্স টেক্সট থেকে আসা আসল ক্যারেক্টার-সিকোয়েন্স (যেমন "42")। একটি সহজ উপমা: "ফল" একটি ক্যাটেগরি (টোকেনের মতো), আর "আম" সেই ক্যাটেগরির একটি নির্দিষ্ট উদাহরণ (লেক্সিমের মতো) — একই ক্যাটেগরিতে "কলা," "কমলা"-ও পড়তে পারে, ঠিক যেমন NUMBER টোকেন-টাইপে "42", "7", "1000" সবই পড়ে।

প্র ০২ একটি NUMBER টোকেনের attribute-এ যদি lexeme-এর মতোই স্ট্রিং "42" রাখা হতো, integer 42 না রেখে, তাহলে কী সমস্যা হতো?

পরবর্তী কম্পাইলার ধাপগুলোতে (যেমন M6-এর টাইপ চেকিং, বা একটি ইন্টারপ্রেটারের এক্সপ্রেশন ইভালুয়েশন) প্রতিটি NUMBER টোকেনের সাথে গাণিতিক অপারেশন করতে হয় (যোগ, গুণ ইত্যাদি) — যদি অ্যাট্রিবিউট স্ট্রিং হতো, প্রতিবার ব্যবহারের আগে int(...) দিয়ে পুনরায় parse করতে হতো, যা অপ্রয়োজনীয় পুনরাবৃত্তি ও পারফরম্যান্স অপচয়। lexeme থেকে integer-এ parse করার কাজটি লেক্সিং ধাপেই একবার করে ফেলা এবং সেই তৈরি মান attribute-এ সংরক্ষণ করা — এটাই একটি সঠিকভাবে ডিজাইন করা লেক্সারের কাজ, যাতে বাকি পাইপলাইনকে বারবার একই কাজ পুনরাবৃত্তি করতে না হয়।

প্র ০৩ যদি লেক্সার maximal munch অনুসরণ না করত, তাহলে "total_sum" ইনপুটে কী ভুল ঘটত?

maximal munch ছাড়া লেক্সার প্রথম বৈধ ক্যারেক্টার ("t") পেয়েই থেমে যেতে পারত এবং সেটিকে একটি সম্পূর্ণ IDENTIFIER টোকেন ধরে নিতে পারত — ফলে "total_sum" একটি টোকেনের বদলে নয়টি আলাদা একক-অক্ষরের IDENTIFIER টোকেনে ("t", "o", "t", ...) ভেঙে যেত। এটি সম্পূর্ণ ভুল — পার্সার (M5) তখন একটি চলকের নাম বুঝতেই পারত না, এবং সিম্বল টেবিলে (M6/L28) ভুল, খণ্ডিত নাম যোগ হতো। maximal munch এই সমস্যা এড়ায় লেক্সিম-কে যতটা সম্ভব দীর্ঘ করে তুলে।

অনুশীলন

  1. হাতে-কলমে করুন: "x1 + 99" ইনপুটের জন্য প্রতিটি টোকেনের type, lexeme ও attribute হাতে-কলমে লিখুন, তারপর কোড সেল চালিয়ে মিলিয়ে দেখুন।

    তিনটি টোকেন: (১) type=IDENTIFIER, lexeme="x1", attribute=None — লক্ষ্য করুন "x1"-এ একটি ডিজিটও আছে, কিন্তু যেহেতু এটি একটি অক্ষর দিয়ে শুরু, পুরোটাই IDENTIFIER (isalnum() letter ও digit দুটোই গ্রহণ করে)। (২) type=OP, lexeme="+", attribute=None। (৩) type=NUMBER, lexeme="99", attribute=99।

  2. পরীক্ষা করুন: কোড সেলে source-এর মান "_temp99 * 3" করে Run চেপে দেখুন IDENTIFIER টোকেনটি সঠিকভাবে "_temp99" (আন্ডারস্কোর দিয়ে শুরু) সম্পূর্ণ একটি টোকেন হিসেবে ধরা পড়ে কিনা।

    হ্যাঁ — tokenize-এর IDENTIFIER শাখা ch.isalpha() or ch == "_" দিয়ে শুরু হওয়া লেক্সিম শনাক্ত করে, তাই "_temp99" সম্পূর্ণটাই একটি একক IDENTIFIER টোকেন হয় — ফলাফল হবে তিনটি টোকেন: IDENTIFIER("_temp99"), OP("*"), NUMBER("3", attribute=3)।

আরও পড়ুন · ABCL TECH-এ আপনার পরবর্তী পদক্ষেপ

আগের পাঠ
L15 · চমস্কি হায়ারার্কি