การสร้างภาษา JVM ที่ใช้งานได้: ภาพรวม
เผยแพร่แล้ว: 2022-03-11มีเหตุผลที่เป็นไปได้หลายประการสำหรับการสร้างภาษา ซึ่งบางสาเหตุอาจไม่ชัดเจนในทันที ฉันต้องการนำเสนอพร้อมกับวิธีการสร้างภาษาสำหรับ Java Virtual Machine (JVM) โดยใช้เครื่องมือที่มีอยู่ซ้ำให้มากที่สุด ด้วยวิธีนี้ เราจะลดความพยายามในการพัฒนาและจัดเตรียม toolchain ที่ผู้ใช้คุ้นเคย ทำให้ง่ายต่อการปรับใช้ภาษาการเขียนโปรแกรมใหม่ของเรา
ในบทความนี้ ซึ่งเป็นบทความชุดแรก ฉันจะนำเสนอภาพรวมของกลยุทธ์และเครื่องมือต่างๆ ที่เกี่ยวข้องในการสร้างภาษาการเขียนโปรแกรมของเราเองสำหรับ JVM ในบทความต่อๆ ไป เราจะเจาะลึกรายละเอียดการใช้งาน
ทำไมต้องสร้างภาษา JVM ของคุณ?
ภาษาโปรแกรมมีอยู่แล้วนับไม่ถ้วน เหตุใดจึงต้องสร้างใหม่ มีคำตอบที่เป็นไปได้มากมายสำหรับสิ่งนั้น
อย่างแรกเลย มีภาษาต่างๆ มากมาย: คุณต้องการสร้างภาษาโปรแกรมทั่วไป (GPL) หรือภาษาเฉพาะโดเมนหรือไม่? ประเภทแรกรวมถึงภาษาเช่น Java หรือ Scala: ภาษาที่มีจุดประสงค์เพื่อเขียนวิธีแก้ปัญหาที่เหมาะสมเพียงพอสำหรับปัญหาชุดใหญ่ Domain Specific Languages (DSL) แทนที่จะมุ่งเน้นไปที่การแก้ปัญหาเฉพาะชุด ลองนึกถึง HTML หรือลาเท็กซ์: คุณสามารถวาดบนหน้าจอหรือสร้างเอกสารใน Java ได้ แต่มันจะยุ่งยาก ด้วย DSL เหล่านี้แทน คุณสามารถสร้างเอกสารได้ง่ายมากแต่จำกัดเฉพาะโดเมนนั้น
ดังนั้น อาจมีชุดของปัญหาที่คุณทำงานบ่อยมากและสำหรับการสร้าง DSL ที่เหมาะสม ภาษาที่จะทำให้คุณทำงานได้อย่างมีประสิทธิผลมากพร้อมๆ กับแก้ปัญหาประเภทเดียวกันซ้ำแล้วซ้ำเล่า
บางทีคุณอาจต้องการสร้าง GPL แทนเพราะคุณมีแนวคิดใหม่ๆ เช่น เพื่อแสดงความสัมพันธ์ในฐานะพลเมืองชั้นหนึ่งหรือแสดงบริบท
สุดท้ายนี้ คุณอาจต้องการสร้างภาษาใหม่เพราะมันสนุก เจ๋ง และเพราะว่าคุณจะได้เรียนรู้มากมายในกระบวนการนี้
ความจริงก็คือหากคุณกำหนดเป้าหมาย JVM คุณจะได้รับภาษาที่ใช้งานได้โดยใช้ความพยายามน้อยลง นั่นเป็นเพราะ:
- คุณเพียงแค่ต้องสร้าง bytecode และรหัสของคุณจะพร้อมใช้งานบนทุกแพลตฟอร์มที่มี JVM
- คุณจะสามารถใช้ประโยชน์จากไลบรารีและเฟรมเวิร์กทั้งหมดที่มีอยู่สำหรับ JVM
ดังนั้นต้นทุนในการพัฒนาภาษาจึงลดลงอย่างมากใน JVM และการสร้างภาษาใหม่ในสถานการณ์ที่จะไม่ประหยัดนอก JVM อาจเป็นเรื่องที่สมเหตุสมผล
คุณต้องการอะไรเพื่อให้ใช้งานได้?
มีเครื่องมือบางอย่างที่คุณจำเป็นต้องใช้จริงๆ เพื่อใช้ภาษาของคุณ - parser และ compiler (หรือ interpreter) เป็นหนึ่งในเครื่องมือเหล่านี้ อย่างไรก็ตาม นี่ยังไม่พอ ในการทำให้ภาษาของคุณใช้งานได้จริง คุณต้องจัดเตรียมส่วนประกอบอื่นๆ ของห่วงโซ่เครื่องมือ ซึ่งอาจรวมเข้ากับเครื่องมือที่มีอยู่ได้
คุณต้องการให้สามารถ:
- จัดการการอ้างอิงถึงโค้ดที่คอมไพล์สำหรับ JVM จากภาษาอื่น
- แก้ไขไฟล์ต้นฉบับใน IDE ที่คุณชื่นชอบด้วยการเน้นไวยากรณ์ การระบุข้อผิดพลาด และการเติมข้อความอัตโนมัติ
- คุณต้องการคอมไพล์ไฟล์โดยใช้ระบบบิลด์ที่คุณชื่นชอบ: maven, gradle หรืออื่น ๆ
- คุณต้องการเขียนการทดสอบและเรียกใช้การทดสอบโดยเป็นส่วนหนึ่งของโซลูชันการรวมต่อเนื่องของคุณ
ถ้าคุณทำได้ การนำภาษาของคุณไปใช้จะง่ายขึ้นมาก
แล้วเราจะบรรลุสิ่งนั้นได้อย่างไร? ในบทความที่เหลือ เราจะตรวจสอบส่วนต่างๆ ที่เราจำเป็นต้องทำเพื่อให้เป็นไปได้
การแยกวิเคราะห์และคอมไพล์
สิ่งแรกที่คุณต้องทำเพื่อแปลงไฟล์ต้นฉบับของคุณในโปรแกรมคือการแยกวิเคราะห์ เพื่อให้ได้ข้อมูลที่แสดงในโค้ดของ Abstract-Syntax-Tree (AST) ณ จุดนั้น คุณจะต้องตรวจสอบโค้ด: มีข้อผิดพลาดทางไวยากรณ์หรือไม่? ข้อผิดพลาดทางความหมาย? คุณต้องค้นหาทั้งหมดและรายงานให้ผู้ใช้ทราบ หากทุกอย่างดำเนินไปอย่างราบรื่น คุณยังต้องแก้ไขสัญลักษณ์ ตัวอย่างเช่น “List” หมายถึง java.util.List หรือ java.awt.List หรือไม่ เมื่อคุณเรียกใช้วิธีการโอเวอร์โหลด คุณจะเรียกใช้วิธีใด สุดท้าย คุณต้องสร้าง bytecode สำหรับโปรแกรมของคุณ
ดังนั้น จากซอร์สโค้ดไปจนถึง bytecode ที่คอมไพล์แล้ว มีสามขั้นตอนหลัก:
- การสร้าง AST
- การวิเคราะห์และการแปลง AST
- การสร้าง bytecode จาก AST
เรามาดูขั้นตอนเหล่านั้นในรายละเอียดกัน
การสร้าง AST : การแยกวิเคราะห์เป็นปัญหาที่แก้ไขได้ มีเฟรมเวิร์กมากมาย แต่ฉันแนะนำให้คุณใช้ ANTLR เป็นที่รู้จักกันดี ได้รับการดูแลอย่างดี และมีคุณสมบัติบางอย่างที่ทำให้ระบุไวยากรณ์ได้ง่ายขึ้น (จัดการกฎที่เรียกซ้ำน้อยกว่า - คุณไม่จำเป็นต้องเข้าใจสิ่งนั้น แต่ต้องขอบคุณที่ทำเช่นนั้น!)
การวิเคราะห์และการแปลง AST : การเขียนระบบประเภท การตรวจสอบความถูกต้อง และการแก้ไขสัญลักษณ์อาจเป็นเรื่องที่ท้าทายและต้องอาศัยการทำงานค่อนข้างมาก หัวข้อนี้เพียงอย่างเดียวจะต้องมีการโพสต์แยกต่างหาก สำหรับตอนนี้ ให้พิจารณาว่านี่เป็นส่วนหนึ่งของคอมไพเลอร์ของคุณซึ่งคุณจะต้องใช้ความพยายามส่วนใหญ่
การสร้าง bytecode จาก AST : ขั้นตอนสุดท้ายนี้ไม่ยากเลย คุณควรแก้ไขสัญลักษณ์ในเฟสก่อนหน้าและเตรียมภูมิประเทศเพื่อให้โดยพื้นฐานแล้วคุณสามารถแปลโหนดเดียวของ AST ที่แปลงเป็นคำสั่ง bytecode หนึ่งหรือสองสามคำสั่ง โครงสร้างการควบคุมอาจต้องการการทำงานพิเศษบางอย่าง เนื่องจากคุณจะต้องแปล for-loop, สวิตช์, ifs และอื่นๆ ตามลำดับของการข้ามแบบมีเงื่อนไขและไม่มีเงื่อนไข (ใช่ ด้านล่างของภาษาที่สวยงามของคุณยังมี gotos จำนวนมาก) คุณต้องเรียนรู้ว่า JVM ทำงานอย่างไรภายใน แต่การใช้งานจริงไม่ได้ยากขนาดนั้น
บูรณาการกับภาษาอื่นๆ
เมื่อคุณจะได้ครอบครองโลกสำหรับภาษาของคุณ รหัสทั้งหมดจะถูกเขียนขึ้นโดยใช้มันเท่านั้น อย่างไรก็ตาม ในฐานะที่เป็นขั้นตอนกลาง ภาษาของคุณอาจจะถูกใช้พร้อมกับภาษา JVM อื่นๆ บางทีอาจมีคนเริ่มเขียนสองสามชั้นเรียนหรือโมดูลเล็ก ๆ ในภาษาของคุณในโครงการที่ใหญ่ขึ้น มีเหตุผลที่จะคาดหวังว่าจะสามารถผสมภาษา JVM ได้หลายภาษา แล้วมันส่งผลต่อเครื่องมือภาษาของคุณอย่างไร?
คุณต้องพิจารณาสองสถานการณ์ที่แตกต่างกัน:
- ภาษาของคุณและภาษาอื่นๆ อยู่ในโมดูลที่รวบรวมแยกจากกัน
- ภาษาของคุณและภาษาอื่นๆ อยู่ในโมดูลเดียวกันและรวบรวมไว้ด้วยกัน
ในสถานการณ์แรก โค้ดของคุณจะต้องใช้โค้ดที่คอมไพล์แล้วซึ่งเขียนในภาษาอื่นเท่านั้น ตัวอย่างเช่น การขึ้นต่อกันบางอย่าง เช่น Guava หรือโมดูลในโปรเจ็กต์เดียวกัน สามารถคอมไพล์แยกกันได้ การรวมประเภทนี้ต้องการสองสิ่ง: อันดับแรก คุณควรจะสามารถตีความไฟล์คลาสที่สร้างโดยภาษาอื่นเพื่อแก้ไขสัญลักษณ์ให้กับพวกมัน และสร้าง bytecode สำหรับการเรียกใช้คลาสเหล่านั้น จุดที่สองมีลักษณะเฉพาะกับจุดแรก: โมดูลอื่นๆ อาจต้องการนำรหัสที่เขียนในภาษาของคุณกลับมาใช้ใหม่หลังจากที่คอมไพล์แล้ว ปกติแล้วนั่นไม่ใช่ปัญหาเพราะ Java สามารถโต้ตอบกับไฟล์คลาสส่วนใหญ่ได้ อย่างไรก็ตาม คุณยังคงสามารถเขียนไฟล์คลาสที่ถูกต้องสำหรับ JVM แต่ไม่สามารถเรียกใช้จาก Java ได้ (เช่น เนื่องจากคุณใช้ตัวระบุที่ไม่ถูกต้องใน Java)

