Làm thế nào được tạo ra trong một trình biên dịch hiện đại?


15

Ý tôi là ở đây là làm thế nào để chúng ta đi từ một số mẫu T add(T a, T b) ...vào mã được tạo? Tôi đã nghĩ ra một vài cách để đạt được điều này, chúng tôi lưu trữ hàm chung trong AST Function_Nodevà sau đó mỗi lần chúng tôi sử dụng nó, chúng tôi lưu trữ trong nút chức năng ban đầu một bản sao của chính nó với tất cả các loại được Tthay thế bằng các loại đang được sử dụng. Ví dụ add<int>(5, 6)sẽ lưu trữ một bản sao của hàm chung cho addvà thay thế tất cả các loại T trong bản sao bằng int.

Vì vậy, nó sẽ trông giống như:

struct Function_Node {
    std::string name; // etc.
    Type return_type;
    std::vector<std::pair<Type, std::string>> arguments;
    std::vector<Function_Node> copies;
};

Sau đó, bạn có thể tạo mã cho những thứ này và khi bạn truy cập vào Function_Nodenơi danh sách các bản sao copies.size() > 0, bạn gọi visitFunctiontrên tất cả các bản sao.

visitFunction(Function_Node& node) {
    if (node.copies.size() > 0) {
        for (auto& node : nodes.copies) {
            visitFunction(node);
        }
        // it's a generic function so we don't want
        // to emit code for this.
        return;
    }
}

Điều này sẽ làm việc tốt? Làm thế nào để trình biên dịch hiện đại tiếp cận vấn đề này? Tôi nghĩ có lẽ một cách khác để làm điều này là bạn có thể đưa các bản sao vào AST để nó chạy qua tất cả các giai đoạn ngữ nghĩa. Tôi cũng nghĩ có lẽ bạn có thể tạo chúng ở dạng ngay lập tức như MIR của Rust hoặc Swifts SIL chẳng hạn.

Mã của tôi được viết bằng Java, các ví dụ ở đây là C ++ vì nó ít dài dòng hơn cho các ví dụ - nhưng về cơ bản nguyên tắc là giống nhau. Mặc dù có thể có một vài lỗi vì nó chỉ được viết bằng tay trong hộp câu hỏi.

Lưu ý rằng tôi có nghĩa là trình biên dịch hiện đại như cách tốt nhất để tiếp cận vấn đề này là gì. Và khi tôi nói chung chung, tôi không có ý nghĩa như các thế hệ Java nơi họ sử dụng kiểu xóa.


Trong C ++ (các ngôn ngữ lập trình khác có khái quát, nhưng mỗi ngôn ngữ thực hiện nó khác nhau), về cơ bản, nó là một hệ thống vĩ mô thời gian biên dịch khổng lồ. Mã thực tế được tạo bằng cách sử dụng loại thay thế.
Robert Harvey

Tại sao không tẩy xóa? Hãy nhớ rằng không chỉ Java làm điều đó và nó không phải là một kỹ thuật tồi (tùy thuộc vào yêu cầu của bạn).
Andres F.

@AresresF. Tôi nghĩ rằng theo cách mà ngôn ngữ của tôi hoạt động, nó sẽ không hoạt động tốt.
Jon Flow

2
Tôi nghĩ bạn nên làm rõ loại nói chung mà bạn đang nói về. Ví dụ, các mẫu C ++, chung chung C # và chung chung Java đều rất khác nhau. Bạn nói rằng bạn không có nghĩa là khái quát về Java, nhưng bạn không nói ý của bạn là gì.
Svick

2
Điều này thực sự cần tập trung vào hệ thống của một ngôn ngữ để tránh bị mở rộng quá mức
Daenyth

Câu trả lời:


14

Làm thế nào được tạo ra trong một trình biên dịch hiện đại?

Tôi mời bạn đọc mã nguồn của trình biên dịch hiện đại nếu bạn muốn biết trình biên dịch hiện đại hoạt động như thế nào. Tôi sẽ bắt đầu với dự án Roslyn, thực hiện các trình biên dịch C # và Visual Basic.

Cụ thể, tôi chú ý đến mã trong trình biên dịch C # thực hiện các ký hiệu loại:

https://github.com/dotnet/roslyn/tree/master/src/Compilers/CSharp/Portable/Symbols

và bạn cũng có thể muốn xem mã cho các quy tắc chuyển đổi. Có nhiều thứ liên quan đến thao tác đại số của các loại chung.

https://github.com/dotnet/roslyn/tree/master/src/Compilers/CSharp/Portable/Binder/Semantics/Conversions

Tôi đã cố gắng để làm cho cái sau dễ đọc.

Tôi đã nghĩ ra một vài cách để đạt được điều này, chúng tôi lưu trữ hàm chung trong AST là Function_Node và sau đó mỗi lần chúng tôi sử dụng nó, chúng tôi lưu trữ trong nút chức năng ban đầu một bản sao của chính nó với tất cả các loại T được thay thế bằng các loại đang được sử dụng

Bạn đang mô tả các mẫu , không phải khái quát . C # và Visual Basic có chung chung thực tế trong các hệ thống loại của họ.

Tóm lại, họ làm việc như thế này.

  • Chúng tôi bắt đầu bằng cách thiết lập các quy tắc cho những gì chính thức cấu thành một loại tại thời gian biên dịch. Ví dụ: intlà một loại, một tham số loại Tlà một loại, đối với bất kỳ loại nào X, loại mảng X[]cũng là một loại, v.v.

  • Các quy tắc cho thuốc generic liên quan đến sự thay thế. Ví dụ, class C with one type parameterkhông phải là một loại. Đó là một mô hình để tạo ra các loại. class C with one type parameter called T, under substitution with int for T một loại

  • Các quy tắc mô tả mối quan hệ giữa các loại - khả năng tương thích khi gán, cách xác định loại biểu thức, v.v. - được thiết kế và triển khai trong trình biên dịch.

  • Một ngôn ngữ mã byte hỗ trợ các loại chung trong hệ thống siêu dữ liệu của nó được thiết kế và triển khai.

  • Trong thời gian chạy, trình biên dịch JIT biến mã byte thành mã máy; nó chịu trách nhiệm xây dựng mã máy thích hợp với sự chuyên môn hóa chung.

Vì vậy, ví dụ, trong C # khi bạn nói

class C<T> { public void X(T t) { Console.WriteLine(t); } }
...
var c = new C<int>(); 
c.X(123);

sau đó trình biên dịch xác minh rằng trong C<int>, đối số intlà sự thay thế hợp lệ cho Tvà tạo siêu dữ liệu và mã byte tương ứng. Khi chạy, jitter phát hiện ra rằng a C<int>đang được tạo lần đầu tiên và tự động tạo mã máy thích hợp.


9

Hầu hết các triển khai của thuốc generic (hay đúng hơn là: đa hình tham số) đều sử dụng kiểu xóa. Điều này đơn giản hóa rất nhiều vấn đề biên dịch mã chung, nhưng chỉ hoạt động đối với các kiểu được đóng hộp: vì mỗi đối số thực sự là một con trỏ mờ, chúng ta cần một cơ chế điều phối VTable hoặc tương tự để thực hiện các thao tác trên các đối số. Trong Java:

<T extends Addable> T add(T a, T b) { … }

có thể được biên dịch, kiểm tra kiểu và được gọi giống như

Addable add(Addable a, Addable b) { … }

ngoại trừ việc generic cung cấp trình kiểm tra loại với nhiều thông tin hơn tại trang web cuộc gọi. Thông tin bổ sung này có thể được xử lý với các biến loại , đặc biệt là khi các loại chung được suy ra. Trong quá trình kiểm tra loại, mỗi loại chung có thể được thay thế bằng một biến, hãy gọi nó là $T1:

$T1 add($T1 a, $T1 b)

Biến loại sau đó được cập nhật với nhiều sự kiện hơn khi chúng được biết đến, cho đến khi nó có thể được thay thế bằng một loại cụ thể. Thuật toán kiểm tra loại phải được viết theo cách chứa các biến loại này ngay cả khi chúng chưa được phân giải thành loại hoàn chỉnh. Trong chính Java, điều này thường có thể được thực hiện dễ dàng vì loại đối số thường được biết trước loại cuộc gọi hàm cần được biết. Một ngoại lệ đáng chú ý là biểu thức lambda là đối số hàm, yêu cầu sử dụng các biến loại như vậy.

