Viết một trình biên dịch bằng ngôn ngữ riêng của nó


204

Theo trực giác, có vẻ như một trình biên dịch cho ngôn ngữ Fookhông thể tự viết bằng Foo. Cụ thể hơn, trình biên dịch đầu tiên cho ngôn ngữ Fookhông thể được viết bằng Foo, nhưng bất kỳ trình biên dịch tiếp theo nào cũng có thể được viết cho Foo.

Nhưng điều này có thực sự đúng không? Tôi có một số hồi ức rất mơ hồ khi đọc về một ngôn ngữ mà trình biên dịch đầu tiên được viết bằng "chính nó". Điều này là có thể, và nếu vậy làm thế nào?



Đây là một câu hỏi rất cũ, nhưng nói rằng tôi đã viết một trình thông dịch cho ngôn ngữ Foo trong Java. Sau đó, với ngôn ngữ foo, tôi đã viết nó thông dịch viên. Foo vẫn sẽ yêu cầu JRE phải không?
George Xavier

Câu trả lời:


231

Điều này được gọi là "bootstrapping". Trước tiên, bạn phải xây dựng trình biên dịch (hoặc trình thông dịch) cho ngôn ngữ của mình bằng một số ngôn ngữ khác (thường là Java hoặc C). Khi đã xong, bạn có thể viết một phiên bản mới của trình biên dịch bằng ngôn ngữ Foo. Bạn sử dụng trình biên dịch bootstrap đầu tiên để biên dịch trình biên dịch, sau đó sử dụng trình biên dịch được biên dịch này để biên dịch mọi thứ khác (bao gồm cả các phiên bản tương lai của chính nó).

Hầu hết các ngôn ngữ thực sự được tạo ra theo kiểu này, một phần vì các nhà thiết kế ngôn ngữ thích sử dụng ngôn ngữ họ đang tạo và cũng vì một trình biên dịch không tầm thường thường đóng vai trò là một chuẩn mực hữu ích cho việc ngôn ngữ có thể "hoàn thành" như thế nào.

Một ví dụ về điều này sẽ là Scala. Trình biên dịch đầu tiên của nó được tạo ra trong Pizza, một ngôn ngữ thử nghiệm của Martin Oderky. Kể từ phiên bản 2.0, trình biên dịch đã được viết lại hoàn toàn bằng Scala. Từ thời điểm đó, trình biên dịch Pizza cũ có thể bị loại bỏ hoàn toàn, do thực tế là trình biên dịch Scala mới có thể được sử dụng để tự biên dịch cho các lần lặp lại trong tương lai.


Có thể là một câu hỏi ngu ngốc: Nếu bạn muốn chuyển trình biên dịch của mình sang một kiến ​​trúc vi xử lý khác, bootstrapping nên khởi động lại từ một trình biên dịch làm việc cho kiến ​​trúc đó. Thê nay đung không? Nếu điều này đúng, điều này có nghĩa là tốt hơn để giữ trình biên dịch đầu tiên vì nó có thể hữu ích để chuyển trình biên dịch của bạn sang các kiến ​​trúc khác (đặc biệt là nếu được viết bằng một 'ngôn ngữ phổ quát' như C)?
piertoni

2
@piertoni thông thường sẽ dễ dàng hơn khi chỉ nhắm mục tiêu lại phần phụ trợ của trình biên dịch sang bộ vi xử lý mới.
bstpierre

Sử dụng LLVM làm phụ trợ, ví dụ

76

Tôi nhớ lại đã nghe một podcast Radio Engineering Radio trong đó Dick Gabriel đã nói về việc khởi động trình thông dịch LISP ban đầu bằng cách viết một phiên bản xương sống trong LISP trên giấy và tự lắp ráp nó thành mã máy. Từ đó trở đi, các tính năng còn lại của LISP đều được viết và giải thích bằng LISP.


Mọi thứ đều được khởi động từ một bóng bán dẫn genesis với rất nhiều bàn tay

47

Thêm một sự tò mò cho các câu trả lời trước.

Đây là một trích dẫn từ hướng dẫn sử dụng Linux From Scratch , tại bước mà người ta bắt đầu xây dựng trình biên dịch GCC từ nguồn của nó. (Linux From Scratch là một cách để cài đặt Linux hoàn toàn khác với cài đặt phân phối, ở chỗ bạn phải biên dịch thực sự từng nhị phân của hệ thống đích.)

make bootstrap

Mục tiêu 'bootstrap' không chỉ biên dịch GCC, mà còn biên dịch nó nhiều lần. Nó sử dụng các chương trình được biên dịch trong vòng đầu tiên để tự biên dịch lần thứ hai và sau đó lại lần thứ ba. Sau đó, nó so sánh các biên dịch thứ hai và thứ ba này để đảm bảo nó có thể tự tái tạo hoàn hảo. Điều này cũng ngụ ý rằng nó đã được biên dịch chính xác.

