การเขียนคอมไพเลอร์ในภาษาของตัวเอง


204

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

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



นี่เป็นคำถามที่เก่ามาก แต่บอกว่าฉันเขียนล่ามภาษา Foo ใน Java จากนั้นด้วยภาษาฟูฉันเขียนมันเป็นของตัวเอง Foo ยังต้องการ JRE อยู่ใช่ไหม?
George Xavier

คำตอบ:


231

สิ่งนี้เรียกว่า "bootstrapping" คุณต้องสร้างคอมไพเลอร์ (หรือล่าม) สำหรับภาษาของคุณในภาษาอื่น ๆ (โดยปกติคือ Java หรือ C) เมื่อเสร็จแล้วคุณสามารถเขียนคอมไพเลอร์เวอร์ชั่นใหม่เป็นภาษาฟู คุณใช้คอมไพเลอร์ bootstrap แรกเพื่อคอมไพเลอร์คอมไพเลอร์แล้วใช้คอมไพเลอร์คอมไพล์นี้เพื่อคอมไพล์ทุกสิ่งทุกอย่าง (รวมถึงเวอร์ชันในอนาคตของตัวเอง)

ภาษาส่วนใหญ่ถูกสร้างขึ้นในลักษณะนี้บางส่วนเนื่องจากนักออกแบบภาษาต้องการใช้ภาษาที่พวกเขาสร้างขึ้นและเพราะคอมไพเลอร์ที่ไม่สำคัญมักใช้เป็นเกณฑ์มาตรฐานที่มีประโยชน์สำหรับภาษาที่ "สมบูรณ์"

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


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

2
@piertoni ปกติแล้วมันจะง่ายกว่าที่จะเพียงแค่กำหนดแบ็กเอนด์คอมไพเลอร์กลับไปที่ไมโครโปรเซสเซอร์ใหม่
bstpierre

ใช้ LLVM เป็นแบ็คเอนด์ตัวอย่างเช่น

76

ฉันจำได้ว่าเคยฟังพอดคาสต์วิทยุวิศวกรรมซอฟต์แวร์ซึ่งดิ๊กกาเบรียลพูดเกี่ยวกับการแปลความหมาย LISP ดั้งเดิมโดยการเขียนเวอร์ชันกระดูกเปลือยใน LISP ลงบนกระดาษและมือประกอบลงในรหัสเครื่อง ตั้งแต่นั้นมาคุณสมบัติ LISP ที่เหลือถูกเขียนและตีความด้วย LISP


ทุกอย่างถูก bootstrapped จากทรานซิสเตอร์แหล่งกำเนิดที่มีมือมากมาย

47

การเพิ่มความอยากรู้อยากเห็นให้กับคำตอบก่อนหน้า

นี่คือคำพูดจากคู่มือLinux ตั้งแต่เริ่มต้นในขั้นตอนที่หนึ่งเริ่มสร้างคอมไพเลอร์ GCC จากแหล่งที่มา (Linux From Scratch เป็นวิธีการติดตั้ง Linux ที่แตกต่างอย่างสิ้นเชิงจากการติดตั้งการแจกจ่ายซึ่งคุณต้องรวบรวมจริง ๆทุก ๆไบนารีของระบบเป้าหมาย)

make bootstrap

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

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


12
"คุณต้องรวบรวมจริง ๆ ทุก ๆ ไบนารีของระบบเป้าหมาย" และคุณต้องเริ่มต้นด้วยไบนารี gcc ที่คุณได้จากที่ไหนสักแห่งเพราะแหล่งไม่สามารถรวบรวมตัวเองได้ ฉันสงสัยว่าคุณติดตามสายเลือดของไบนารี gcc แต่ละตัวที่ใช้ในการคอมไพล์ต่อเนื่อง gcc แต่ละครั้งคุณจะได้รับกลับไปที่คอมไพเลอร์ C ดั้งเดิมของ K & R หรือไม่?
robru

43

เมื่อคุณเขียนคอมไพเลอร์ตัวแรกสำหรับ C คุณเขียนมันเป็นภาษาอื่น ตอนนี้คุณมีคอมไพเลอร์สำหรับ C ใน, พูด, แอสเซมเบลอร์ ในที่สุดคุณจะมาถึงสถานที่ที่คุณต้องแยกสตริงโดยเฉพาะลำดับหนี คุณจะเขียนโค้ดเพื่อแปลง\nเป็นอักขระด้วยรหัสทศนิยม 10 (และ\rถึง 13 เป็นต้น)

หลังจากคอมไพเลอร์นั้นพร้อมแล้วคุณจะเริ่มนำมันไปใช้ใหม่ใน C กระบวนการนี้เรียกว่า " bootstrapping "

รหัสการแยกวิเคราะห์สตริงจะกลายเป็น:

...
if (c == 92) { // backslash
    c = getc();
    if (c == 110) { // n
        return 10;
    } else if (c == 92) { // another backslash
        return 92;
    } else {
        ...
    }
}
...

เมื่อคอมไพล์คุณมีไบนารี่ที่เข้าใจ '\ n' หมายความว่าคุณสามารถเปลี่ยนรหัสต้นฉบับได้:

...
if (c == '\\') {
    c = getc();
    if (c == 'n') {
        return '\n';
    } else if (c == '\\') {
        return '\\';
    } else {
        ...
    }
}
...

ดังนั้นข้อมูลที่ '\ n' คือรหัสสำหรับ 13? มันอยู่ในไบนารี! มันก็เหมือนกับ DNA: การคอมไพล์ซอร์สโค้ด C ด้วยไบนารีนี้จะสืบทอดข้อมูลนี้ หากคอมไพเลอร์รวบรวมตัวเองมันจะถ่ายทอดความรู้นี้ไปยังลูกหลานของมัน จากจุดนี้เป็นต้นไปไม่มีวิธีการดูจากแหล่งเดียวสิ่งที่คอมไพเลอร์จะทำ

หากคุณต้องการซ่อนไวรัสในซอร์สของบางโปรแกรมคุณสามารถทำได้ดังนี้: รับซอร์สของคอมไพเลอร์, ค้นหาฟังก์ชันที่รวบรวมฟังก์ชั่นและแทนที่ด้วยอันนี้:

void compileFunction(char * name, char * filename, char * code) {
    if (strcmp("compileFunction", name) == 0 && strcmp("compile.c", filename) == 0) {
        code = A;
    } else if (strcmp("xxx", name) == 0 && strcmp("yyy.c", filename) == 0) {
        code = B;
    }

    ... code to compile the function body from the string in "code" ...
}

