Compiler คืออะไร ทำไมต้องลองสร้างเองสักครั้ง
ถ้าคุณเคยสงสัยว่าโค้ดภาษาอังกฤษที่คุณพิมพ์ลงไป คอมพิวเตอร์เข้าใจได้อย่างไร คำตอบคือมันมี Compiler (ตัวแปลภาษาโปรแกรม) คอยทำหน้าที่เป็นล่ามให้คุณอยู่ตลอดเวลา Compiler ทำหน้าที่เปลี่ยนโค้ดที่มนุษย์อ่านออก ให้กลายเป็นภาษาเครื่องที่คอมพิวเตอร์ทำงานได้โดยตรง
เปรียบเทียบง่ายๆ เหมือนคุณมีหนังสือภาษาต่างประเทศเล่มหนึ่ง แล้วต้องมีคนแปลให้เป็นภาษาไทยเพื่อให้คุณอ่านเข้าใจ Compiler ก็คือ "นักแปล" ที่ทำหน้าที่เปลี่ยนภาษาโปรแกรมที่คุณเขียน ไปเป็นคำสั่งที่คอมพิวเตอร์เอาไปรันได้จริง
การสร้าง Compiler ขึ้นมาเองตั้งแต่ต้น จะช่วยให้คุณเลิกมองว่ามันเป็นแค่กล่องดำที่น่ากลัว คุณจะเข้าใจทันทีว่า Syntax (กฎการเขียนโค้ด) ที่คุณเคยบ่นว่าจำยากนั้น มีที่มาที่ไปอย่างไร และทำไมคอมพิวเตอร์ถึงฟ้อง Error (ข้อผิดพลาด) เวลาคุณลืมใส่เครื่องหมายบางอย่าง
รู้จักกับภาษา Frog ภาษาตัวน้อยสำหรับเรียนรู้
เพื่อให้เราเข้าใจการทำงานของ Compiler ได้ง่ายขึ้น เราจะใช้ภาษาที่ชื่อว่า Frog เป็นภาษาจำลองสำหรับการเรียนรู้ ภาษาแบบนี้ถูกออกแบบมาให้เรียบง่ายที่สุด ไม่ซับซ้อนเหมือนภาษาอย่าง C++ หรือ Java เพื่อให้เราโฟกัสไปที่กลไกการทำงานหลักของตัวแปลภาษาได้เต็มที่
ภาษา Frog มีคำสั่งพื้นฐาน เช่น การประกาศตัวแปร การพิมพ์ข้อความออกหน้าจอ และการทำงานซ้ำๆ (Loop) โดยมีรูปแบบที่คล้ายกับภาษา Pascal (ภาษาโปรแกรมยุคเก่าที่ใช้สอนพื้นฐาน) ซึ่งช่วยให้เราเขียน Compiler จัดการกับคำสั่งเหล่านี้ได้โดยไม่หลงทาง
ลองดูตัวอย่างโค้ดของภาษา Frog ที่เราจะใช้สร้าง Compiler กัน:
FRG_Begin
FRG_Int i, j
i := 10
# ถ้า i น้อยกว่า 20 ให้พิมพ์ข้อความ
If [i < 20]
FRG_Print "Small number"
FRG_End
ในโค้ดนี้ FRG_Begin และ FRG_End คือตัวบอกจุดเริ่มต้นและจุดสิ้นสุดของโปรแกรม ส่วน # ใช้สำหรับเขียนคอมเมนต์ (คำอธิบายโค้ดที่คอมพิวเตอร์ไม่นำไปรัน) และ := ใช้สำหรับกำหนดค่าให้ตัวแปร
ผลลัพธ์ที่ได้จากการรันโค้ดนี้คือ หน้าจอจะแสดงข้อความว่า Small number ออกมา ซึ่งเป็นการยืนยันว่า Compiler ของเราสามารถประมวลผลคำสั่งตามที่เราเขียนไว้ได้อย่างถูกต้อง
ขั้นตอนที่ 1: Lexical Analysis เปลี่ยนตัวอักษรให้เป็นความหมาย
ขั้นตอนแรกของ Compiler คือ Lexical Analysis (การวิเคราะห์คำศัพท์) ซึ่งทำหน้าที่อ่านโค้ดทีละตัวอักษร แล้วจัดกลุ่มให้เป็นสิ่งที่เรียกว่า Token (หน่วยคำที่คอมพิวเตอร์เข้าใจ) เปรียบได้กับการที่คุณอ่านประโยค แล้วแยกแยะว่าคำไหนเป็นชื่อคน คำไหนเป็นกริยา
หน้าที่ของ Lexer (ตัวทำ Lexical Analysis) คือการมองข้ามสิ่งที่ไม่จำเป็น เช่น ช่องว่างหรือคอมเมนต์ และเปลี่ยนข้อความดิบให้เป็นชุดของข้อมูลที่เตรียมส่งต่อให้ขั้นตอนถัดไป เช่น เปลี่ยนจาก i := 10 เป็นชุดข้อมูลประเภท ID(i), ASSIGN, INTNUMBER(10)
หากคุณไม่ระวังในขั้นตอนนี้ คุณจะเจอปัญหาเรื่องการแยกประเภทข้อมูล เช่น คอมพิวเตอร์อาจสับสนระหว่างเลข 3 กับ 3.14 หากคุณไม่สอนให้มันรู้จักความต่างของจำนวนเต็มและทศนิยมตั้งแต่เนิ่นๆ
// ตัวอย่างการแยก Token แบบง่าย
const tokens = [
{ type: 'KEYWORD', value: 'FRG_Int' },
{ type: 'ID', value: 'i' },
{ type: 'ASSIGN', value: ':=' },
{ type: 'NUMBER', value: '10' }
];
ในตัวอย่างนี้ เราได้จัดกลุ่มข้อความต้นทางให้อยู่ในรูปของ Array (รายการข้อมูล) เพื่อให้โปรแกรมถัดไปนำไปใช้งานต่อได้ง่ายขึ้น โดยแต่ละก้อนข้อมูลจะมี type เพื่อบอกว่ามันคืออะไร และ value คือค่าของมันจริงๆ
ผลลัพธ์คือ คุณจะได้รายการ Token ที่สะอาดและเป็นระเบียบ ซึ่งจะช่วยให้ขั้นตอนการตรวจสอบกฎไวยากรณ์ในขั้นตอนถัดไปทำงานได้อย่างแม่นยำและไม่ผิดพลาดจากข้อความที่ปนเปกัน
ขั้นตอนที่ 2: Syntax Analysis การตรวจสอบกฎไวยากรณ์
เมื่อเราได้ Token มาแล้ว ขั้นตอนที่สองคือ Syntax Analysis (การวิเคราะห์ไวยากรณ์) หรือที่เรียกกันว่า Parser (ตัวตรวจสอบโครงสร้าง) หน้าที่ของมันคือการตรวจสอบว่าลำดับของ Token ที่ได้มานั้น ถูกต้องตามกฎของภาษา Frog หรือไม่
ลองนึกภาพว่า Parser คือครูสอนภาษาที่ดุมาก ถ้าคุณเขียนว่า If [i < 20 โดยลืมปิดวงเล็บ ] ครูคนนี้จะสั่งหยุดทันทีและแจ้ง Error เพราะโครงสร้างประโยคไม่เป็นไปตามที่ตกลงกันไว้ตั้งแต่แรก
หัวใจสำคัญของขั้นตอนนี้คือการเขียนฟังก์ชันแบบ Recursive (การเรียกตัวเองซ้ำๆ) เพื่อจัดการกับนิพจน์ทางคณิตศาสตร์ที่ซับซ้อน เช่น x + 5 * 2 ซึ่งคอมพิวเตอร์ต้องเข้าใจว่าต้องคูณก่อนบวก หาก Parser ทำงานผิดพลาด โปรแกรมของคุณก็จะไม่สามารถทำงานได้เลย
function parseExpression() {
// ตรวจสอบโครงสร้างว่าสอดคล้องกับกฎของภาษาหรือไม่
if (currentToken.type === 'ID') {
// ดำเนินการตรวจสอบการประกาศตัวแปร
}
}
ฟังก์ชันนี้ทำหน้าที่ตรวจสอบว่าถ้าเจอชื่อตัวแปร จะต้องมีเครื่องหมายกำหนดค่าตามมาเสมอ หากไม่พบข้อมูลตามที่คาดหวัง ระบบจะหยุดทำงานทันทีเพื่อป้องกันการรันโค้ดที่ผิดพลาด
ผลลัพธ์ที่ได้จากการผ่านขั้นตอน Parser คือ AST (โครงสร้างข้อมูลแบบต้นไม้ที่แทนลำดับการทำงาน) ซึ่งคอมพิวเตอร์สามารถไล่ลำดับการทำงานจากกิ่งก้านของต้นไม้ไปจนจบโปรแกรมได้อย่างเป็นระบบ
ขั้นตอนที่ 3: Semantic Analysis และการรันโปรแกรม
ขั้นตอนสุดท้ายคือ Semantic Analysis (การวิเคราะห์ความหมาย) ซึ่งเป็นการตรวจสอบว่าสิ่งที่เขียนนั้น "สมเหตุสมผล" ไหม เช่น คุณพยายามใช้งานตัวแปรที่ยังไม่ได้ประกาศ หรือพยายามเอาตัวเลขไปบวกกับข้อความ ซึ่งเป็นเรื่องที่ยอมรับไม่ได้
ในขั้นตอนนี้ เราจะใช้ Symbol Table (ตารางเก็บข้อมูลตัวแปร) เพื่อจดบันทึกว่าในโปรแกรมนี้มีตัวแปรชื่ออะไรบ้าง ชนิดข้อมูลเป็นแบบไหน และมีค่าปัจจุบันเท่าไหร่ หากมีการใช้ตัวแปรที่ไม่มีในตาราง ระบบจะแจ้งเตือนว่าตัวแปรนั้นยังไม่ได้ถูกนิยาม
นอกจากนี้ ขั้นตอนนี้ยังทำหน้าที่รันโค้ดจริงโดยการวนลูปอ่านคำสั่งทีละบรรทัดจาก AST ที่สร้างไว้ ถ้าเป็นคำสั่ง If ระบบจะตรวจสอบเงื่อนไข ถ้าเป็น Repeat ระบบจะวนซ้ำจนกว่าเงื่อนไขจะเป็นจริง โดยเราต้องระวังไม่ให้เกิดการวนลูปไม่รู้จบ
// ตรวจสอบค่าในตารางก่อนประมวลผล
if (!symbolTable.has(variableName)) {
throw new Error("ตัวแปรนี้ยังไม่ได้ประกาศใช้งาน");
}
บรรทัดนี้คือการเช็คความปลอดภัยก่อนนำตัวแปรไปคำนวณ ถ้าไม่มีตัวแปรในตาราง โปรแกรมจะแจ้งเตือนผู้ใช้ทันที เพื่อไม่ให้เกิดข้อผิดพลาดขณะรันจริง
ผลลัพธ์คือโปรแกรมจะทำงานตามตรรกะที่คุณวางไว้ เช่น การแสดงผลลัพธ์ออกมาทางหน้าจอ หรือการเปลี่ยนค่าของตัวแปรตามเงื่อนไขที่กำหนดไว้ในโค้ด Frog ของคุณ
สรุป: สิ่งที่ได้จากการสร้าง Compiler ด้วยตัวเอง
การสร้าง Compiler ขนาดเล็กช่วยให้คุณเข้าใจการทำงานของซอฟต์แวร์ในระดับที่ลึกซึ้งกว่าเดิม คุณจะเลิกกลัวข้อความ Error ที่ขึ้นบนหน้าจอ เพราะคุณเข้าใจแล้วว่ามันเกิดจากการที่ Parser หรือ Semantic Analyzer ตรวจพบความผิดปกติในโค้ดของคุณ
ถ้าคุณอยากเก่งขึ้น ลองเริ่มจากโปรเจกต์เล็กๆ แบบนี้ดูครับ เลือกภาษาที่คุณใช้ประจำ แล้วลองสร้างเครื่องมือที่อ่านโค้ดของภาษานั้นสัก 5-6 ฟีเจอร์พื้นฐาน เช่น การประกาศตัวแปร การบวกเลข และการใช้เงื่อนไข if ซึ่งจะทำให้คุณเห็นภาพชัดเจนขึ้นว่าโครงสร้างโปรแกรมที่แท้จริงเป็นอย่างไร
หัวใจสำคัญคือการลงมือทำ ไม่จำเป็นต้องใช้เครื่องมือที่ซับซ้อน เพียงแค่ใช้ภาษาที่คุณถนัดอย่าง TypeScript หรือ Python ก็เพียงพอแล้ว การได้ลองเขียน Compiler ด้วยตัวเองจะเปลี่ยนคุณจากคนที่แค่ "เขียนโค้ดตามตัวอย่าง" ให้กลายเป็นคนที่ "เข้าใจวิธีที่คอมพิวเตอร์คิด" อย่างแท้จริงครับ
ที่มา: 🐸 I Built a Mini Compiler From Scratch, Here's What I Learned — DEV Community