Việc sử dụng mục tiêu 'bootstrap' được thúc đẩy bởi thực tế là trình biên dịch sử dụng để xây dựng chuỗi công cụ của hệ thống đích có thể không có cùng phiên bản của trình biên dịch đích. Tiếp tục theo cách đó người ta chắc chắn sẽ có được, trong hệ thống đích, một trình biên dịch có thể tự biên dịch.


12
"bạn phải biên dịch thực sự từng nhị phân của hệ thống đích" và bạn phải bắt đầu với nhị phân gcc mà bạn nhận được từ đâu đó, vì nguồn không thể tự biên dịch. Tôi tự hỏi nếu bạn truy ngược lại dòng của mỗi nhị phân gcc đã được sử dụng để biên dịch lại từng gcc kế tiếp, bạn có thể quay trở lại trình biên dịch C gốc của K & R không?
cướp

43

Khi bạn viết trình biên dịch đầu tiên cho C, bạn viết nó bằng một số ngôn ngữ khác. Bây giờ, bạn có một trình biên dịch cho C in, giả sử, trình biên dịch chương trình. Cuối cùng, bạn sẽ đến nơi mà bạn phải phân tích các chuỗi, đặc biệt là thoát các chuỗi. Bạn sẽ viết mã để chuyển đổi \nthành ký tự với mã thập phân 10 (và \rthành 13, v.v.).

Sau khi trình biên dịch đó sẵn sàng, bạn sẽ bắt đầu thực hiện lại nó trong C. Quá trình này được gọi là " bootstrapping ".

Mã phân tích chuỗi sẽ trở thành:

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

Khi điều này biên dịch, bạn có một tệp nhị phân hiểu '\ n'. Điều này có nghĩa là bạn có thể thay đổi mã nguồn:

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

Vậy đâu là thông tin mà '\ n' là mã cho 13? Đó là trong nhị phân! Giống như DNA: Biên dịch mã nguồn C với tệp nhị phân này sẽ kế thừa thông tin này. Nếu trình biên dịch tự biên dịch, nó sẽ truyền kiến ​​thức này cho con cháu của nó. Từ thời điểm này, không có cách nào để xem từ nguồn một mình trình biên dịch sẽ làm gì.

Nếu bạn muốn ẩn virus trong nguồn của một số chương trình, bạn có thể làm điều đó như sau: Lấy nguồn của trình biên dịch, tìm hàm biên dịch các hàm và thay thế nó bằng chương trình này:

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" ...
}

Các phần thú vị là A và B. A là mã nguồn để compileFunctionbao gồm virus, có thể được mã hóa theo một cách nào đó để không tìm thấy nhị phân kết quả. Điều này đảm bảo rằng việc biên dịch sang trình biên dịch với chính nó sẽ bảo vệ mã tiêm virus.

B là giống nhau cho chức năng chúng ta muốn thay thế bằng virus của chúng tôi. Ví dụ, nó có thể là chức năng "đăng nhập" trong tệp nguồn "login.c" có thể là từ nhân Linux. Chúng tôi có thể thay thế nó bằng một phiên bản sẽ chấp nhận mật khẩu "joshua" cho tài khoản root bên cạnh mật khẩu thông thường.

Nếu bạn biên dịch nó và phát tán nó dưới dạng nhị phân, sẽ không có cách nào để tìm virus bằng cách xem nguồn.

Nguồn gốc của ý tưởng: https://web.archive.org/web/20070714062657/http://www.acm.org/ classics / sep95 /


1
Điểm của nửa sau về việc viết trình biên dịch bị nhiễm virus là gì? :)
mhvelplund

3
@mhvelplund Chỉ cần truyền bá kiến ​​thức về cách bootstrapping có thể giết chết bạn.
Aaron Digulla

19

Bạn không thể tự viết trình biên dịch vì bạn không có gì để biên dịch mã nguồn bắt đầu. Có hai cách tiếp cận để giải quyết điều này.

Ít được ưa chuộng nhất là sau đây. Bạn viết một trình biên dịch tối thiểu trong trình biên dịch chương trình (yuck) cho một tập hợp ngôn ngữ tối thiểu và sau đó sử dụng trình biên dịch đó để thực hiện các tính năng bổ sung của ngôn ngữ. Xây dựng theo cách của bạn cho đến khi bạn có một trình biên dịch với tất cả các tính năng ngôn ngữ cho chính nó. Một quá trình đau đớn thường chỉ được thực hiện khi bạn không có lựa chọn nào khác.

Cách tiếp cận ưa thích là sử dụng một trình biên dịch chéo. Bạn thay đổi mặt sau của trình biên dịch hiện có trên một máy khác để tạo đầu ra chạy trên máy đích. Sau đó, bạn có một trình biên dịch đầy đủ tốt đẹp và làm việc trên máy đích. Phổ biến nhất cho điều này là ngôn ngữ C, vì có rất nhiều trình biên dịch hiện có có phần cuối có thể cắm được có thể hoán đổi.