ชิ้นส่วนที่น่าสนใจคือ A และ B. A เป็นซอร์สโค้ดสำหรับcompileFunctionรวมถึงไวรัสซึ่งอาจเข้ารหัสในบางวิธีดังนั้นจึงไม่ชัดเจนจากการค้นหาไบนารีที่เกิดขึ้น สิ่งนี้ทำให้แน่ใจว่าการคอมไพล์คอมไพเลอร์ด้วยตัวเองจะรักษารหัสการฉีดไวรัส

B เหมือนกันสำหรับฟังก์ชั่นที่เราต้องการแทนที่ด้วยไวรัสของเรา ตัวอย่างเช่นอาจเป็นฟังก์ชัน "เข้าสู่ระบบ" ในไฟล์ต้นฉบับ "login.c" ซึ่งอาจมาจากเคอร์เนล Linux เราสามารถแทนที่ด้วยรุ่นที่จะยอมรับรหัสผ่าน "joshua" สำหรับบัญชีรูทนอกเหนือจากรหัสผ่านปกติ

ถ้าคุณคอมไพล์มันและแพร่กระจายมันเป็นไบนารี่, มันจะไม่มีทางที่จะหาไวรัสได้โดยดูจากแหล่งที่มา

แหล่งความคิดดั้งเดิม: https://web.archive.org/web/20070714062657/http://www.acm.org/classics/sep95/


1
ในช่วงครึ่งปีหลังเกี่ยวกับการเขียนคอมไพเลอร์ไวรัสที่รบกวน :)
mhvelplund

3
@mhvelplund เพียงแค่กระจายความรู้ว่า bootstrapping สามารถฆ่าคุณได้อย่างไร
Aaron Digulla

19

คุณไม่สามารถเขียนคอมไพเลอร์ในตัวเองเพราะคุณไม่มีอะไรจะรวบรวมซอร์สโค้ดเริ่มต้นของคุณด้วย มีสองวิธีในการแก้ปัญหานี้

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

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

ความจริงที่รู้จักกันน้อยคือคอมไพเลอร์ GNU C ++ มีการใช้งานที่ใช้เฉพาะชุดย่อย C สาเหตุที่มักจะเป็นเรื่องง่ายที่จะหาคอมไพเลอร์ C สำหรับเครื่องเป้าหมายใหม่ที่ช่วยให้คุณสร้างคอมไพเลอร์ GNU C ++ แบบเต็มจากมันได้ ตอนนี้คุณบูตเครื่องแล้วมัดตัวเองให้มีคอมไพเลอร์ C ++ บนเครื่องเป้าหมาย


14

โดยทั่วไปคุณต้องมีการตัดการทำงาน (ถ้าเป็นแบบดั้งเดิม) ของคอมไพเลอร์ทำงานก่อน - จากนั้นคุณสามารถเริ่มคิดเกี่ยวกับการทำให้โฮสติ้งด้วยตนเอง นี่ถือเป็นความสำเร็จครั้งสำคัญในบางภาษา

จากสิ่งที่ผมจำได้จาก "ขาวดำ" ก็มีโอกาสที่พวกเขาจะต้องเพิ่มบางสิ่งที่จะสะท้อนให้เห็นถึงที่จะได้รับมันทำงาน: ทีมโมโนเก็บชี้ให้เห็นว่าบางสิ่งบางอย่างก็ไม่เป็นไปได้ด้วยReflection.Emit; แน่นอนทีม MS อาจพิสูจน์ว่าพวกเขาผิด

