ไทย

สำรวจการวิเคราะห์คำศัพท์ ขั้นตอนแรกของคอมไพเลอร์ เรียนรู้เรื่องโทเค็น, เล็กซีม, นิพจน์ปรกติ, ออโตมาตาจำกัด และการใช้งานจริง

การออกแบบคอมไพเลอร์: พื้นฐานการวิเคราะห์คำศัพท์

การออกแบบคอมไพเลอร์เป็นสาขาที่น่าสนใจและมีความสำคัญอย่างยิ่งในวิทยาการคอมพิวเตอร์ ซึ่งเป็นรากฐานของการพัฒนาซอฟต์แวร์สมัยใหม่ส่วนใหญ่ คอมไพเลอร์คือสะพานเชื่อมระหว่างซอร์สโค้ดที่มนุษย์อ่านได้กับคำสั่งที่เครื่องจักรสามารถ εκτελέสได้ บทความนี้จะเจาะลึกถึงพื้นฐานของการวิเคราะห์คำศัพท์ (lexical analysis) ซึ่งเป็นขั้นตอนแรกในกระบวนการคอมไพล์ เราจะสำรวจวัตถุประสงค์ แนวคิดหลัก และผลกระทบในทางปฏิบัติสำหรับนักออกแบบคอมไพเลอร์และวิศวกรซอฟต์แวร์ทั่วโลก

การวิเคราะห์คำศัพท์ (Lexical Analysis) คืออะไร?

การวิเคราะห์คำศัพท์ หรือที่เรียกว่าการสแกน (scanning) หรือการทำโทเค็น (tokenizing) เป็นขั้นตอนแรกของคอมไพเลอร์ หน้าที่หลักคือการอ่านซอร์สโค้ดในรูปแบบของกระแสอักขระ (stream of characters) และจัดกลุ่มให้เป็นลำดับที่มีความหมายเรียกว่า เล็กซีม (lexemes) จากนั้นเล็กซีมแต่ละตัวจะถูกจัดหมวดหมู่ตามบทบาทของมัน ส่งผลให้เกิดลำดับของ โทเค็น (tokens) ลองนึกภาพว่ามันเป็นกระบวนการจัดเรียงและติดป้ายเบื้องต้นเพื่อเตรียมข้อมูลสำหรับการประมวลผลต่อไป

สมมติว่าคุณมีประโยค: `x = y + 5;` ตัววิเคราะห์คำศัพท์จะแบ่งประโยคนี้ออกเป็นโทเค็นต่อไปนี้:

โดยพื้นฐานแล้ว ตัววิเคราะห์คำศัพท์จะระบุส่วนประกอบพื้นฐานเหล่านี้ของภาษาโปรแกรม

แนวคิดสำคัญในการวิเคราะห์คำศัพท์

โทเค็น (Tokens) และเล็กซีม (Lexemes)

ดังที่ได้กล่าวไปแล้ว โทเค็น (token) คือการแสดงแทนเล็กซีมที่จัดหมวดหมู่แล้ว ส่วน เล็กซีม (lexeme) คือลำดับของอักขระจริงในซอร์สโค้ดที่ตรงกับรูปแบบของโทเค็น พิจารณาโค้ดตัวอย่างในภาษา Python ต่อไปนี้:

if x > 5:
    print("x is greater than 5")

นี่คือตัวอย่างบางส่วนของโทเค็นและเล็กซีมจากโค้ดตัวอย่างนี้:

โทเค็นแสดงถึง *หมวดหมู่* ของเล็กซีม ในขณะที่เล็กซีมคือ *สตริงจริง* จากซอร์สโค้ด ตัวแจงส่วน (parser) ซึ่งเป็นขั้นตอนถัดไปในการคอมไพล์ จะใช้โทเค็นเพื่อทำความเข้าใจโครงสร้างของโปรแกรม

นิพจน์ปรกติ (Regular Expressions)