Một thực tế ít được biết đến là trình biên dịch GNU C ++ có một triển khai chỉ sử dụng tập hợp con C. Lý do là thường dễ dàng tìm thấy trình biên dịch C cho máy đích mới cho phép bạn xây dựng trình biên dịch GNU C ++ đầy đủ từ nó. Bây giờ bạn đã tự khởi động để có một trình biên dịch C ++ trên máy đích.


14

Nói chung, bạn cần phải có một phần cắt làm việc (nếu nguyên thủy) của trình biên dịch làm việc trước - sau đó bạn có thể bắt đầu suy nghĩ về việc làm cho nó tự lưu trữ. Đây thực sự được coi là một cột mốc quan trọng trong một số langauges.

Từ những gì tôi nhớ từ "mono", có khả năng họ sẽ cần thêm một vài điều để phản ánh để làm cho nó hoạt động: nhóm mono tiếp tục chỉ ra rằng một số điều đơn giản là không thể Reflection.Emit; Tất nhiên, nhóm MS có thể chứng minh họ sai.

Điều này có một vài lợi thế thực sự : nó là một bài kiểm tra đơn vị khá tốt, cho người mới bắt đầu! Và bạn chỉ có một ngôn ngữ để lo lắng (có thể là một chuyên gia C # có thể không biết nhiều về C ++; nhưng bây giờ thy có thể sửa trình biên dịch C #). Nhưng tôi tự hỏi nếu không có một niềm tự hào nghề nghiệp nào trong công việc ở đây: họ chỉ đơn giản muốn nó tự lưu trữ.

Không hẳn là một trình biên dịch, nhưng gần đây tôi đã làm việc trên một hệ thống tự lưu trữ; trình tạo mã được sử dụng để tạo trình tạo mã ... vì vậy nếu lược đồ thay đổi, tôi chỉ cần chạy nó trên chính nó: phiên bản mới. Nếu có lỗi, tôi chỉ cần quay lại phiên bản cũ hơn và thử lại. Rất thuận tiện, và rất dễ bảo trì.


Cập nhật 1

Tôi vừa xem video này của Anders tại PDC, và (khoảng một giờ nữa) anh ấy đưa ra một số lý do hợp lệ hơn nhiều - tất cả về trình biên dịch như một dịch vụ. Chỉ để cho bản ghi âm thôi.


4

Đây là một bãi chứa (chủ đề khó tìm kiếm trên thực tế):

Đây cũng là ý tưởng của PyPyRubinius :

(Tôi nghĩ điều này cũng có thể áp dụng cho Forth , nhưng tôi không biết gì về Forth.)


Liên kết đầu tiên đến một bài viết được cho là liên quan đến Smalltalk hiện đang trỏ đến một trang mà không có thông tin hữu ích và rõ ràng ngay lập tức.
nbro

1

GNAT, trình biên dịch GNU Ada, yêu cầu trình biên dịch Ada được xây dựng đầy đủ. Điều này có thể là một nỗi đau khi chuyển nó sang một nền tảng nơi không có sẵn nhị phân GNAT.


1
Tôi không thấy tại sao? Không có quy tắc nào bạn phải bootstrap nhiều lần (như đối với mọi nền tảng mới), bạn cũng có thể biên dịch chéo với nền tảng hiện tại.
Marco van de Voort

1

Trên thực tế, hầu hết các trình biên dịch được viết bằng ngôn ngữ họ biên dịch, vì những lý do đã nêu ở trên.

Trình biên dịch bootstrap đầu tiên thường được viết bằng C, C ++ hoặc hội.


1

Trình biên dịch C # của dự án Mono đã được "tự lưu trữ" từ lâu, điều đó có nghĩa là nó đã được viết bằng chính C #.

Những gì tôi biết là trình biên dịch đã được bắt đầu dưới dạng mã C thuần túy, nhưng một khi các tính năng "cơ bản" của ECMA được triển khai, chúng bắt đầu viết lại trình biên dịch trong C #.

Tôi không nhận thức được những lợi thế của việc viết trình biên dịch trong cùng một ngôn ngữ, nhưng tôi chắc chắn rằng nó phải làm ít nhất với các tính năng mà chính ngôn ngữ đó có thể cung cấp (ví dụ: C không hỗ trợ lập trình hướng đối tượng) .

Bạn có thể tìm thêm thông tin ở đây .


1

Tôi đã viết SLIC (Hệ thống ngôn ngữ để triển khai trình biên dịch). Sau đó tự tay biên soạn nó thành lắp ráp. Có rất nhiều thứ cho SLIC vì nó là một trình biên dịch duy nhất gồm năm ngôn ngữ phụ:

  • Ngôn ngữ lập trình phân tích cú pháp SYNTAX PPL
  • GENERATOR LISP 2 ngôn ngữ tạo mã PSEUDO thu thập dữ liệu dựa trên cây
  • ISO theo trình tự, mã PSEUDO, ngôn ngữ tối ưu hóa
  • PSEUDO Macro giống như ngôn ngữ sản xuất mã hội.
  • Máy móc hướng dẫn xác định ngôn ngữ máy.

SLIC được lấy cảm hứng từ CWIC (Trình biên dịch để viết và triển khai trình biên dịch). Không giống như hầu hết các gói phát triển trình biên dịch SLIC và CWIC tạo địa chỉ mã với các ngôn ngữ chuyên biệt, tên miền cụ thể. SLIC mở rộng việc tạo mã CWIC bằng cách thêm các ngôn ngữ phụ ISO, PSEUDO và MachOP tách biệt các chi tiết cụ thể của máy mục tiêu ra khỏi ngôn ngữ trình tạo thu thập dữ liệu cây.

LISP 2 cây và danh sách

Hệ thống quản lý bộ nhớ động của ngôn ngữ trình tạo LISP 2 là thành phần chính. Danh sách được thể hiện bằng ngôn ngữ được bao gồm trong dấu ngoặc vuông, các thành phần của nó được phân tách bằng dấu phẩy tức là danh sách ba phần tử [a, b, c].

Cây:

     ADD
    /   \
  MPY     3
 /   \
5     x

được đại diện bởi các danh sách có mục nhập đầu tiên là một đối tượng nút:

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

Cây thường được hiển thị với nút riêng biệt trước các nhánh:

ADD[MPY[5,x],3]

Unparsing với các chức năng tạo dựa trên LISP 2

Hàm tạo là một tập hợp có tên (unparse) => hành động> cặp ...

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

Các biểu thức khác nhau là các thử nghiệm khớp với các mẫu cây và / hoặc các loại đối tượng phá vỡ chúng và gán các phần đó cho biến cục bộ để được xử lý bằng hành động thủ tục của nó. Kiểu như một hàm quá tải lấy các kiểu đối số khác nhau. Ngoại trừ các thử nghiệm () => ... được thử theo thứ tự được mã hóa. Unparse thành công đầu tiên thực hiện hành động tương ứng của nó. Các biểu thức khác nhau là các bài kiểm tra tháo gỡ. ADD [x, y] khớp với hai nhánh ADD cây gán các nhánh của nó cho các biến cục bộ x và y. Hành động có thể là một biểu thức đơn giản hoặc một khối mã giới hạn .BEGIN ... .END. Tôi sẽ sử dụng các khối c {{} ngày hôm nay. Kết hợp cây, [], các quy tắc khác nhau có thể gọi các trình tạo chuyển qua (các) kết quả được trả về cho hành động:

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

Cụ thể, expr_gen unparse ở trên khớp với cây ADD hai nhánh. Trong mẫu thử nghiệm, một trình tạo đối số duy nhất được đặt trong nhánh cây sẽ được gọi với nhánh đó. Danh sách đối số của nó mặc dù là các biến cục bộ được gán đối tượng trả về. Phía trên unparse chỉ định hai nhánh là THÊM phân tách cây, đệ quy nhấn từng nhánh để expr_gen. Trả về nhánh bên trái được đặt vào các biến cục bộ x. Tương tự, nhánh bên phải được truyền cho expr_gen với y đối tượng return. Ở trên có thể là một phần của bộ đánh giá biểu thức số. Có các tính năng phím tắt được gọi là vectơ ở trên thay vì chuỗi nút, vectơ của các nút có thể được sử dụng với một vectơ hành động tương ứng:

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);

Trình đánh giá biểu thức đầy đủ hơn ở trên chỉ định trả về từ expr_gen nhánh trái cho x và nhánh phải cho y. Các vectơ hành động tương ứng được thực hiện trên x và y được trả về. Các cặp hành động unparse => cuối cùng khớp với các đối tượng số và ký hiệu.

Biểu tượng và thuộc tính biểu tượng

Biểu tượng có thể có thuộc tính được đặt tên. val: (x) truy cập thuộc tính val của đối tượng ký hiệu có trong x. Một bảng biểu tượng tổng quát là một phần của SLIC. Bảng SYMBOL có thể được đẩy và bật cung cấp các ký hiệu cục bộ cho các hàm. Biểu tượng mới được tạo ra được xếp vào bảng biểu tượng trên cùng. Tra cứu biểu tượng tìm kiếm ngăn xếp bảng biểu tượng từ bảng trên cùng trước tiên xuống phía sau ngăn xếp.

Tạo mã độc lập cho máy

Ngôn ngữ trình tạo của SLIC tạo ra các đối tượng hướng dẫn PSEUDO, nối chúng vào danh sách mã phần. Một .Lush làm cho danh sách mã PSEUDO của nó được chạy loại bỏ từng lệnh PSEUDO khỏi danh sách và gọi nó. Sau khi thực hiện, bộ nhớ đối tượng PSEUDO được giải phóng. Các cơ quan tố tụng của các hành động PSEUDO và GENERATOR về cơ bản là cùng một ngôn ngữ ngoại trừ đầu ra của chúng. PSEUDO có nghĩa là hoạt động như các macro lắp ráp cung cấp tuần tự mã độc lập cho máy. Chúng cung cấp một sự tách biệt của máy mục tiêu cụ thể ra khỏi ngôn ngữ trình tạo thu thập dữ liệu cây. Các PSEUDO gọi các hàm MachOP để xuất mã máy. Máy được sử dụng để xác định ops giả lắp ráp (như dc, xác định hằng số, v.v.) và hướng dẫn máy hoặc một họ các hướng dẫn được định dạng giống như sử dụng mục nhập vectơ. Họ chỉ đơn giản chuyển đổi các tham số của họ thành một chuỗi các trường bit tạo thành lệnh. Các cuộc gọi MachOP có nghĩa là trông giống như lắp ráp và cung cấp định dạng in của các trường khi lắp ráp được hiển thị trong danh sách biên dịch. Trong mã ví dụ tôi đang sử dụng nhận xét kiểu c có thể dễ dàng thêm nhưng không có trong các ngôn ngữ gốc. Máy đang sản xuất mã vào một bộ nhớ địa chỉ bit. Trình liên kết SLIC xử lý đầu ra của trình biên dịch. Một máy cho hướng dẫn chế độ người dùng DEC-10 bằng cách sử dụng mục nhập có vectơ: Máy đang sản xuất mã vào một bộ nhớ địa chỉ bit. Trình liên kết SLIC xử lý đầu ra của trình biên dịch. Một máy cho hướng dẫn chế độ người dùng DEC-10 bằng cách sử dụng mục nhập có vectơ: Máy đang sản xuất mã vào một bộ nhớ địa chỉ bit. Trình liên kết SLIC xử lý đầu ra của trình biên dịch. Một máy cho hướng dẫn chế độ người dùng DEC-10 bằng cách sử dụng mục nhập có vectơ:

.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; căn chỉnh vị trí thành ranh giới 36 bit in vị trí địa chỉ $ / 36 từ 18 bit trong bát phân. Opcd 9 bit, thanh ghi 4 bit, bit gián tiếp và thanh ghi chỉ số 4 bit được kết hợp và in như thể một trường 18 bit duy nhất. Địa chỉ 18 bit / 36 hoặc giá trị ngay lập tức là đầu ra và được in bằng số bát phân. Một ví dụ MOVEI in ra với r1 = 1 và r2 = 2:

400020 201082 000005            MOVEI r1,5(r2)

Với tùy chọn lắp ráp trình biên dịch, bạn có được mã lắp ráp được tạo trong danh sách biên dịch.

Liên kết nó lại với nhau

Trình liên kết SLIC được cung cấp dưới dạng thư viện xử lý các độ phân giải liên kết và ký hiệu. Định dạng tệp tải đầu ra cụ thể mặc dù phải được ghi cho các máy đích và được liên kết với thư viện thư viện liên kết.

Ngôn ngữ trình tạo có khả năng ghi cây vào một tệp và đọc chúng cho phép thực hiện nhiều trình biên dịch.

Tóm tắt ngắn về việc tạo mã và nguồn gốc

Tôi đã đi qua việc tạo mã trước tiên để đảm bảo rằng SLIC là một trình biên dịch trình biên dịch thực sự. SLIC được lấy cảm hứng từ CWIC (Trình biên dịch để viết và triển khai trình biên dịch) được phát triển tại Tập đoàn Phát triển Hệ thống vào cuối những năm 1960. CWIC chỉ có các ngôn ngữ SYNTAX và GENERATOR tạo mã byte số ngoài ngôn ngữ GENERATOR. Mã byte được đặt hoặc trồng (thuật ngữ được sử dụng trong tài liệu CWICs) vào bộ đệm bộ nhớ được liên kết với các phần được đặt tên và được viết ra bởi một câu lệnh. Một bài báo ACM trên CWIC có sẵn từ kho lưu trữ ACM.

Thực hiện thành công một ngôn ngữ lập trình chính

Vào cuối những năm 1970, SLIC đã được sử dụng để viết một trình biên dịch chéo COBOL. Hoàn thành trong khoảng 3 tháng chủ yếu bởi một lập trình viên duy nhất. Tôi đã làm việc một chút với các lập trình viên khi cần thiết. Một lập trình viên khác đã viết thư viện thời gian chạy và MÁY TÍNH cho máy tính mini TI-990. Trình biên dịch COBOL đó đã biên dịch nhiều dòng hơn mỗi giây sau đó trình biên dịch COBOL gốc DEC-10 được viết bằng cách lắp ráp.

Thêm vào một trình biên dịch sau đó thường nói về

Một phần lớn của việc viết một trình biên dịch từ đầu là thư viện thời gian chạy. Bạn cần một bảng biểu tượng. Bạn cần đầu vào và đầu ra. Quản lý bộ nhớ động, vv Có thể dễ dàng hơn khi viết thư viện thời gian chạy cho trình biên dịch sau đó viết trình biên dịch. Nhưng với SLIC, thư viện thời gian chạy là chung cho tất cả các trình biên dịch phát triển trong SLIC. Lưu ý có hai thư viện thời gian chạy. Một cho máy mục tiêu của ngôn ngữ (ví dụ như COBOL). Cái khác là trình biên dịch thư viện thời gian chạy.

Tôi nghĩ rằng tôi đã thiết lập rằng đây không phải là trình tạo phân tích cú pháp. Vì vậy, bây giờ với một chút hiểu biết về back end, tôi có thể giải thích ngôn ngữ lập trình trình phân tích cú pháp.

Ngôn ngữ lập trình phân tích cú pháp

Trình phân tích cú pháp được viết bằng công thức được viết dưới dạng các phương trình đơn giản.

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

Yếu tố ngôn ngữ ở cấp độ thấp nhất là nhân vật. Mã thông báo được hình thành từ một tập hợp con của các ký tự của ngôn ngữ. Các lớp ký tự được sử dụng để đặt tên và định nghĩa các tập hợp con ký tự đó. Toán tử định nghĩa lớp ký tự là ký tự dấu hai chấm (:). Các ký tự là thành viên của lớp được mã hóa ở bên phải của định nghĩa. Các ký tự có thể in được đặt trong các chuỗi số nguyên tố. Các ký tự không in và đặc biệt có thể được biểu diễn bằng số thứ tự của chúng. Các thành viên trong lớp được phân tách bằng một phương án | nhà điều hành. Một công thức lớp kết thúc bằng dấu chấm phẩy. Các lớp nhân vật có thể bao gồm các lớp được xác định trước đó:

/*  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_group 0b00000001 được xác định trước nhưng có thể vượt quá định nghĩa là Skip_group.

Tóm lại: Một lớp ký tự là một danh sách thay thế chỉ có thể là hằng số ký tự, thứ tự của một ký tự hoặc một lớp ký tự được xác định trước đó. Khi tôi triển khai các lớp ký tự: Công thức lớp được gán một mặt nạ bit lớp. (Hiển thị trong các nhận xét ở trên) Bất kỳ công thức lớp nào có bất kỳ ký tự hoặc ký tự nào đều khiến bit lớp được phân bổ. Một mặt nạ được tạo bằng cách ghép (các) mặt nạ lớp của lớp được bao gồm cùng với bit được phân bổ (nếu có). Một bảng lớp được tạo từ các lớp nhân vật. Một mục được lập chỉ mục bởi thứ tự của một ký tự chứa các bit biểu thị tư cách thành viên lớp của nhân vật. Kiểm tra lớp được thực hiện nội tuyến. Một ví dụ mã IA-86 với thứ tự của nhân vật trong eax minh họa thử nghiệm lớp:

test    byte ptr [eax+_classmap],dgt

Theo sau là một:

jne      <success>

hoặc là

je       <failure>

Các ví dụ mã lệnh IA-86 được sử dụng vì tôi nghĩ rằng các lệnh IA-86 được biết đến rộng rãi hơn ngày nay. Tên lớp đánh giá cho mặt nạ lớp của nó là không phá hủy ANDed với bảng lớp được lập chỉ mục bởi các ký tự thứ tự (trong eax). Một kết quả khác không chỉ ra thành viên lớp. (EAX bằng 0 ngoại trừ al (8 bit thấp của EAX) có chứa ký tự).

Mã thông báo có một chút khác biệt trong các trình biên dịch cũ này. Từ khóa không được giải thích là mã thông báo. Chúng chỉ đơn giản được khớp bởi các hằng chuỗi được trích dẫn trong ngôn ngữ trình phân tích cú pháp. Chuỗi trích dẫn thường không được giữ. Công cụ sửa đổi có thể được sử dụng. A + giữ cho chuỗi khớp. (tức là + '-' khớp với một - ký tự giữ ký tự khi thành công) Thao tác, (tức là 'E') chèn chuỗi vào mã thông báo. Khoảng trắng được xử lý bằng công thức mã thông báo bỏ qua các ký tự SKIP_CLASS hàng đầu cho đến khi trận đấu đầu tiên được thực hiện. Lưu ý rằng một kết hợp ký tự Skip_group rõ ràng sẽ dừng việc bỏ qua cho phép mã thông báo bắt đầu bằng ký tự Skip_group. Công thức mã thông báo chuỗi bỏ qua các ký tự bỏ qua hàng đầu khớp với một ký tự trích dẫn trích dẫn đơn hoặc một chuỗi trích dẫn kép. Điều đáng quan tâm là khớp một "ký tự trong một chuỗi" được trích dẫn:

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

Sự thay thế đầu tiên phù hợp với bất kỳ trích dẫn duy nhất trích dẫn nhân vật. Lựa chọn thay thế phù hợp với một chuỗi trích dẫn kép có thể bao gồm các ký tự trích dẫn kép sử dụng hai ký tự "cùng nhau để thể hiện một ký tự". Công thức này xác định các chuỗi được sử dụng trong định nghĩa riêng của nó. Thay thế bên phải bên trong '"' $ (-" "" .ANY | "" "" "", "" "") '"' khớp với một trích dẫn kép trích dẫn. Chúng ta có thể sử dụng một ký tự được trích dẫn để khớp với một ký tự "trích dẫn". Tuy nhiên, trong chuỗi trích dẫn "kép" nếu chúng ta muốn sử dụng một ký tự "chúng ta phải sử dụng hai" ký tự để có được một ký tự. Ví dụ: ở bên trái thay thế phù hợp với bất kỳ ký tự nào ngoại trừ một trích dẫn:

-"""" .ANY

một cái nhìn tiêu cực phía trước - "" "" được sử dụng mà khi thành công (không khớp với "ký tự) thì khớp với ký tự .ANY (không thể là" ký tự vì - "" "" đã loại bỏ khả năng đó). Lựa chọn thay thế phù hợp đang đảm nhận - "" "" khớp với một ký tự "và thất bại là lựa chọn thay thế phù hợp:

"""""",""""

cố gắng khớp hai "ký tự thay thế chúng bằng một ký tự kép" bằng cách sử dụng "," "" để chèn ký tự thw ". Cả hai lựa chọn bên trong không thể ký tự trích dẫn chuỗi đóng được khớp và MAKSTR [] được gọi để tạo đối tượng chuỗi. chuỗi, vòng lặp trong khi thành công, toán tử được sử dụng để khớp chuỗi. Công thức mã thông báo bỏ qua các ký tự lớp bỏ qua hàng đầu (khoảng trắng). Khi một kết quả khớp đầu tiên được thực hiện bỏ qua bỏ qua. Chúng ta có thể gọi các hàm được lập trình bằng các ngôn ngữ khác bằng cách sử dụng []. MAKSTR [], MAKBIN [], MAKOCT [], MAKHEX [], MAKFLOAT [] và MAKINT [] được cung cấp chức năng thư viện để chuyển đổi chuỗi mã thông báo phù hợp thành đối tượng được nhập. Công thức số bên dưới minh họa nhận dạng mã thông báo khá phức tạp:

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

Công thức mã thông báo số ở trên nhận ra số nguyên và số dấu phẩy động. Các - lựa chọn thay thế luôn luôn thành công. Các đối tượng số có thể được sử dụng trong tính toán. Các đối tượng mã thông báo được đẩy lên ngăn xếp phân tích thành công của công thức. Dẫn số mũ trong (+ 'E' | 'e', ​​'E') rất thú vị. Chúng tôi muốn luôn có chữ hoa E cho MAKEFLOAT []. Nhưng chúng tôi cho phép viết thường 'e' thay thế bằng 'E'.

Bạn có thể đã nhận thấy tính nhất quán của lớp ký tự và công thức mã thông báo. Công thức phân tích cú pháp tiếp tục bổ sung các giải pháp thay thế quay lui và các toán tử xây dựng cây. Các toán tử thay thế quay lui và không quay lui có thể không được trộn lẫn trong một mức biểu thức. Bạn có thể không có (a | b \ c) trộn không quay lại | tạm biệt thay thế quay lui. (a \ b \ c), (a | b | c) và ((a | b) \ c) là hợp lệ. Một thay thế quay lui \ lưu trạng thái phân tích cú pháp trước khi thử thay thế bên trái và khi thất bại khôi phục trạng thái phân tích cú pháp trước khi thử phương án thay thế bên phải. Trong một chuỗi các lựa chọn thay thế, sự thay thế thành công đầu tiên thỏa mãn nhóm. Các lựa chọn thay thế khác không được cố gắng. Bao thanh toán và nhóm cung cấp cho một phân tích tiến bộ liên tục. Sự thay thế backtrack tạo ra một trạng thái đã lưu của phân tích cú pháp trước khi nó cố gắng thay thế bên trái của nó. Quay lui được yêu cầu khi phân tích cú pháp có thể khớp một phần và sau đó không thành công:

(a b | c d)\ e

Trong trường hợp trên nếu lỗi trả về thì cd thay thế được thử. Nếu sau đó c trả về thất bại, thay thế backtrack sẽ được thử. Nếu a thành công và b thất bại, parse wile sẽ bị quay lại và e đã cố gắng. Tương tự như vậy, một c thất bại thành công và b thất bại phân tích cú pháp được quay lại và thay thế e đã thực hiện. Quay lui không giới hạn trong một công thức. Nếu bất kỳ công thức phân tích cú pháp nào thực hiện khớp một phần bất cứ lúc nào và sau đó thất bại, phân tích cú pháp sẽ được đặt lại về backtrack hàng đầu và thay thế được thực hiện. Một lỗi biên dịch có thể xảy ra nếu mã đã được xuất có nghĩa là backtrack được tạo. Một backtrack được thiết lập trước khi bắt đầu biên dịch. Trả lại thất bại hoặc quay lại với nó là một lỗi biên dịch. Backtracks được xếp chồng lên nhau. Chúng ta có thể sử dụng tiêu cực - và tích cực? nhìn trộm / nhìn về phía trước các nhà khai thác để kiểm tra mà không tiến tới phân tích cú pháp. kiểm tra chuỗi là một cái nhìn phía trước chỉ cần lưu lại và thiết lập lại trạng thái đầu vào. Nhìn về phía trước sẽ là một biểu thức phân tích làm cho khớp một phần trước khi thất bại. Một cái nhìn phía trước được thực hiện bằng cách sử dụng quay lui.

Ngôn ngữ trình phân tích cú pháp không phải là trình phân tích cú pháp LL hoặc LR. Nhưng một ngôn ngữ lập trình để viết một trình phân tích cú pháp tốt đệ quy trong đó bạn lập trình xây dựng cây:

:<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.

Một ví dụ phân tích cú pháp thường được sử dụng là một biểu thức số học:

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

Exp và Term sử dụng một vòng lặp tạo ra một cây thuận tay trái. Yếu tố sử dụng đệ quy đúng tạo ra một cây thuận tay phải:

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

Dưới đây là một chút về trình biên dịch cc, một phiên bản cập nhật của SLIC với các bình luận kiểu c. Các loại hàm (ngữ pháp, mã thông báo, lớp ký tự, trình tạo, PSEUDO hoặc MachOP được xác định theo cú pháp ban đầu theo id của chúng. Với các trình phân tích cú pháp từ trên xuống này, bạn bắt đầu với một công thức xác định chương trình:

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.

// Lưu ý cách id được bao thanh toán và sau đó kết hợp khi tạo cây.

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) "*/";

Đáng chú ý là cách ngôn ngữ trình phân tích cú pháp xử lý bình luận và phục hồi lỗi.

Tôi nghĩ rằng tôi đã trả lời câu hỏi. Đã viết một phần lớn của người kế nhiệm SLIC, ngôn ngữ cc ở đây. Không có trình biên dịch cho nó được nêu ra. Nhưng tôi có thể biên dịch nó thành mã lắp ráp, các hàm cm hoặc c ++ trần trụi.


0

Có, bạn có thể viết một trình biên dịch cho một ngôn ngữ bằng ngôn ngữ đó. Không, bạn không cần một trình biên dịch đầu tiên cho ngôn ngữ đó để bootstrap.

Những gì bạn cần để bootstrap là việc thực hiện ngôn ngữ. Đó có thể là trình biên dịch hoặc trình thông dịch.

Trong lịch sử, các ngôn ngữ thường được coi là ngôn ngữ được giải thích hoặc ngôn ngữ được biên dịch. Thông dịch viên chỉ được viết cho cái trước và trình biên dịch chỉ được viết cho cái sau. Vì vậy, thông thường nếu trình biên dịch sẽ được viết cho một ngôn ngữ, trình biên dịch đầu tiên sẽ được viết bằng một số ngôn ngữ khác để khởi động nó, sau đó, tùy chọn, trình biên dịch sẽ được viết lại cho ngôn ngữ chủ đề. Nhưng viết một thông dịch viên bằng ngôn ngữ khác thay vào đó là một lựa chọn.

Đây không chỉ là lý thuyết. Tôi tình cờ hiện đang làm điều này bản thân mình. Tôi đang làm việc trên một trình biên dịch cho một ngôn ngữ, Salmon, mà tôi đã tự phát triển. Lần đầu tiên tôi tạo trình biên dịch Salmon bằng C và bây giờ tôi đang viết trình biên dịch bằng Salmon, vì vậy tôi có thể khiến trình biên dịch Salmon hoạt động mà không bao giờ có trình biên dịch cho Salmon được viết bằng bất kỳ ngôn ngữ nào khác.


-1

Có lẽ bạn có thể viết một BNF mô tả BNF.


4
Bạn thực sự có thể (điều đó cũng không khó), nhưng ứng dụng thực tế duy nhất của nó sẽ nằm trong trình tạo trình phân tích cú pháp.
Daniel Spiewak

Quả thực tôi đã sử dụng chính phương thức đó để tạo trình tạo trình phân tích cú pháp LIME. Một đại diện dạng bảng bị hạn chế, đơn giản hóa, của metagrammar đi qua một trình phân tích cú pháp gốc đệ quy đơn giản. Sau đó, LIME tạo trình phân tích cú pháp cho ngôn ngữ ngữ pháp và sau đó nó sử dụng trình phân tích cú pháp đó để đọc ngữ pháp mà ai đó thực sự quan tâm trong việc tạo trình phân tích cú pháp. Điều này có nghĩa là tôi không phải biết cách viết những gì tôi vừa viết. Nó cảm thấy như ma thuật.
Ian

Thật ra bạn không thể, vì BNF không thể mô tả chính nó. Bạn cần một biến thể như biến thể được sử dụng trong yacc nơi các ký hiệu không đầu cuối không được trích dẫn.
Hầu tước Lorne

1
Bạn không thể sử dụng bnf để xác định bnf vì <> không thể được nhận ra. EBNF đã sửa lỗi đó bằng cách trích dẫn mã thông báo chuỗi không đổi của ngôn ngữ.
GK
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.