Rất lâu sau, một trình tối ưu hóa có thể tạo mã chuyên biệt cho một tập hợp các đối số nhất định, điều này sau đó sẽ thực sự là một kiểu nội tuyến.

Có thể tránh được VTable cho các đối số được nhập chung nếu hàm chung không thực hiện bất kỳ hoạt động nào trên loại mà chỉ chuyển chúng sang hàm khác. Ví dụ, hàm Haskell call :: (a -> b) -> a -> b; call f x = f xsẽ không phải đóng hộp xđối số. Tuy nhiên, điều này không đòi hỏi một quy ước gọi có thể đi qua các giá trị mà không biết kích thước của chúng, về cơ bản hạn chế nó cho con trỏ.


C ++ rất khác với hầu hết các ngôn ngữ về mặt này. Một lớp hoặc hàm templated (tôi sẽ chỉ thảo luận về các hàm templated ở đây) không thể gọi được. Thay vào đó, các mẫu nên được hiểu là một hàm meta thời gian biên dịch trả về một hàm thực tế. Bỏ qua suy luận đối số mẫu trong giây lát, cách tiếp cận chung sau đó thực hiện theo các bước sau:

  1. Áp dụng mẫu cho các đối số mẫu được cung cấp. Ví dụ, gọi template<class T> T add(T a, T b) { … }như add<int>(1, 2)sẽ cung cấp cho chúng ta chức năng thực tế int __add__T_int(int a, int b)(hoặc bất kỳ cách tiếp cận xáo trộn tên nào được sử dụng).

  2. Nếu mã cho chức năng đó đã được tạo trong đơn vị biên dịch hiện tại, hãy tiếp tục. Mặt khác, tạo mã như thể một hàm int __add__T_int(int a, int b) { … }đã được ghi trong mã nguồn. Điều này liên quan đến việc thay thế tất cả các lần xuất hiện của đối số mẫu bằng các giá trị của nó. Đây có lẽ là một phép biến đổi AST → AST. Sau đó, thực hiện kiểm tra loại trên AST được tạo.

  3. Biên dịch cuộc gọi như thể mã nguồn đã được __add__T_int(1, 2).

Lưu ý rằng các mẫu C ++ có tương tác phức tạp với cơ chế giải quyết quá tải, điều mà tôi không muốn mô tả ở đây. Cũng lưu ý rằng việc tạo mã này làm cho không thể có một phương thức templated cũng là ảo - một cách tiếp cận dựa trên kiểu xóa không bị hạn chế đáng kể này.


Điều này có ý nghĩa gì đối với trình biên dịch và / hoặc ngôn ngữ của bạn? Bạn phải suy nghĩ cẩn thận về loại thuốc generic mà bạn muốn cung cấp. Loại xóa trong trường hợp không có suy luận kiểu là cách tiếp cận đơn giản nhất có thể nếu bạn hỗ trợ các kiểu đóng hộp. Chuyên môn hóa mẫu có vẻ khá đơn giản, nhưng thường liên quan đến việc xáo trộn tên và (đối với nhiều đơn vị biên dịch) sao chép đáng kể trong đầu ra, vì các mẫu được khởi tạo tại trang web cuộc gọi, không phải trang web định nghĩa.

Cách tiếp cận mà bạn đã chỉ ra về cơ bản là cách tiếp cận khuôn mẫu giống như C ++. Tuy nhiên, bạn lưu trữ các mẫu chuyên biệt / được khởi tạo dưới dạng các phiên bản của Cameron. Điều này là sai lệch: chúng không giống nhau về mặt khái niệm và các tức thời khác nhau của một chức năng có thể có các loại cực kỳ khác nhau. Điều này sẽ làm phức tạp mọi thứ trong thời gian dài nếu bạn cũng cho phép quá tải chức năng. Thay vào đó, bạn sẽ cần một khái niệm về một tập hợp quá tải có chứa tất cả các hàm và mẫu có thể có chung tên. Ngoại trừ việc giải quyết quá tải, bạn có thể coi các mẫu khởi tạo khác nhau hoàn toàn tách biệt với nhau.

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.