นิพจน์ปรกติ (regex) เป็นสัญกรณ์ที่ทรงพลังและรัดกุมสำหรับอธิบายรูปแบบของอักขระ นิพจน์ปรกติถูกนำมาใช้อย่างแพร่หลายในการวิเคราะห์คำศัพท์เพื่อกำหนดรูปแบบที่เล็กซีมต้องตรงกันเพื่อให้ได้รับการยอมรับว่าเป็นโทเค็นเฉพาะ นิพจน์ปรกติเป็นแนวคิดพื้นฐานไม่เพียงแต่ในการออกแบบคอมไพเลอร์เท่านั้น แต่ยังรวมถึงในหลายๆ ด้านของวิทยาการคอมพิวเตอร์ ตั้งแต่การประมวลผลข้อความไปจนถึงความปลอดภัยของเครือข่าย

นี่คือสัญลักษณ์นิพจน์ปรกติที่พบบ่อยและความหมาย:

ลองดูตัวอย่างการใช้นิพจน์ปรกติเพื่อกำหนดโทเค็น:

ภาษาโปรแกรมต่างๆ อาจมีกฎเกณฑ์ที่แตกต่างกันสำหรับตัวระบุ ค่าคงที่จำนวนเต็ม และโทเค็นอื่นๆ ดังนั้น นิพจน์ปรกติที่เกี่ยวข้องจึงต้องปรับเปลี่ยนตามไปด้วย ตัวอย่างเช่น บางภาษาอาจอนุญาตให้อักขระ Unicode อยู่ในตัวระบุได้ ซึ่งต้องใช้ regex ที่ซับซ้อนมากขึ้น

ออโตมาตาจำกัด (Finite Automata)

ออโตมาตาจำกัด (FA) เป็นเครื่องจักรนามธรรมที่ใช้ในการจดจำรูปแบบที่กำหนดโดยนิพจน์ปรกติ เป็นแนวคิดหลักในการนำตัววิเคราะห์คำศัพท์ไปปฏิบัติจริง ออโตมาตาจำกัดมีสองประเภทหลัก:

กระบวนการทั่วไปในการวิเคราะห์คำศัพท์ประกอบด้วย:

  1. แปลงนิพจน์ปรกติสำหรับโทเค็นแต่ละประเภทเป็น NFA
  2. แปลง NFA เป็น DFA
  3. นำ DFA ไปปฏิบัติเป็นสแกนเนอร์ที่ขับเคลื่อนด้วยตาราง (table-driven scanner)

จากนั้น DFA จะถูกใช้เพื่อสแกนกระแสอินพุตและระบุโทเค็น DFA จะเริ่มต้นที่สถานะเริ่มต้นและอ่านอินพุตทีละอักขระ จากสถานะปัจจุบันและอักขระอินพุต มันจะเปลี่ยนไปยังสถานะใหม่ หาก DFA ไปถึงสถานะยอมรับ (accepting state) หลังจากอ่านลำดับของอักขระแล้ว ลำดับนั้นจะถูกจดจำว่าเป็นเล็กซีม และโทเค็นที่สอดคล้องกันจะถูกสร้างขึ้น

การวิเคราะห์คำศัพท์ทำงานอย่างไร