นี่เป็นข้อได้เปรียบที่แท้จริงบางประการ: เป็นการทดสอบหน่วยที่ค่อนข้างดีสำหรับผู้เริ่มต้น! และคุณมีเพียงภาษาเดียวที่ต้องกังวล (เช่นเป็นไปได้ที่ผู้เชี่ยวชาญ C # อาจไม่รู้จัก C ++ มาก แต่ตอนนี้เจ้าสามารถแก้ไขคอมไพเลอร์ C # ได้) แต่ฉันสงสัยว่าที่นี่ไม่มีความภาคภูมิใจในการทำงานระดับมืออาชีพที่นี่: พวกเขาเพียงต้องการให้ตนเองเป็นเจ้าภาพ

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


อัปเดต 1

ฉันเพิ่งดูวิดีโอของ Anders ที่ PDC และ (ประมาณหนึ่งชั่วโมง) เขาให้เหตุผลที่ถูกต้องมากกว่านี้ - ทั้งหมดเกี่ยวกับคอมไพเลอร์เป็นบริการ เพียงเพื่อบันทึก


4

นี่คือการถ่ายโอนข้อมูล (หัวข้อที่ยากต่อการค้นหาจริง):

นี่เป็นแนวคิดของPyPyและRubinius :

(ฉันคิดว่านี่อาจนำไปใช้กับForthแต่ฉันไม่รู้อะไรเลยเกี่ยวกับ Forth)


ลิงก์แรกไปยังบทความที่เกี่ยวข้องกับ Smalltalk ที่คาดคะเนกำลังชี้ไปยังหน้าเว็บโดยไม่มีข้อมูลที่เป็นประโยชน์และชัดเจนทันที
nbro

1

GNAT ซึ่งเป็นคอมไพเลอร์ GNU Ada ต้องการคอมไพเลอร์ Ada เพื่อสร้างขึ้นอย่างสมบูรณ์ นี่อาจเป็นความเจ็บปวดเมื่อทำการย้ายไปยังแพลตฟอร์มที่ไม่มีไบนารี่ไบนารีของ GNAT


1
ฉันไม่เห็นว่าทำไม ไม่มีกฎใดที่คุณต้อง bootstrap มากกว่าหนึ่งครั้ง (เช่นสำหรับทุก ๆ แพลตฟอร์มใหม่) คุณสามารถข้ามคอมไพล์กับอันปัจจุบันได้
Marco van de Voort

1

ที่จริงแล้วคอมไพเลอร์ส่วนใหญ่เขียนด้วยภาษาที่คอมไพล์ด้วยเหตุผลดังกล่าวข้างต้น

คอมไพเลอร์ bootstrap แรกมักเขียนใน C, C ++ หรือ Assembly


1

คอมไพเลอร์โครงการ C # Mono ได้รับ "โฮสต์ในตัวเอง" เป็นเวลานานตอนนี้ความหมายคือว่ามันถูกเขียนใน C # เอง

สิ่งที่ฉันรู้คือคอมไพเลอร์เริ่มต้นเป็นรหัส C บริสุทธิ์ แต่เมื่อคุณสมบัติ "พื้นฐาน" ของ ECMA ถูกนำไปใช้พวกเขาเริ่มเขียนคอมไพเลอร์ใหม่ใน C #

ฉันไม่ทราบถึงข้อดีของการเขียนคอมไพเลอร์ในภาษาเดียวกัน แต่ฉันแน่ใจว่าต้องทำอย่างน้อยด้วยคุณสมบัติที่ภาษาสามารถนำเสนอได้ (ตัวอย่างเช่น C ไม่รองรับการเขียนโปรแกรมเชิงวัตถุ) .

ท่านสามารถหาข้อมูลเพิ่มเติมได้ที่นี่


1

ฉันเขียน SLIC (ระบบของภาษาเพื่อการใช้งานคอมไพเลอร์) ในตัวของมันเอง จากนั้นรวบรวมมือเข้าสู่การชุมนุม SLIC มีมากเนื่องจากเป็นคอมไพเลอร์เดี่ยวของห้าภาษาย่อย:

  • โปรแกรมแปลภาษา SYNTAX Parser PPL
  • GENERATOR LISP 2 ภาษาที่สร้างรหัสการรวบรวมข้อมูลต้นไม้ PSEUDO
  • ISO ในลำดับ, รหัส PSEUDO, ภาษาการปรับให้เหมาะสม
  • มาโคร PSEUDO เช่นภาษาแอสเซมบลีที่ผลิตรหัส
  • MACHOP คำสั่งแอสเซมบลีของเครื่องกำหนดภาษา

SLIC ได้รับแรงบันดาลใจจาก CWIC (คอมไพเลอร์สำหรับการเขียนและการใช้คอมไพเลอร์) ซึ่งแตกต่างจากแพคเกจการพัฒนาคอมไพเลอร์ส่วนใหญ่ SLIC และ CWIC จ่าหน้าถึงการสร้างรหัสด้วยความเชี่ยวชาญภาษาเฉพาะโดเมน SLIC ขยายการสร้างรหัส CWICs โดยเพิ่มภาษาย่อย ISO, PSEUDO และ MACHOP เพื่อแยกเครื่องเป้าหมายออกจากภาษาของเครื่องกำเนิดต้นไม้ที่ตระเวน

LISP 2 ต้นไม้และรายการ

ระบบจัดการหน่วยความจำไดนามิกของภาษา LISP 2 เป็นองค์ประกอบสำคัญ รายการจะแสดงเป็นภาษาที่รวมอยู่ในวงเล็บเหลี่ยมส่วนประกอบของมันคั่นด้วยเครื่องหมายจุลภาคเช่นรายการสามองค์ประกอบ [a, b, c]

ต้นไม้:

     ADD
    /   \
  MPY     3
 /   \
5     x

ถูกแทนด้วยรายการซึ่งรายการแรกคือวัตถุโหนด:

[ADD,[MPY,5,x],3]

ต้นไม้จะปรากฏขึ้นโดยทั่วไปพร้อมโหนดที่แยกจากกันก่อนหน้ากิ่งก้าน:

ADD[MPY[5,x],3]

การแยกวิเคราะห์ด้วยฟังก์ชันตัวสร้าง LISP 2

ฟังก์ชั่นเครื่องกำเนิดไฟฟ้าเป็นชุดชื่อของ (unparse) => action> pairs ...

<NAME>(<unparse>)=><action>;
      (<unparse>)=><action>;
            ...
      (<unparse>)=><action>;

นิพจน์ที่ไม่ชัดเจนคือการทดสอบที่จับคู่รูปแบบต้นไม้และ / หรือชนิดของวัตถุที่แตกออกเป็นชิ้น ๆ และกำหนดส่วนเหล่านั้นให้กับตัวแปรท้องถิ่นที่จะประมวลผลโดยการดำเนินการตามขั้นตอน ชนิดของฟังก์ชั่นโอเวอร์โหลดที่รับอาร์กิวเมนต์ชนิดต่าง ๆ ยกเว้นการทดสอบ () => ... จะพยายามตามลำดับรหัส ความสำเร็จครั้งแรกในการดำเนินการ unparse ที่เกี่ยวข้อง นิพจน์ที่ unparse เป็นการทดสอบแยกชิ้นส่วน เพิ่ม [x, y] จับคู่กับต้นไม้เพิ่มสองกิ่งที่กำหนดสาขาให้กับตัวแปรท้องถิ่น x และ y การกระทำอาจเป็นการแสดงออกที่ง่ายหรือ. เริ่มต้น ... บล็อกรหัสที่ถูกผูกไว้ ฉันจะใช้บล็อก c style {... } วันนี้ การจับคู่ต้นไม้, [], กฎที่แยกกันอาจเรียกผู้สร้างผ่านผลลัพธ์ที่ส่งคืนไปยังแอ็คชัน:

expr_gen(ADD[expr_gen(x),expr_gen(y)])=> x+y;

expr_gen unparse ด้านบนตรงกับต้นไม้ ADD สองสาขา ภายในรูปแบบการทดสอบตัวสร้างอาร์กิวเมนต์เดี่ยวที่วางไว้ในกิ่งต้นไม้จะถูกเรียกพร้อมกับสาขานั้น รายการอาร์กิวเมนต์ของมันคือตัวแปรท้องถิ่นที่กำหนดวัตถุที่ส่งคืน ด้านบน unparse ระบุว่ามีสองสาขาคือ ADD tree disassembly, การเรียกซ้ำโดยกดแต่ละสาขาเพื่อ expr_gen การส่งคืนสาขาทางซ้ายวางลงในตัวแปรท้องถิ่น x ในทำนองเดียวกันสาขาขวาส่งผ่านไปยัง expr_gen กับ y วัตถุกลับ ด้านบนอาจเป็นส่วนหนึ่งของตัวประเมินนิพจน์ตัวเลข มีคุณสมบัติทางลัดที่เรียกว่าเวกเตอร์อยู่ด้านบนแทนสตริงของโหนดเวกเตอร์ของโหนดสามารถใช้กับเวกเตอร์ของการกระทำที่สอดคล้องกัน:

expr_gen(#node[expr_gen(x),expr_gen(y)])=> #action;

  node:   ADD, SUB, MPY, DIV;
  action: x+y, x-y, x*y, x/y;

        (NUMBER(x))=> x;
        (SYMBOL(x))=> val:(x);

ตัวประเมินนิพจน์ที่สมบูรณ์ยิ่งขึ้นที่กำหนดการส่งคืนจาก expr_gen ออกจากกิ่งไปยัง x และสาขาที่ถูกต้องถึง y เวกเตอร์การกระทำที่สอดคล้องกันดำเนินการกับ x และ y กลับมา คู่สุดท้าย unparse => action จับคู่ออบเจ็กต์ตัวเลขและสัญลักษณ์

คุณลักษณะสัญลักษณ์และสัญลักษณ์

สัญลักษณ์อาจมีชื่อคุณลักษณะ val: (x) เข้าถึงแอตทริบิวต์ val ของวัตถุสัญลักษณ์ที่มีอยู่ใน x สัญลักษณ์ตารางสแต็กทั่วไปเป็นส่วนหนึ่งของ SLIC ตาราง SYMBOL อาจถูกผลักและผุดให้สัญลักษณ์ท้องถิ่นสำหรับฟังก์ชั่น สัญลักษณ์ที่สร้างขึ้นใหม่จะแสดงรายการในตารางสัญลักษณ์ด้านบน การค้นหาสัญลักษณ์ค้นหาสแต็กตารางสัญลักษณ์จากตารางด้านบนก่อนย้อนหลังสแต็ก

การสร้างรหัสอิสระของเครื่อง

ภาษาเครื่องกำเนิดของ SLIC สร้างวัตถุคำสั่ง PSEUDO ผนวกเข้ากับรายการรหัสส่วน A. FLUSH ทำให้รายการรหัส PSEUDO ถูกเรียกใช้ลบคำสั่ง PSEUDO แต่ละรายการออกจากรายการและเรียกมัน หลังจากดำเนินการหน่วยความจำวัตถุ PSEUDO จะถูกปล่อยออกมา เนื้อหากระบวนงานของการกระทำของ PSEUDO และ GENERATOR นั้นเป็นภาษาเดียวกันยกเว้นการส่งออก PSEUDO มีวัตถุประสงค์เพื่อทำหน้าที่เป็นมาโครประกอบที่ให้การจัดลำดับรหัสอิสระของเครื่อง พวกเขาจัดให้มีการแยกเครื่องเป้าหมายที่เฉพาะเจาะจงออกจากภาษาของเครื่องมือสร้างแผนภูมิการรวบรวมข้อมูล PSEUDO เรียกใช้ฟังก์ชัน MACHOP เพื่อส่งออกรหัสเครื่อง MACHOPs ใช้เพื่อกำหนด ops เทียมประกอบ (เช่น dc, กำหนดค่าคงที่เป็นต้น) และคำสั่งเครื่องหรือตระกูลของคำสั่งที่จัดรูปแบบเหมือนกันโดยใช้รายการแบบเวกเตอร์ พวกเขาเพียงแค่เปลี่ยนพารามิเตอร์ของพวกเขาเป็นลำดับของบิตฟิลด์ประกอบคำสั่ง การเรียก MACHOP นั้นดูเหมือนว่าจะเป็นแอสเซมบลีและจัดรูปแบบการพิมพ์ของฟิลด์สำหรับเมื่อแอสเซมบลีแสดงในรายการคอมไพล์ ในโค้ดตัวอย่างฉันใช้การคอมเม้นต์สไตล์ c ที่สามารถเพิ่มได้อย่างง่ายดาย แต่ไม่ได้อยู่ในภาษาต้นฉบับ MACHOPs กำลังสร้างโค้ดในหน่วยความจำบิตที่กำหนดแอดเดรสได้ SLIC linker จัดการเอาต์พุตของคอมไพเลอร์ MACHOP สำหรับคำแนะนำโหมดผู้ใช้ DEC-10 โดยใช้รายการแบบเวกเตอร์: MACHOPs กำลังสร้างโค้ดในหน่วยความจำบิตที่กำหนดแอดเดรสได้ SLIC linker จัดการเอาต์พุตของคอมไพเลอร์ MACHOP สำหรับคำแนะนำโหมดผู้ใช้ DEC-10 โดยใช้รายการแบบเวกเตอร์: MACHOPs กำลังสร้างโค้ดในหน่วยความจำบิตที่กำหนดแอดเดรสได้ SLIC linker จัดการเอาต์พุตของคอมไพเลอร์ MACHOP สำหรับคำแนะนำโหมดผู้ใช้ DEC-10 โดยใช้รายการแบบเวกเตอร์:

.MACHOP #opnm register,@indirect offset (index): // Instruction's parameters.
.MORG 36, O(18): $/36; // Align to 36 bit boundary print format: 18 bit octal $/36
O(9):  #opcd;          // Op code 9 bit octal print out
 (4):  register;       // 4 bit register field appended print
 (1):  indirect;       // 1 bit appended print
 (4):  index;          // 4 bit index register appended print
O(18): if (#opcd&&3==1) offset // immediate mode use value else
       else offset/36;         // memory address divide by 36
                               // to get word address.
// Vectored entry opcode table:
#opnm := MOVE, MOVEI, MOVEM, MOVES, MOVS, MOVSI, MOVSM, MOVSS,
         MOVN, MOVNI, MOVNM, MOVNS, MOVM, MOVMI, MOVMM, MOVMS,
         IMUL, IMULI, IMULM, IMULB, MUL,  MULI,  MULM,  MULB,
                           ...
         TDO,  TSO,   TDOE,  TSOE,  TDOA, TSOA,  TDON,  TSON;
// corresponding opcode value:
#opcd := 0O200, 0O201, 0O202, 0O203, 0O204, 0O205, 0O206, 0O207,
         0O210, 0O211, 0O212, 0O213, 0O214, 0O215, 0O216, 0O217,
         0O220, 0O221, 0O222, 0O223, 0O224, 0O225, 0O226, 0O227,
                           ...
         0O670, 0O671, 0O672, 0O673, 0O674, 0O675, 0O676, 0O677;

. Morg 36, O (18): $ / 36; จัดตำแหน่งให้อยู่ในขอบเขต 36 บิตการพิมพ์ที่อยู่คำที่ $ / 36 ตำแหน่ง 18 บิตในแปด 9 บิต opcd, 4 บิตลงทะเบียน, บิตทางอ้อมและดัชนี 4 บิตจะรวมกันและพิมพ์ราวกับว่าฟิลด์ 18 บิตเดียว ที่อยู่ 18 บิต / 36 หรือค่าทันทีคือเอาต์พุตและพิมพ์เป็นฐานแปด ตัวอย่าง MOVEI พิมพ์ด้วย r1 = 1 และ r2 = 2:

400020 201082 000005            MOVEI r1,5(r2)

ด้วยตัวเลือกคอมไพเลอร์แอสเซมบลีคุณจะได้รับรหัสแอสเซมบลีที่สร้างขึ้นในรายการคอมไพล์

เชื่อมโยงเข้าด้วยกัน

SLIC linker จัดทำขึ้นเป็นห้องสมุดที่จัดการการเชื่อมโยงและการแก้ปัญหาสัญลักษณ์ การจัดรูปแบบไฟล์โหลดเอาต์พุตเป้าหมายเฉพาะ แต่ต้องเขียนสำหรับเครื่องเป้าหมายและลิงก์กับไลบรารีไลบรารี linker

ภาษาเครื่องกำเนิดมีความสามารถในการเขียนต้นไม้ไปยังไฟล์และอ่านพวกเขาอนุญาตให้คอมไพเลอร์มัลติพาสที่จะดำเนินการ

การสร้างรหัสและต้นกำเนิดสั้น ๆ ในฤดูร้อน

ฉันได้ไปสร้างรหัสก่อนเพื่อประกันเป็นที่เข้าใจว่า SLIC เป็นคอมไพเลอร์จริง SLIC ได้รับแรงบันดาลใจจาก CWIC (คอมไพเลอร์สำหรับการเขียนและการนำคอมไพเลอร์) ไปพัฒนาที่ Systems Development Corporation ในช่วงปลายทศวรรษ 1960 CWIC มีภาษา SYNTAX และ GENERATOR เท่านั้นที่สร้างโค้ดไบต์ตัวเลขจากภาษา GENERATOR รหัสไบต์ถูกวางหรือวาง (คำที่ใช้ในเอกสาร CWICs) ลงในบัฟเฟอร์หน่วยความจำที่เกี่ยวข้องกับส่วนที่มีชื่อและเขียนโดยคำสั่ง. FLUSH เอกสาร ACM บน CWIC นั้นมีอยู่ในคลัง ACM

ประสบความสำเร็จในการใช้ภาษาการเขียนโปรแกรมที่สำคัญ

ในปลายปี 1970 SLIC ถูกใช้เพื่อเขียนคอมไพเลอร์ข้ามภาษาโคบอล เสร็จสมบูรณ์ในเวลาประมาณ 3 เดือนส่วนใหญ่โดยโปรแกรมเมอร์เดียว ฉันทำงานกับโปรแกรมเมอร์เล็กน้อยตามที่ต้องการ โปรแกรมเมอร์อีกคนเขียนไลบรารีรันไทม์และ MACHOP สำหรับเป้าหมาย TI-990 mini-COMPUTER คอมไพเลอร์ COBOL นั้นรวบรวมบรรทัดมากขึ้นต่อวินาทีจากนั้นคอมไพเลอร์ COBOL ดั้งเดิม DEC-10 ที่เขียนในชุดประกอบ

มากกว่าจะเรียบเรียงแล้วมักจะพูดถึง

ส่วนใหญ่ในการเขียนคอมไพเลอร์จากศูนย์คือไลบรารีรันไทม์ คุณต้องมีตารางสัญลักษณ์ คุณต้องการอินพุตและเอาต์พุต การจัดการหน่วยความจำแบบไดนามิคเป็นต้นสามารถเขียนไลบรารีรันไทม์สำหรับคอมไพเลอร์ได้ง่ายขึ้นจากนั้นจึงเขียนคอมไพเลอร์ แต่ด้วย SLIC นั้น runtime library นั้นเป็นเรื่องธรรมดาสำหรับคอมไพเลอร์ทั้งหมดที่พัฒนาใน SLIC โปรดทราบว่ามีสองไลบรารีรันไทม์ หนึ่งตัวสำหรับเครื่องเป้าหมาย (ตัวอย่างเช่น COBOL) อีกอันคือคอมไพเลอร์รันไทม์คอมไพเลอร์ไลบรารี

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

ภาษาการเขียนโปรแกรม Parser

parser ถูกเขียนโดยใช้สูตรที่เขียนในรูปแบบของสมการง่าย ๆ

<name> <formula type operator> <expression> ;

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

/*  Character Class Formula                                    class_mask */
bin: '0'|'1';                                                // 0b00000010
oct: bin|'2'|'3'|'4'|'5'|'6'|'7';                            // 0b00000110
dgt: oct|'8'|'9';                                            // 0b00001110
hex: dgt|'A'|'B'|'C'|'D'|'E'|'F'|'a'|'b'|'c'|'d'|'e'|'f';    // 0b00011110
upr:  'A'|'B'|'C'|'D'|'E'|'F'|'G'|'H'|'I'|'J'|'K'|'L'|'M'|
      'N'|'O'|'P'|'Q'|'R'|'S'|'T'|'U'|'V'|'W'|'X'|'Y'|'Z';   // 0b00100000
lwr:  'a'|'b'|'c'|'d'|'e'|'f'|'g'|'h'|'i'|'j'|'k'|'l'|'m'|
      'n'|'o'|'p'|'q'|'r'|'s'|'t'|'u'|'v'|'w'|'x'|'y'|'z';   // 0b01000000
alpha:  upr|lwr;                                             // 0b01100000
alphanum: alpha|dgt;                                         // 0b01101110

skip_class 0b00000001 มีการกำหนดไว้ล่วงหน้า แต่อาจ overroad กำลังกำหนด skip_class

โดยสรุป: คลาสอักขระเป็นรายการของทางเลือกที่สามารถเป็นค่าคงที่อักขระลำดับของอักขระหรือคลาสอักขระที่กำหนดไว้ก่อนหน้านี้เท่านั้น เมื่อฉันนำคลาสอักขระไปใช้: สูตรคลาสได้รับการกำหนดมาส์กระดับบิต (แสดงความคิดเห็นด้านบน) สูตรคลาสใด ๆ ที่มีตัวอักษรหรือตัวอักษรใด ๆ จะทำให้บิตของคลาสได้รับการจัดสรร มาสก์ถูกสร้างขึ้นโดย oring มาสก์คลาสที่รวมไว้พร้อมกับบิตที่จัดสรร (ถ้ามี) ตารางเรียนถูกสร้างจากคลาสตัวละคร รายการที่จัดทำดัชนีตามลำดับของตัวละครประกอบด้วยบิตที่ระบุถึงการเป็นสมาชิกคลาสของตัวละคร การทดสอบในชั้นเรียนจะทำแบบอินไลน์ ตัวอย่างรหัส IA-86 พร้อมลำดับของตัวละครใน eax แสดงให้เห็นถึงการทดสอบในชั้นเรียน:

test    byte ptr [eax+_classmap],dgt

ตามมาด้วย:

jne      <success>

หรือ

je       <failure>

มีการใช้ตัวอย่างรหัสคำสั่ง IA-86 เพราะฉันคิดว่าคำแนะนำของ IA-86 นั้นเป็นที่รู้จักอย่างกว้างขวางมากขึ้นในปัจจุบัน ชื่อคลาสที่ประเมินค่ามาสก์ของคลาสนั้นไม่ถูกทำลายด้วย ANDed กับตารางคลาสที่ทำดัชนีโดยอักขระลำดับ (เป็น eax) ผลลัพธ์ที่ไม่เป็นศูนย์บ่งบอกความเป็นสมาชิกของคลาส (EAX เป็นศูนย์ยกเว้น al (8 บิตต่ำของ EAX) ที่มีอักขระ)

โทเค็นแตกต่างกันเล็กน้อยในคอมไพเลอร์เก่าเหล่านี้ คำสำคัญไม่ได้อธิบายว่าเป็นโทเค็น พวกเขาเพียงแค่ถูกจับคู่โดยค่าคงที่สตริงที่ยกมาในภาษา parser สตริงที่อ้างถึงจะไม่ถูกเก็บไว้ตามปกติ อาจใช้ตัวดัดแปลง A + ทำให้สตริงตรงกัน (เช่น + '-' จับคู่ a - อักขระที่รักษาอักขระเมื่อสำเร็จ) The, การดำเนินการ (เช่น 'E') จะแทรกสตริงลงในโทเค็น พื้นที่สีขาวได้รับการจัดการโดยสูตรโทเค็นที่ข้ามอักขระ SKIP_CLASS ชั้นนำจนกว่าจะมีการจับคู่ครั้งแรก โปรดทราบว่าการจับคู่อักขระ skip_class ชัดเจนจะหยุดการข้ามเพื่อให้โทเค็นเริ่มต้นด้วยอักขระ skip_class สูตรโทเค็นสตริงจะข้ามอักขระ skip_class ชั้นนำที่ตรงกับอักขระอัญประกาศเดี่ยวเดียวหรือสตริงที่ยกมาสองครั้ง สิ่งที่น่าสนใจคือการจับคู่ "อักขระภายในสตริง" ที่อ้างถึง:

string .. (''' .ANY ''' | '"' $(-"""" .ANY | """""","""") '"') MAKSTR[];

ทางเลือกแรกตรงกับตัวละครที่ยกมาอ้างใด ๆ ทางเลือกที่ถูกต้องตรงกับสตริงที่มีเครื่องหมายคำพูดคู่ซึ่งอาจรวมถึงอักขระเครื่องหมายคำพูดคู่โดยใช้ "อักขระร่วมกันเพื่อแสดงถึงอักขระ" ตัวเดียว สูตรนี้กำหนดสตริงที่ใช้ในคำจำกัดความของตัวเอง ทางเลือกด้านขวาภายใน '' '$ (- "" "" ".any |" "" "", "" "" ")" "' ตรงกับสตริงที่ยกมาอ้างคู่ เราสามารถใช้อักขระ 'ที่ยกมาเดี่ยวเพื่อจับคู่เครื่องหมายอัญประกาศคู่ "อักขระภายในสตริงที่ยกมาสองครั้งถ้าเราต้องการใช้อักขระ" เราต้องใช้อักขระสองตัว "เพื่อรับหนึ่ง ตัวอย่างเช่นในตัวเลือกด้านซ้ายภายในจับคู่กับอักขระใด ๆ ยกเว้นเครื่องหมายคำพูด:

-"""" .ANY

มองเชิงลบไปข้างหน้า - "" "" จะใช้เมื่อประสบความสำเร็จ (ไม่ตรงกับ "ตัวอักษร") จากนั้นจะจับคู่อักขระ. yany (ซึ่งไม่สามารถเป็น "ตัวอักษรเพราะ -" "" "กำจัดโอกาสที่จะเกิดขึ้น) ทางเลือกที่เหมาะสมกำลังดำเนินการ - "" "" จับคู่อักขระ "และความล้มเหลวเป็นตัวเลือกที่เหมาะสม:

"""""",""""

พยายามที่จะจับคู่ "ตัวละครแทนที่พวกเขาด้วยสองครั้งเดียว" โดยใช้ "" "" "เพื่อแทรก thw เดียว" ตัวละครทั้งสองทางเลือกภายในล้มเหลวอักขระอัญประกาศปิดสตริงถูกจับคู่และ MAKSTR [] เรียกว่าเพื่อสร้างวัตถุสตริง $ ลำดับลูปในขณะที่ประสบความสำเร็จจะใช้ตัวดำเนินการในการจับคู่ลำดับโทเค็นสูตรข้ามตัวละครคลาส skip ชั้นนำ (ช่องว่างเล็กน้อย) เมื่อการจับคู่ครั้งแรกจะทำให้ skip_class ข้ามถูกปิดใช้งานเราสามารถเรียกฟังก์ชั่นโปรแกรมในภาษาอื่น ๆ [], MAKBIN [], MAKOCT [], MAKHEX [], MAKFLOAT [] และ MAKINT [] เป็นฟังก์ชันของไลบรารีที่แปลงสตริงโทเค็นที่ตรงกันให้เป็นวัตถุที่พิมพ์สูตรตัวเลขด้านล่างแสดงการรับรู้โทเค็นที่ค่อนข้างซับซ้อน:

number .. "0B" bin $bin MAKBIN[]        // binary integer
         |"0O" oct $oct MAKOCT[]        // octal integer
         |("0H"|"0X") hex $hex MAKHEX[] // hexadecimal integer
// look for decimal number determining if integer or floating point.
         | ('+'|+'-'|--)                // only - matters
           dgt $dgt                     // integer part
           ( +'.' $dgt                  // fractional part?
              ((+'E'|'e','E')           // exponent  part
               ('+'|+'-'|--)            // Only negative matters
               dgt(dgt(dgt|--)|--)|--)  // 1 2 or 3 digit exponent
             MAKFLOAT[] )               // floating point
           MAKINT[];                    // decimal integer

สูตรโทเค็นตัวเลขด้านบนรับรู้จำนวนเต็มและจำนวนจุดลอยตัว ตัวเลือก - จะประสบความสำเร็จเสมอ อาจใช้วัตถุตัวเลขในการคำนวณได้ โทเค็นออบเจ็กต์ถูกส่งไปยังการแยกวิเคราะห์สแต็คตามความสำเร็จของสูตร เลขชี้กำลังเป็นเลขชี้กำลังใน (+ 'E' | 'e', ​​'E') นั้นน่าสนใจ เราต้องการที่จะมี E ตัวพิมพ์ใหญ่สำหรับ MAKEFLOAT [] แต่เราอนุญาตให้ตัวพิมพ์เล็ก 'e' แทนที่ใช้ 'E'

คุณอาจสังเกตเห็นความสอดคล้องของคลาสอักขระและสูตรโทเค็น สูตรการแยกวิเคราะห์ดำเนินการต่อที่เพิ่มทางเลือกการย้อนรอยและผู้ดำเนินการก่อสร้างต้นไม้ ตัวดำเนินการสำรองและการย้อนรอยที่ไม่ใช่การย้อนรอยอาจไม่ได้รับการผสมภายในระดับนิพจน์ คุณอาจไม่มี (a | b \ c) ผสมการไม่ย้อนรอย | withe \ backtracking ทางเลือก (a \ b \ c), (a | b | c) และ ((a | b) \ c) ถูกต้อง A \ backtracking ทางเลือกบันทึกสถานะการแยกวิเคราะห์ก่อนที่จะลองทางเลือกซ้ายและความล้มเหลวในการกู้คืนสถานะการแยกวิเคราะห์ก่อนที่จะลองทางเลือกที่เหมาะสม ในลำดับของทางเลือกทางเลือกแรกที่ประสบความสำเร็จเป็นไปตามกลุ่ม ทางเลือกเพิ่มเติมจะไม่พยายาม การแยกตัวประกอบและการจัดกลุ่มให้การแยกวิเคราะห์ที่ก้าวหน้าอย่างต่อเนื่อง ตัวเลือกย้อนกลับสร้างสถานะการแยกวิเคราะห์ก่อนที่จะพยายามทดแทนทางซ้าย ต้องใช้การย้อนรอยเมื่อการแยกวิเคราะห์อาจทำการจับคู่บางส่วนและล้มเหลว:

(a b | c d)\ e

ในกรณีข้างต้นหากมีความผิดพลาดในการส่งคืนจะมีการพยายามใช้ซีดีทางเลือก ถ้าหาก c ส่งคืนความล้มเหลวตัวเลือกย้อนกลับจะถูกพยายาม หาก a ประสบความสำเร็จและ b ล้มเหลว parse wile จะย้อนรอยและพยายาม e ในทำนองเดียวกันความล้มเหลวที่ประสบความสำเร็จและขล้มเหลวในการแยกเป็น backtracked และ e ทางเลือกที่นำมา การย้อนรอยไม่ได้ จำกัด อยู่ภายในสูตร หากมีการแยกวิเคราะห์สูตรทำให้การแข่งขันบางส่วนได้ตลอดเวลาและจากนั้นล้มเหลวในการแยกวิเคราะห์จะถูกรีเซ็ตเป็น backtrack ด้านบนและทางเลือกที่นำมาใช้ ความล้มเหลวในการรวบรวมสามารถเกิดขึ้นได้หากรหัสได้รับการส่งออกความรู้สึก backtrack ถูกสร้างขึ้น มีการตั้งค่าย้อนรอยก่อนเริ่มการคอมไพล์ การส่งคืนความล้มเหลวหรือการย้อนรอยกลับเป็นความล้มเหลวของคอมไพเลอร์ Backtracks ซ้อนกัน เราอาจใช้ค่าลบ - และค่าบวก? peek / มองไปข้างหน้าผู้ประกอบการในการทดสอบโดยไม่ต้องแยกวิเคราะห์ล่วงหน้า การทดสอบสตริงคือการมองไปข้างหน้าเพียงต้องการสถานะอินพุตที่บันทึกและรีเซ็ต การมองไปข้างหน้าจะเป็นนิพจน์การแยกวิเคราะห์ที่ทำให้การจับคู่บางส่วนก่อนที่จะล้มเหลว มองไปข้างหน้าจะดำเนินการโดยใช้การย้อนรอย

ภาษาตัวแยกวิเคราะห์ไม่ใช่ตัวแยกวิเคราะห์ LL หรือ LR แต่ภาษาการเขียนโปรแกรมสำหรับการเขียนโปรแกรมวิเคราะห์คำซ้ำซึ่งคุณเขียนโปรแกรมสร้างทรี:

:<node name> creates a node object and pushes it onto the node stack.
..           Token formula create token objects and push them onto 
             the parse stack.
!<number>    pops the top node object and top <number> of parstack 
             entries into a list representation of the tree. The 
             tree then pushed onto the parse stack.
+[ ... ]+    creates a list of the parse stack entries created 
             between them:
              '(' +[argument $(',' argument]+ ')'
             could parse an argument list. into a list.

ตัวอย่างการแยกวิเคราะห์ที่ใช้กันทั่วไปคือการแสดงออกทางคณิตศาสตร์:

Exp = Term $(('+':ADD|'-':SUB) Term!2); 
Term = Factor $(('*':MPY|'/':DIV) Factor!2);
Factor = ( number
         | id  ( '(' +[Exp $(',' Exp)]+ ')' :FUN!2
               | --)
         | '(' Exp ')" )
         (^' Factor:XPO!2 |--);

Exp และ Term โดยใช้การวนซ้ำสร้างแผนผังมือซ้าย ปัจจัยที่ใช้การเรียกซ้ำที่ถูกต้องจะสร้างทรีที่ถนัดขวา:

d^(x+5)^3-a+b*c => ADD[SUB[EXP[EXP[d,ADD[x,5]],3],a],MPY[b,c]]

              ADD
             /   \
          SUB     MPY
         /   \   /   \
      EXP     a b     c
     /   \
    d     EXP     
         /   \
      ADD     3
     /   \
    x     5

นี่คือคอมไพเลอร์ cc เล็กน้อย SLIC เวอร์ชันอัปเดตพร้อมกับข้อคิดเห็นสไตล์ c ประเภทฟังก์ชั่น (ไวยากรณ์, โทเค็น, คลาสตัวอักษร, ตัวสร้าง, PSEUDO หรือ MACHOP ถูกกำหนดโดยไวยากรณ์เริ่มต้นตาม ID ของพวกเขาด้วยตัวแยกวิเคราะห์จากบนลงล่างเหล่านี้คุณเริ่มต้นด้วยสูตรกำหนดโปรแกรม:

program = $((declaration            // A program is a sequence of
                                    // declarations terminated by
            |.EOF .STOP)            // End Of File finish & stop compile
           \                        // Backtrack: .EOF failed or
                                    // declaration long-failed.
             (ERRORX["?Error?"]     // report unknown error
                                    // flagging furthest parse point.
              $(-';' (.ANY          // find a ';'. skiping .ANY
                     | .STOP))      // character: .ANY fails on end of file
                                    // so .STOP ends the compile.
                                    // (-';') failing breaks loop.
              ';'));                // Match ';' and continue

declaration =  "#" directive                // Compiler directive.
             | comment                      // skips comment text
             | global        DECLAR[*1]     // Global linkage
             |(id                           // functions starting with an id:
                ( formula    PARSER[*1]     // Parsing formula
                | sequencer  GENERATOR[*1]  // Code generator
                | optimizer  ISO[*1]        // Optimizer
                | pseudo_op  PRODUCTION[*1] // Pseudo instruction
                | emitor_op  MACHOP[*1]     // Machine instruction
                )        // All the above start with an identifier
              \ (ERRORX["Syntax error."]
                 garbol);                    // skip over error.

// สังเกตว่า id ถูกแยกออกจากกันอย่างไรและรวมกันในภายหลังเมื่อสร้างต้นไม้

formula =   ("==" syntax  :BCKTRAK   // backtrack grammar formula
            |'='  syntax  :SYNTAX    // grammar formula.
            |':'  chclass :CLASS     // character class define
            |".." token   :TOKEN     // token formula
              )';' !2                // Combine node name with id 
                                     // parsed in calling declaration 
                                     // formula and tree produced
                                     // by the called syntax, token
                                     // or character class formula.
                $(-(.NL |"/*") (.ANY|.STOP)); Comment ; to line separator?

chclass = +[ letter $('|' letter) ]+;// a simple list of character codes
                                     // except 
letter  = char | number | id;        // when including another class

syntax  = seq ('|' alt1|'\' alt2 |--);

alt1    = seq:ALT!2 ('|' alt1|--);  Non-backtrack alternative sequence.

alt2    = seq:BKTK!2 ('\' alt2|--); backtrack alternative sequence

seq     = +[oper $oper]+;

oper    = test | action | '(' syntax ')' | comment; 

test    = string | id ('[' (arg_list| ,NILL) ']':GENCALL!2|.EMPTY);

action  = ':' id:NODE!1
        | '!' number:MAKTREE!1
        | "+["  seq "]+" :MAKLST!1;

//     C style comments
comment  = "//" $(-.NL .ANY)
         | "/*" $(-"*/" .ANY) "*/";

สิ่งที่ควรทราบคือภาษา parser จัดการกับความคิดเห็นและการกู้คืนข้อผิดพลาด

ฉันคิดว่าฉันได้ตอบคำถาม ต้องเขียนส่วนใหญ่ของผู้สืบทอด SLIC ภาษาซีซีในตัวเองที่นี่ ไม่มีคอมไพเลอร์สำหรับมันในขณะนี้ แต่ฉันสามารถรวบรวมมันลงในรหัสการชุมนุม, ฟังก์ชั่น asm c หรือ c ++ เปล่า


0

ใช่คุณสามารถเขียนคอมไพเลอร์สำหรับภาษาในภาษานั้น ไม่คุณไม่จำเป็นต้องมีคอมไพเลอร์ตัวแรกสำหรับภาษานั้นในการบู๊ตสแตรป

สิ่งที่คุณต้องใช้ในการ bootstrap คือการนำภาษาไปใช้ ที่สามารถเป็นได้ทั้งคอมไพเลอร์หรือล่าม

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

นี่ไม่ใช่แค่ทางทฤษฎี ขณะนี้ฉันกำลังทำสิ่งนี้อยู่ ฉันกำลังทำงานกับคอมไพเลอร์สำหรับภาษาแซลมอนที่ฉันพัฒนาตัวเอง ฉันสร้างคอมไพเลอร์ Salmon เป็นครั้งแรกใน C และตอนนี้ฉันกำลังเขียนคอมไพเลอร์ใน Salmon ดังนั้นฉันสามารถทำให้คอมไพเลอร์ Salmon ทำงานได้โดยไม่ต้องมีคอมไพเลอร์สำหรับ Salmon ที่เขียนในภาษาอื่น


-1

บางทีคุณสามารถเขียนBNFอธิบาย BNF


4
คุณสามารถจริง ๆ (มันก็ไม่ยากเช่นกัน) แต่แอปพลิเคชันเชิงปฏิบัติเท่านั้นที่จะอยู่ในตัวแยกวิเคราะห์
Daniel Spiewak

อันที่จริงฉันใช้วิธีการนั้นมากในการผลิตตัวแยกวิเคราะห์ LIME การแทนแบบตารางแบบเรียบง่ายและเรียบง่ายของ metagrammar ต้องผ่านตัวแยกวิเคราะห์แบบสืบเชื้อสายแบบง่ายๆ จากนั้น LIME จะสร้าง parser สำหรับภาษาของไวยากรณ์และใช้ parser นั้นเพื่ออ่านไวยากรณ์ที่บางคนสนใจในการสร้าง parser หมายความว่าฉันไม่ต้องรู้วิธีเขียนสิ่งที่ฉันเพิ่งเขียน รู้สึกเหมือนเวทมนต์
เอียน

ที่จริงแล้วคุณไม่สามารถทำได้เนื่องจาก BNF ไม่สามารถอธิบายตัวเองได้ คุณต้องมีชุดตัวเลือกเช่นที่ใช้ในyaccโดยที่ไม่มีการอ้างอิงสัญลักษณ์ที่มินัล
มาร์ควิสแห่ง Lorne

1
คุณไม่สามารถใช้ bnf เพื่อกำหนด bnf เนื่องจาก <> ไม่สามารถจดจำได้ EBNF แก้ไขว่าโดยการอ้างอิงโทเค็นสตริงคงที่ของภาษา
GK
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.