สถานการณ์ที่สองซับซ้อนกว่า: สมมติว่าคุณมีคลาส A ที่กำหนดไว้ในโค้ด Java และคลาส B เขียนด้วยภาษาของคุณ สมมติว่าทั้งสองคลาสอ้างอิงถึงกัน (เช่น A สามารถขยาย B และ B สามารถยอมรับ A เป็นพารามิเตอร์สำหรับวิธีการเดียวกัน) ประเด็นคือคอมไพเลอร์ Java ไม่สามารถประมวลผลโค้ดในภาษาของคุณได้ ดังนั้นคุณต้องจัดเตรียมไฟล์คลาสสำหรับคลาส B ให้กับมัน อย่างไรก็ตาม ในการคอมไพล์คลาส B คุณต้องแทรกการอ้างอิงถึงคลาส A ดังนั้นสิ่งที่คุณต้องทำคือ เพื่อให้มีคอมไพเลอร์ Java บางส่วนซึ่งให้ซอร์สไฟล์ Java สามารถตีความและสร้างแบบจำลองซึ่งคุณสามารถใช้เพื่อคอมไพล์คลาส B ของคุณ โปรดทราบว่าสิ่งนี้ต้องการให้คุณแยกวิเคราะห์โค้ด Java (โดยใช้ บางอย่างเช่น JavaParser) และแก้สัญลักษณ์ หากคุณไม่รู้ว่าจะเริ่มต้นจากตรงไหน ให้ดูที่ java-symbol-solver
เครื่องมือ: Gradle, Maven, กรอบการทดสอบ, CI
ข่าวดีก็คือคุณสามารถสร้างความจริงที่ว่าพวกเขากำลังใช้โมดูลที่เขียนด้วยภาษาของคุณที่โปร่งใสสำหรับผู้ใช้โดยการพัฒนาปลั๊กอินสำหรับ gradle หรือ maven คุณสามารถสั่งให้ระบบสร้างคอมไพล์ไฟล์ในภาษาการเขียนโปรแกรมของคุณ ผู้ใช้จะยังคงทำงาน mvn compile หรือ gradle assemble และไม่สังเกตเห็นความแตกต่างใด ๆ
ข่าวร้ายคือการเขียนปลั๊กอิน Maven ไม่ใช่เรื่องง่าย: เอกสารประกอบไม่ดีนัก ไม่เข้าใจ และส่วนใหญ่ล้าสมัยหรือ ผิดพลาด ใช่มันฟังดูไม่สบายใจ ฉันยังไม่ได้เขียนปลั๊กอิน gradle แต่ดูเหมือนง่ายกว่ามาก
โปรดทราบว่าคุณควรพิจารณาด้วยว่าการทดสอบสามารถรันโดยใช้ระบบบิลด์ได้อย่างไร สำหรับการทดสอบที่สนับสนุน คุณควรนึกถึงเฟรมเวิร์กพื้นฐานสำหรับการทดสอบหน่วย และคุณควรรวมเข้ากับระบบบิลด์ เพื่อให้การทดสอบ maven ทำงาน ค้นหาการทดสอบในภาษาของคุณ คอมไพล์และเรียกใช้รายงานเอาต์พุตไปยังผู้ใช้
คำแนะนำของฉันคือการดูตัวอย่างที่มี: หนึ่งในนั้นคือปลั๊กอิน Maven สำหรับภาษาโปรแกรม Turin
เมื่อคุณใช้งานแล้ว ทุกคนควรจะสามารถรวบรวมไฟล์ต้นฉบับที่เขียนด้วยภาษาของคุณได้อย่างง่ายดาย และใช้ในบริการการรวมอย่างต่อเนื่อง เช่น Travis
IDE Plugin
ปลั๊กอินสำหรับ IDE จะเป็นเครื่องมือที่มองเห็นได้ชัดเจนที่สุดสำหรับผู้ใช้ของคุณและเป็นสิ่งที่จะส่งผลต่อการรับรู้ภาษาของคุณอย่างมาก ปลั๊กอินที่ดีสามารถช่วยให้ผู้ใช้เรียนรู้ภาษาได้ด้วยการเติมข้อมูลอัตโนมัติอัจฉริยะ ข้อผิดพลาดตามบริบท และการปรับโครงสร้างใหม่ที่แนะนำ
ตอนนี้ กลยุทธ์ที่พบบ่อยที่สุดคือการเลือก IDE หนึ่งตัว (โดยทั่วไปคือ Eclipse หรือ IntelliJ IDEA) และพัฒนาปลั๊กอินเฉพาะสำหรับมัน นี่อาจเป็นส่วนที่ซับซ้อนที่สุดของ toolchain ของคุณ กรณีนี้เกิดจากหลายสาเหตุ: อย่างแรกเลย คุณไม่สามารถนำงานกลับมาใช้ใหม่ได้อย่างสมเหตุสมผล ซึ่งคุณจะต้องใช้ในการพัฒนาปลั๊กอินสำหรับ IDE ตัวหนึ่งสำหรับอีกตัวหนึ่ง Eclipse และปลั๊กอิน IntelliJ ของคุณจะถูกแยกออกจากกันโดยสิ้นเชิง จุดที่สองคือการพัฒนาปลั๊กอิน IDE เป็นสิ่งที่ไม่ธรรมดา ดังนั้นจึงมีเอกสารไม่มากและชุมชนมีขนาดเล็ก หมายความว่าคุณจะต้องใช้เวลามากมายในการค้นหาสิ่งต่าง ๆ สำหรับตัวคุณเอง ฉันได้พัฒนาปลั๊กอินสำหรับ Eclipse และ IntelliJ IDEA เป็นการส่วนตัว คำถามของฉันในฟอรัม Eclipse ยังไม่ได้รับคำตอบเป็นเวลาหลายเดือนหรือหลายปี ในฟอรัม IntelliJ ฉันโชคดีกว่า และบางครั้งฉันก็ได้รับคำตอบจากนักพัฒนา อย่างไรก็ตาม ฐานผู้ใช้ของนักพัฒนาปลั๊กอินนั้นเล็กกว่าและ API นั้นมีความไบแซนไทน์มาก เตรียมรับความทุกข์.
มีทางเลือกอื่นสำหรับสิ่งเหล่านี้ และใช้ Xtext Xtext เป็นเฟรมเวิร์กสำหรับการพัฒนาปลั๊กอินสำหรับ Eclipse, IntelliJ IDEA และเว็บ มันเกิดขึ้นบน Eclipse และเพิ่งได้รับการขยายเพื่อรองรับแพลตฟอร์มอื่น ๆ ดังนั้นจึงไม่มีประสบการณ์มากนัก แต่อาจเป็นทางเลือกที่คุ้มค่าที่จะพิจารณา ให้ฉันพูดตรงๆ วิธีเดียวในการพัฒนาปลั๊กอินที่ดีคือการพัฒนาโดยใช้ Native API ของแต่ละ IDE อย่างไรก็ตาม ด้วย Xtext คุณสามารถมีสิ่งที่ดีพอสมควรโดยใช้ความพยายามเพียงเศษเสี้ยว - คุณเพียงแค่ใส่มันให้กับไวยากรณ์ของภาษาของคุณและคุณจะได้รับข้อผิดพลาด / การเติมเต็มทางไวยากรณ์ฟรี ถึงกระนั้น คุณต้องใช้ความละเอียดของสัญลักษณ์และส่วนที่ยาก แต่นี่เป็นจุดเริ่มต้นที่น่าสนใจมาก อย่างไรก็ตาม ฮาร์ดบิตเป็นการผสานรวมกับไลบรารีเฉพาะของแพลตฟอร์มเพื่อแก้ไขสัญลักษณ์ Java ดังนั้นจึงไม่สามารถแก้ปัญหาทั้งหมดของคุณได้
บทสรุป
มีหลายวิธีที่คุณอาจสูญเสียผู้ที่มีแนวโน้มจะเป็นผู้ใช้ที่แสดงความสนใจในภาษาของคุณ การใช้ภาษาใหม่เป็นสิ่งที่ท้าทายเพราะต้องเรียนรู้และปรับนิสัยการพัฒนาของเรา การลดทอนให้ได้มากที่สุดและใช้ประโยชน์จากระบบนิเวศที่ผู้ใช้รู้จักอยู่แล้ว จะทำให้ผู้ใช้ไม่ยอมแพ้ก่อนที่พวกเขาจะเรียนรู้และตกหลุมรักภาษาของคุณ
ในสถานการณ์ที่เหมาะสม ผู้ใช้ของคุณสามารถโคลนโปรเจ็กต์ง่ายๆ ที่เขียนด้วยภาษาของคุณ และสร้างโดยใช้เครื่องมือมาตรฐาน (Maven หรือ Gradle) โดยไม่สังเกตเห็นความแตกต่างใดๆ ถ้าเขาต้องการแก้ไขโครงการ เขาสามารถเปิดมันในโปรแกรมแก้ไขที่ชื่นชอบ และปลั๊กอินจะช่วยชี้ให้เขาเห็นข้อผิดพลาดและให้ความสำเร็จที่ชาญฉลาด นี่เป็นสถานการณ์ที่แตกต่างจากการต้องหาวิธีเรียกใช้คอมไพเลอร์และแก้ไขไฟล์โดยใช้แผ่นจดบันทึก ระบบนิเวศรอบ ๆ ภาษาของคุณสามารถสร้างความแตกต่างได้อย่างแท้จริง และทุกวันนี้ก็สามารถสร้างได้ด้วยความพยายามที่สมเหตุสมผล
คำแนะนำของฉันคือการสร้างสรรค์ในภาษาของคุณ แต่ไม่ใช่ในเครื่องมือของคุณ ลดปัญหาเบื้องต้นที่ผู้คนต้องเผชิญเพื่อนำภาษาของคุณไปใช้โดยใช้มาตรฐานที่คุ้นเคย
ออกแบบภาษาอย่างมีความสุข!
อ่านเพิ่มเติมในบล็อก Toptal Engineering:
- วิธีการเขียนล่ามตั้งแต่เริ่มต้น