ตัววิเคราะห์คำศัพท์ทำงานดังนี้:

  1. อ่านซอร์สโค้ด: เล็กเซอร์จะอ่านซอร์สโค้ดทีละอักขระจากไฟล์หรือกระแสอินพุต
  2. ระบุเล็กซีม: เล็กเซอร์ใช้นิพจน์ปรกติ (หรือแม่นยำกว่านั้นคือ DFA ที่ได้มาจากนิพจน์ปรกติ) เพื่อระบุลำดับของอักขระที่ก่อตัวเป็นเล็กซีมที่ถูกต้อง
  3. สร้างโทเค็น: สำหรับแต่ละเล็กซีมที่พบ เล็กเซอร์จะสร้างโทเค็นซึ่งประกอบด้วยเล็กซีมเองและประเภทของโทเค็น (เช่น IDENTIFIER, INTEGER_LITERAL, OPERATOR)
  4. จัดการข้อผิดพลาด: หากเล็กเซอร์พบกับลำดับของอักขระที่ไม่ตรงกับรูปแบบที่กำหนดไว้ (คือไม่สามารถทำเป็นโทเค็นได้) มันจะรายงานข้อผิดพลาดทางคำศัพท์ (lexical error) ซึ่งอาจรวมถึงอักขระที่ไม่ถูกต้องหรือตัวระบุที่สร้างขึ้นอย่างไม่เหมาะสม
  5. ส่งต่อโทเค็นไปยังตัวแจงส่วน: เล็กเซอร์จะส่งกระแสของโทเค็นไปยังขั้นตอนถัดไปของคอมไพเลอร์ คือตัวแจงส่วน (parser)

พิจารณาโค้ด C ตัวอย่างง่ายๆ นี้:

int main() {
  int x = 10;
  return 0;
}

ตัววิเคราะห์คำศัพท์จะประมวลผลโค้ดนี้และสร้างโทเค็นต่อไปนี้ (แบบย่อ):

การนำไปปฏิบัติจริงของตัววิเคราะห์คำศัพท์

มีสองแนวทางหลักในการนำตัววิเคราะห์คำศัพท์ไปปฏิบัติจริง:

  1. การสร้างด้วยตนเอง (Manual Implementation): เขียนโค้ดเล็กเซอร์ด้วยมือ วิธีนี้ให้การควบคุมและความเป็นไปได้ในการปรับแต่งที่มากขึ้น แต่ใช้เวลามากกว่าและมีโอกาสเกิดข้อผิดพลาดได้ง่าย
  2. การใช้โปรแกรมสร้างเล็กเซอร์ (Lexer Generators): ใช้เครื่องมืออย่าง Lex (Flex), ANTLR, หรือ JFlex ซึ่งจะสร้างโค้ดเล็กเซอร์โดยอัตโนมัติตามข้อกำหนดของนิพจน์ปรกติ

การสร้างด้วยตนเอง (Manual Implementation)

การสร้างด้วยตนเองโดยทั่วไปจะเกี่ยวข้องกับการสร้างเครื่องสถานะ (DFA) และเขียนโค้ดเพื่อเปลี่ยนระหว่างสถานะต่างๆ ตามอักขระอินพุต แนวทางนี้ช่วยให้สามารถควบคุมกระบวนการวิเคราะห์คำศัพท์ได้อย่างละเอียดและสามารถปรับให้เหมาะสมกับความต้องการด้านประสิทธิภาพเฉพาะได้ อย่างไรก็ตาม มันต้องอาศัยความเข้าใจอย่างลึกซึ้งเกี่ยวกับนิพจน์ปรกติและออโตมาตาจำกัด และอาจเป็นเรื่องท้าทายในการบำรุงรักษาและดีบัก

นี่คือตัวอย่างเชิงแนวคิด (และง่ายมาก) ของการที่เล็กเซอร์ที่สร้างด้วยมืออาจจัดการกับค่าคงที่จำนวนเต็มใน Python:

def lexer(input_string):
    tokens = []
    i = 0
    while i < len(input_string):
        if input_string[i].isdigit():
            # Found a digit, start building the integer
            num_str = ""
            while i < len(input_string) and input_string[i].isdigit():
                num_str += input_string[i]
                i += 1
            tokens.append(("INTEGER", int(num_str)))
            i -= 1 # Correct for the last increment
        elif input_string[i] == '+':
            tokens.append(("PLUS", "+"))
        elif input_string[i] == '-':
            tokens.append(("MINUS", "-"))
        # ... (handle other characters and tokens)
        i += 1
    return tokens

นี่เป็นตัวอย่างพื้นฐาน แต่แสดงให้เห็นแนวคิดหลักของการอ่านสตริงอินพุตด้วยตนเองและระบุโทเค็นตามรูปแบบของอักขระ

โปรแกรมสร้างเล็กเซอร์ (Lexer Generators)

โปรแกรมสร้างเล็กเซอร์เป็นเครื่องมือที่ทำให้กระบวนการสร้างตัววิเคราะห์คำศัพท์เป็นไปโดยอัตโนมัติ เครื่องมือเหล่านี้จะรับไฟล์ข้อกำหนด (specification file) เป็นอินพุต ซึ่งกำหนดนิพจน์ปรกติสำหรับโทเค็นแต่ละประเภทและการกระทำที่จะดำเนินการเมื่อโทเค็นถูกจดจำ จากนั้นโปรแกรมสร้างจะผลิตโค้ดเล็กเซอร์ในภาษาโปรแกรมเป้าหมาย

นี่คือโปรแกรมสร้างเล็กเซอร์ที่นิยมบางตัว:

การใช้โปรแกรมสร้างเล็กเซอร์มีข้อดีหลายประการ:

นี่คือตัวอย่างข้อกำหนดของ Flex ง่ายๆ สำหรับการจดจำจำนวนเต็มและตัวระบุ:

%%
[0-9]+      { printf("INTEGER: %s\n", yytext); }
[a-zA-Z_][a-zA-Z0-9_]* { printf("IDENTIFIER: %s\n", yytext); }
[ \t\n]+  ; // Ignore whitespace
.           { printf("ILLEGAL CHARACTER: %s\n", yytext); }
%%

ข้อกำหนดนี้กำหนดกฎสองข้อ: ข้อหนึ่งสำหรับจำนวนเต็มและอีกข้อสำหรับตัวระบุ เมื่อ Flex ประมวลผลข้อกำหนดนี้ มันจะสร้างโค้ด C สำหรับเล็กเซอร์ที่จดจำโทเค็นเหล่านี้ ตัวแปร `yytext` จะมีเล็กซีมที่ตรงกัน

การจัดการข้อผิดพลาดในการวิเคราะห์คำศัพท์

การจัดการข้อผิดพลาดเป็นส่วนสำคัญของการวิเคราะห์คำศัพท์ เมื่อเล็กเซอร์พบอักขระที่ไม่ถูกต้องหรือเล็กซีมที่สร้างขึ้นอย่างไม่เหมาะสม มันจำเป็นต้องรายงานข้อผิดพลาดให้ผู้ใช้ทราบ ข้อผิดพลาดทางคำศัพท์ที่พบบ่อยได้แก่:

เมื่อตรวจพบข้อผิดพลาดทางคำศัพท์ เล็กเซอร์ควร:

  1. รายงานข้อผิดพลาด: สร้างข้อความแสดงข้อผิดพลาดที่ระบุหมายเลขบรรทัดและหมายเลขคอลัมน์ที่เกิดข้อผิดพลาด พร้อมทั้งคำอธิบายข้อผิดพลาด
  2. พยายามกู้คืน: พยายามกู้คืนจากข้อผิดพลาดและสแกนอินพุตต่อไป ซึ่งอาจเกี่ยวข้องกับการข้ามอักขระที่ไม่ถูกต้องหรือยุติโทเค็นปัจจุบัน เป้าหมายคือเพื่อหลีกเลี่ยงข้อผิดพลาดที่ต่อเนื่องกันและให้ข้อมูลแก่ผู้ใช้ให้มากที่สุดเท่าที่จะเป็นไปได้

ข้อความแสดงข้อผิดพลาดควรชัดเจนและให้ข้อมูล ช่วยให้โปรแกรมเมอร์ระบุและแก้ไขปัญหาได้อย่างรวดเร็ว ตัวอย่างเช่น ข้อความแสดงข้อผิดพลาดที่ดีสำหรับสตริงที่ไม่สิ้นสุดอาจเป็น: `Error: Unterminated string literal at line 10, column 25`

บทบาทของการวิเคราะห์คำศัพท์ในกระบวนการคอมไพล์

การวิเคราะห์คำศัพท์เป็นขั้นตอนแรกที่สำคัญในกระบวนการคอมไพล์ ผลลัพธ์ของมัน ซึ่งเป็นกระแสของโทเค็น ทำหน้าที่เป็นอินพุตสำหรับขั้นตอนต่อไป คือตัวแจงส่วน (parser หรือ syntax analyzer) ตัวแจงส่วนจะใช้โทเค็นเพื่อสร้างแผนภูมินามธรรมวากยสัมพันธ์ (Abstract Syntax Tree - AST) ซึ่งแสดงถึงโครงสร้างทางไวยากรณ์ของโปรแกรม หากไม่มีการวิเคราะห์คำศัพท์ที่แม่นยำและเชื่อถือได้ ตัวแจงส่วนจะไม่สามารถตีความซอร์สโค้ดได้อย่างถูกต้อง

ความสัมพันธ์ระหว่างการวิเคราะห์คำศัพท์และการแจงส่วนสามารถสรุปได้ดังนี้:

จากนั้น AST จะถูกใช้โดยขั้นตอนต่อๆ ไปของคอมไพเลอร์ เช่น การวิเคราะห์ความหมาย (semantic analysis), การสร้างรหัสกลาง (intermediate code generation) และการปรับปรุงรหัส (code optimization) เพื่อผลิตโค้ด εκτελέสสุดท้าย

หัวข้อขั้นสูงในการวิเคราะห์คำศัพท์

แม้ว่าบทความนี้จะครอบคลุมพื้นฐานของการวิเคราะห์คำศัพท์ แต่ก็มีหัวข้อขั้นสูงหลายอย่างที่น่าสำรวจ:

ข้อควรพิจารณาด้านความเป็นสากล (Internationalization)

เมื่อออกแบบคอมไพเลอร์สำหรับภาษาที่มีไว้สำหรับการใช้งานทั่วโลก ควรพิจารณาประเด็นด้านความเป็นสากลเหล่านี้สำหรับการวิเคราะห์คำศัพท์:

การไม่จัดการความเป็นสากลอย่างเหมาะสมอาจนำไปสู่การทำโทเค็นที่ไม่ถูกต้องและข้อผิดพลาดในการคอมไพล์เมื่อต้องจัดการกับซอร์สโค้ดที่เขียนในภาษาต่างๆ หรือใช้ชุดอักขระที่แตกต่างกัน

สรุป

การวิเคราะห์คำศัพท์เป็นส่วนพื้นฐานของการออกแบบคอมไพเลอร์ ความเข้าใจอย่างลึกซึ้งในแนวคิดที่กล่าวถึงในบทความนี้เป็นสิ่งจำเป็นสำหรับทุกคนที่เกี่ยวข้องกับการสร้างหรือทำงานกับคอมไพเลอร์, อินเทอร์พรีเตอร์ หรือเครื่องมือประมวลผลภาษาอื่นๆ ตั้งแต่การทำความเข้าใจโทเค็นและเล็กซีม ไปจนถึงการเรียนรู้นิพจน์ปรกติและออโตมาตาจำกัด ความรู้เกี่ยวกับการวิเคราะห์คำศัพท์เป็นรากฐานที่แข็งแกร่งสำหรับการสำรวจโลกของการสร้างคอมไพเลอร์ต่อไป ด้วยการใช้โปรแกรมสร้างเล็กเซอร์และพิจารณาประเด็นด้านความเป็นสากล นักพัฒนาสามารถสร้างตัววิเคราะห์คำศัพท์ที่แข็งแกร่งและมีประสิทธิภาพสำหรับภาษาโปรแกรมและแพลตฟอร์มที่หลากหลาย ในขณะที่การพัฒนาซอฟต์แวร์ยังคงพัฒนาต่อไป หลักการของการวิเคราะห์คำศัพท์จะยังคงเป็นรากฐานที่สำคัญของเทคโนโลยีการประมวลผลภาษาทั่วโลก