Phần tổng hợp gốc: 0x11, Tin tức tầm nhìn xa
Phần tổng hợp gốc: 0x11, Tin tức tầm nhìn xa
Máy ảo Ethereum (EVM) là một máy Turing 256-bit, dựa trên ngăn xếp, có thể truy cập toàn cầu. Do kiến trúc khác biệt đáng kể so với các máy ảo và máy vật lý khác, EVM yêu cầu DSL ngôn ngữ dành riêng cho miền (Lưu ý: ngôn ngữ dành riêng cho miền đề cập đến ngôn ngữ máy tính tập trung vào một miền ứng dụng nhất định).
Trong bài viết này, chúng tôi xem xét tính tiên tiến trong thiết kế EVM DSL, giới thiệu sáu ngôn ngữ Solidity, Vyper, Fe, Huff, Yul và ETK.
phiên bản ngôn ngữ
Solidity: 0.8.19
Vyper: 0.3.7
Fe: 0.21.0
Huff: 0.3.1
ETK: 0.2.1
Yul: 0.8.19
Đọc bài viết này yêu cầu bạn phải có hiểu biết cơ bản về EVM, ngăn xếp và lập trình.
Tổng quan về Máy ảo Ethereum
EVM là máy Turing dựa trên ngăn xếp 256-bit. Tuy nhiên, trước khi đi sâu vào trình biên dịch của nó, nên giới thiệu một số tính năng chức năng.
Vì EVM là "Turing hoàn tất", nên nó gặp phải "sự cố tạm dừng". Nói tóm lại, trước khi chương trình thực thi, không có cách nào để xác định liệu nó có chấm dứt trong tương lai hay không. Cách EVM giải quyết vấn đề này là thông qua đơn vị tính toán "Khí", thường tỷ lệ thuận với các tài nguyên vật lý cần thiết để thực hiện một lệnh. Lượng Gas của mỗi giao dịch bị hạn chế và người thực hiện giao dịch phải trả ETH tỷ lệ thuận với lượng Gas được tiêu thụ bởi giao dịch. Một trong những hàm ý của chiến lược này là nếu có hai hợp đồng thông minh giống hệt nhau về chức năng, hợp đồng tiêu thụ ít gas hơn sẽ được chấp nhận nhiều hơn. Điều này dẫn đến các giao thức cạnh tranh để đạt được hiệu quả sử dụng khí cực cao, trong đó các kỹ sư cố gắng giảm thiểu mức tiêu thụ khí cho các nhiệm vụ cụ thể.
Ngoài ra, khi một hợp đồng được gọi, nó sẽ tạo ra một ngữ cảnh thực thi. Trong ngữ cảnh này, hợp đồng có một ngăn xếp để vận hành và xử lý, một phiên bản bộ nhớ tuyến tính để đọc và ghi, một bộ lưu trữ liên tục cục bộ để đọc và ghi hợp đồng và dữ liệu "calldata" được đính kèm với cuộc gọi có thể được đọc nhưng không được ghi lại .
Một lưu ý quan trọng về bộ nhớ là mặc dù không có "giới hạn trên" nhất định đối với kích thước của nó, nhưng nó vẫn là hữu hạn. Chi phí gas của việc mở rộng bộ nhớ là động: một khi đạt đến ngưỡng, chi phí mở rộng bộ nhớ sẽ tăng theo bậc hai, nghĩa là chi phí gas tỷ lệ với bình phương của phân bổ bộ nhớ bổ sung.
Hợp đồng cũng có thể sử dụng một vài hướng dẫn khác nhau để gọi các hợp đồng khác. Lệnh "gọi" gửi dữ liệu và ETH tùy chọn đến hợp đồng mục tiêu, sau đó tạo ngữ cảnh thực thi của chính nó cho đến khi việc thực thi hợp đồng mục tiêu dừng lại. Chỉ thị "staticcall" giống như "call", nhưng thêm một kiểm tra khẳng định rằng không có phần nào của trạng thái chung được cập nhật cho đến khi cuộc gọi tĩnh hoàn tất. Cuối cùng, lệnh "delegatecall" hoạt động giống như "cuộc gọi", ngoại trừ việc nó giữ lại một số thông tin môi trường từ ngữ cảnh trước đó. Điều này thường được sử dụng cho các thư viện bên ngoài và hợp đồng proxy.
Tại sao thiết kế ngôn ngữ lại quan trọng
Các ngôn ngữ dành riêng cho miền (DSL) là cần thiết khi tương tác với các kiến trúc không điển hình. Mặc dù các chuỗi công cụ biên dịch như LLVM tồn tại nhưng việc dựa vào chúng để xử lý các hợp đồng thông minh là không lý tưởng khi tính chính xác của chương trình và hiệu quả tính toán là rất quan trọng.
Tính chính xác của thủ tục rất quan trọng vì hợp đồng thông minh là bất biến theo mặc định và với các thuộc tính của máy ảo chuỗi khối (VM), hợp đồng thông minh là một lựa chọn phổ biến cho các ứng dụng tài chính. Mặc dù tồn tại một giải pháp có thể nâng cấp cho EVM, nhưng tốt nhất thì đó là một bản vá và tệ nhất là một lỗ hổng thực thi mã tùy ý.
Tính hiệu quả của tính toán cũng rất quan trọng, vì việc giảm thiểu tính toán mang lại lợi ích kinh tế nhưng không ảnh hưởng đến tính bảo mật.
Nói tóm lại, EVM DSL phải cân bằng giữa tính chính xác của chương trình và hiệu suất Gas, đồng thời đạt được một trong số chúng bằng cách thực hiện các sự đánh đổi khác nhau mà không phải hy sinh quá nhiều tính linh hoạt.
tổng quan về ngôn ngữ
Đối với mỗi ngôn ngữ, chúng tôi mô tả các tính năng nổi bật và lựa chọn thiết kế của chúng, đồng thời bao gồm một hợp đồng thông minh có chức năng đếm đơn giản. Mức độ phổ biến của từ được xác định dựa trên dữ liệu Tổng giá trị bị khóa (TVL) trên Defi Llama.
Solidity
Solidity là một ngôn ngữ cấp cao có cú pháp tương tự như C, Java và Javascript. Đó là ngôn ngữ phổ biến nhất theo TVL, gấp mười lần TVL của ngôn ngữ tốt nhất tiếp theo. Để tái sử dụng mã, nó sử dụng một mẫu hướng đối tượng, trong đó các hợp đồng thông minh được coi là đối tượng lớp, sử dụng đa kế thừa. Trình biên dịch được viết bằng C++ với kế hoạch chuyển sang Rust trong tương lai.
Các trường hợp đồng có thể thay đổi được lưu trữ trong bộ lưu trữ liên tục trừ khi giá trị của chúng được biết tại thời điểm biên dịch (không đổi) hoặc thời gian triển khai (bất biến). Các phương thức được khai báo trong hợp đồng có thể được khai báo là thuần túy, dạng xem, phải trả hoặc không phải trả theo mặc định nhưng trạng thái có thể được sửa đổi. Các phương thức thuần túy không đọc từ môi trường thực thi, chúng cũng không thể đọc hoặc ghi vào bộ lưu trữ liên tục; nghĩa là, các phương thức thuần túy sẽ luôn trả về cùng một đầu ra với cùng một đầu vào và chúng không tạo ra tác dụng phụ. Các phương thức xem có thể đọc dữ liệu từ bộ lưu trữ liên tục hoặc môi trường thực thi, nhưng chúng không thể ghi vào bộ nhớ liên tục, cũng như không thể tạo các tác dụng phụ như nối thêm nhật ký giao dịch. Phương thức thanh toán có thể đọc và ghi bộ lưu trữ liên tục, đọc dữ liệu từ môi trường thực thi, tạo ra các tác dụng phụ và có thể nhận ETH được đính kèm với lệnh gọi. Phương thức không phải trả cũng giống như phương thức phải trả, nhưng có kiểm tra thời gian chạy để khẳng định rằng không có ETH nào được đính kèm với ngữ cảnh thực thi hiện tại.
Lưu ý: Việc đính kèm ETH vào giao dịch khác với việc thanh toán phí gas. ETH đính kèm được nhận theo hợp đồng, có thể được chấp nhận hoặc từ chối bằng cách khôi phục bối cảnh.
Khi được khai báo trong phạm vi của hợp đồng, các phương thức có thể chỉ định một trong bốn công cụ sửa đổi khả năng hiển thị: riêng tư, nội bộ, công khai hoặc bên ngoài. Các phương thức riêng tư có thể được truy cập nội bộ thông qua lệnh "nhảy" trong hợp đồng hiện tại. Mọi hợp đồng được kế thừa không thể truy cập trực tiếp vào các phương thức riêng tư. Các phương thức nội bộ cũng có thể được truy cập nội bộ thông qua lệnh "nhảy", nhưng các hợp đồng kế thừa có thể sử dụng các phương thức nội bộ trực tiếp. Các phương thức công khai có thể được truy cập bằng các hợp đồng bên ngoài thông qua lệnh "gọi", lệnh này tạo ra ngữ cảnh thực thi mới và bên trong thông qua các bước nhảy khi gọi phương thức trực tiếp. Các phương thức công khai cũng có thể được truy cập từ trong cùng một hợp đồng trong ngữ cảnh thực thi mới bằng cách đặt trước lệnh gọi phương thức bằng "this.". Chỉ có thể truy cập phương thức bên ngoài thông qua lệnh "gọi", cho dù đó là từ một hợp đồng khác hay trong cùng một hợp đồng, bạn cần thêm "this." trước lệnh gọi phương thức.
Lưu ý: Lệnh "nhảy" điều khiển bộ đếm chương trình và lệnh "gọi" tạo bối cảnh thực thi mới trong thời gian thực hiện hợp đồng đích. Nếu có thể, sử dụng "nhảy" thay vì "gọi" sẽ tiết kiệm xăng hơn.
Solidity cũng cung cấp ba cách để xác định thư viện. Đầu tiên là một thư viện bên ngoài, là một hợp đồng phi trạng thái được triển khai trên chuỗi một cách riêng biệt, được liên kết động khi hợp đồng được gọi và được truy cập thông qua lệnh "delegatecall". Đây là cách tiếp cận ít phổ biến nhất do hỗ trợ công cụ kém cho các thư viện bên ngoài, "các cuộc gọi ủy nhiệm" rất tốn kém, nó phải tải thêm mã từ bộ lưu trữ liên tục và yêu cầu nhiều giao dịch để triển khai. Các thư viện nội bộ được định nghĩa giống như các thư viện bên ngoài, ngoại trừ mỗi phương thức phải được định nghĩa là một phương thức nội bộ. Tại thời điểm biên dịch, thư viện nội bộ được nhúng vào hợp đồng cuối cùng và trong giai đoạn phân tích mã chết, các phương thức không sử dụng trong thư viện sẽ bị xóa. Cách thứ ba tương tự như thư viện nội bộ, nhưng thay vì xác định cấu trúc dữ liệu và chức năng bên trong thư viện, chúng được xác định ở cấp tệp và có thể được nhập và sử dụng trực tiếp trong hợp đồng cuối cùng. Phương pháp thứ ba cung cấp khả năng tương tác giữa người và máy tính tốt hơn, bạn có thể sử dụng cấu trúc dữ liệu tùy chỉnh, áp dụng các hàm trong phạm vi toàn cầu và áp dụng các toán tử bí danh cho một số hàm ở một mức độ nhất định.
Trình biên dịch cung cấp hai lượt tối ưu hóa. Đầu tiên là trình tối ưu hóa mức hướng dẫn thực hiện các hoạt động tối ưu hóa trên mã byte cuối cùng. Thứ hai là sự gia tăng gần đây trong việc sử dụng ngôn ngữ Yul (được mô tả chi tiết sau) làm biểu diễn trung gian (IR) trong quá trình biên dịch, sau đó tối ưu hóa mã Yul được tạo.
Để tương tác với các phương thức công khai và bên ngoài trong một hợp đồng, Solidity chỉ định tiêu chuẩn Giao diện nhị phân ứng dụng (ABI) để tương tác với các hợp đồng của nó. Hiện tại, Solidity ABI được coi là tiêu chuẩn thực tế cho EVM DSL. Tiêu chuẩn Ethereum ERC chỉ định giao diện bên ngoài được triển khai theo hướng dẫn kiểu và đặc điểm kỹ thuật ABI của Solidity. Các ngôn ngữ khác cũng tuân theo đặc điểm kỹ thuật ABI của Solidity với một số sai lệch.
Solidity cũng cung cấp các khối Yul nội tuyến, cho phép truy cập cấp thấp vào tập lệnh EVM. Các khối Yul chứa một tập hợp con chức năng Yul, xem phần Yul để biết chi tiết. Điều này thường được sử dụng để tối ưu hóa gas, để tận dụng các tính năng không được hỗ trợ bởi cú pháp cấp cao và để tùy chỉnh lưu trữ, bộ nhớ và dữ liệu cuộc gọi.
Do sự phổ biến của Solidity, các công cụ dành cho nhà phát triển rất hoàn thiện và được thiết kế tốt, Foundry là một đại diện nổi bật về mặt này.
Đây là một hợp đồng đơn giản được viết bằng Solidity:

Vyper
Vyper là một ngôn ngữ cấp cao có cú pháp tương tự như Python. Nó gần như là một tập hợp con của Python với một số khác biệt nhỏ. Đây là DSL EVM phổ biến thứ hai. Vyper được tối ưu hóa cho tính bảo mật, khả năng đọc, khả năng kiểm tra và hiệu suất gas. Nó không sử dụng các mẫu hướng đối tượng, lắp ráp nội tuyến và không hỗ trợ tái sử dụng mã. Trình biên dịch của nó được viết bằng Python.
Các biến được lưu trữ trong bộ lưu trữ liên tục được khai báo ở cấp độ tệp. Nếu giá trị của chúng được biết tại thời điểm biên dịch, chúng có thể được khai báo là "không đổi"; nếu giá trị của chúng được biết tại thời điểm triển khai, chúng có thể được khai báo là "bất biến"; nếu chúng được đánh dấu là công khai, hợp đồng cuối cùng sẽ hiển thị thông báo đọc- chức năng duy nhất cho biến đó. Các giá trị của hằng số và bất biến được truy cập nội bộ bằng tên của chúng, nhưng các biến có thể thay đổi trong bộ lưu trữ liên tục có thể được truy cập bằng cách thêm tiền tố vào tên của chúng bằng "self.". Điều này hữu ích để ngăn xung đột không gian tên giữa các biến được lưu trữ, tham số chức năng và biến cục bộ.
Tương tự như Solidity, Vyper cũng sử dụng các thuộc tính chức năng để thể hiện khả năng hiển thị và khả năng thay đổi của các chức năng. Các chức năng được đánh dấu là "@external" có thể được truy cập từ các hợp đồng bên ngoài thông qua lệnh "gọi". Các chức năng được đánh dấu là "@internal" chỉ có thể được truy cập trong cùng một hợp đồng và phải có tiền tố là "self.". Hàm được đánh dấu "@pure" không thể đọc từ môi trường thực thi hoặc bộ lưu trữ liên tục, cũng như không thể ghi vào bộ lưu trữ liên tục hoặc tạo ra bất kỳ tác dụng phụ nào. Các chức năng được đánh dấu bằng "@view" có thể đọc dữ liệu từ môi trường thực thi hoặc bộ lưu trữ liên tục, nhưng không thể ghi vào bộ lưu trữ liên tục hoặc tạo tác dụng phụ. Các chức năng được đánh dấu bằng "@payable" có thể đọc hoặc ghi vào bộ lưu trữ liên tục, tạo tác dụng phụ và chấp nhận hoặc nhận ETH. Các hàm không khai báo thuộc tính khả năng thay đổi này mặc định là không thể thanh toán, nghĩa là chúng hoạt động giống như các hàm phải trả, nhưng không thể nhận ETH.
Trình biên dịch Vyper cũng chọn lưu trữ các biến cục bộ trong bộ nhớ thay vì trên ngăn xếp. Điều này làm cho các hợp đồng trở nên đơn giản và hiệu quả hơn, đồng thời giải quyết vấn đề "ngăn xếp quá sâu" thường gặp ở các ngôn ngữ cấp cao khác. Tuy nhiên, điều này cũng đi kèm với một số sự đánh đổi.
Ngoài ra, vì bố cục bộ nhớ phải được biết tại thời điểm biên dịch, dung lượng tối đa của loại động cũng phải được biết tại thời điểm biên dịch, đây là một hạn chế. Ngoài ra, việc phân bổ lượng lớn bộ nhớ có thể dẫn đến mức tiêu thụ khí phi tuyến tính, như đã đề cập trong phần tổng quan về EVM. Tuy nhiên, đối với nhiều trường hợp sử dụng, chi phí gas này là không đáng kể.
Mặc dù Vyper không hỗ trợ lắp ráp nội tuyến, nhưng nó cung cấp nhiều chức năng tích hợp hơn để đảm bảo rằng hầu hết mọi chức năng trong Solidity và Yul cũng có thể được triển khai trong Vyper. Hoạt động bit cấp thấp, cuộc gọi bên ngoài và hoạt động hợp đồng proxy có thể được truy cập thông qua các chức năng tích hợp sẵn và có thể triển khai bố cục lưu trữ tùy chỉnh bằng cách cung cấp tệp lớp phủ tại thời điểm biên dịch.
Vyper không có bộ công cụ phát triển phong phú, nhưng nó có nhiều công cụ tích hợp chặt chẽ hơn và cũng có thể cắm vào các công cụ phát triển Solidity. Các công cụ đáng chú ý của Vyper bao gồm trình thông dịch Titanaboa, có nhiều công cụ tích hợp sẵn liên quan đến EVM và Vyper để thử nghiệm và phát triển, và Dasy, một Lisp dựa trên Vyper với khả năng thực thi mã thời gian biên dịch.
Đây là một hợp đồng đơn giản được viết bằng Vyper:

Fe
Fe là một ngôn ngữ giống như Rust cấp cao hiện đang được phát triển tích cực, với hầu hết các tính năng chưa khả dụng. Trình biên dịch của nó chủ yếu được viết bằng Rust, nhưng sử dụng Yul làm biểu diễn trung gian (IR), dựa trên trình tối ưu hóa Yul được viết bằng C++. Điều này dự kiến sẽ thay đổi với việc bổ sung Sonatina, một phụ trợ gốc Rust. Fe sử dụng các mô-đun để chia sẻ mã, do đó, thay vì sử dụng các mẫu hướng đối tượng, mã được sử dụng lại thông qua một hệ thống dựa trên mô-đun nơi các biến, loại và hàm được khai báo trong các mô-đun, có thể được nhập theo cách giống như Rust.
Các biến lưu trữ lâu dài được khai báo ở cấp hợp đồng và không thể truy cập công khai nếu không có hàm getter được xác định thủ công. Các hằng số có thể được khai báo ở cấp độ tệp hoặc mô-đun và có thể được truy cập bên trong hợp đồng. Biến thời gian triển khai bất biến hiện không được hỗ trợ.
Các phương thức có thể được khai báo ở cấp độ mô-đun hoặc trong một hợp đồng, các giá trị mặc định là thuần túy và riêng tư. Để công khai một phương thức hợp đồng, định nghĩa phải được đặt trước từ khóa "pub", giúp cho phương thức đó có thể truy cập được từ bên ngoài. Để đọc từ một biến lưu trữ liên tục, tham số đầu tiên của phương thức phải là "self", đặt trước tên biến là "self." cấp cho phương thức quyền truy cập chỉ đọc vào biến lưu trữ cục bộ. Để đọc và ghi vào bộ lưu trữ liên tục, đối số đầu tiên phải là "mut self". Từ khóa "mut" chỉ ra rằng bộ nhớ của hợp đồng có thể thay đổi trong quá trình thực thi phương thức. Việc truy cập các biến môi trường được thực hiện bằng cách chuyển tham số "Ngữ cảnh" cho phương thức, thường được đặt tên là "ctx".
Các chức năng và loại tùy chỉnh có thể được khai báo ở cấp độ mô-đun. Theo mặc định, các mục mô-đun là riêng tư và không thể truy cập trừ khi từ khóa "pub" được thêm vào. Tuy nhiên, đừng nhầm lẫn với từ khóa "pub" ở cấp độ hợp đồng. Các thành viên công khai của mô-đun chỉ có thể được truy cập bên trong hợp đồng cuối cùng hoặc các mô-đun khác.
Fe hiện không hỗ trợ lắp ráp nội tuyến, thay vào đó, các hướng dẫn được bao bọc bởi nội tại của trình biên dịch hoặc các hàm đặc biệt giải quyết các hướng dẫn tại thời điểm biên dịch.
Fe tuân theo hệ thống kiểu và cú pháp của Rust, hỗ trợ các bí danh kiểu, enums với các kiểu con, đặc điểm và kiểu chung. Hỗ trợ cho điều này hiện đang bị hạn chế, nhưng công việc đang được tiến hành. Các đặc điểm có thể được xác định và triển khai cho các loại khác nhau, nhưng không hỗ trợ các ràng buộc chung cũng như đặc điểm. Enums hỗ trợ phân nhóm và các phương thức có thể được triển khai trên chúng, nhưng chúng không thể được mã hóa trong các hàm bên ngoài. Mặc dù hệ thống loại của Fe vẫn đang trong quá trình hoàn thiện, nhưng nó cho thấy rất nhiều tiềm năng để các nhà phát triển viết mã kiểm tra thời gian biên dịch, an toàn hơn.
Đây là một hợp đồng đơn giản được viết bằng Fe:

Huff
Huff là một ngôn ngữ hợp ngữ với điều khiển ngăn xếp thủ công và sự trừu tượng hóa tối thiểu của tập lệnh EVM. Thông qua chỉ thị "#include", bất kỳ tệp Huff nào được bao gồm đều có thể được phân tích cú pháp tại thời điểm biên dịch để sử dụng lại mã. Ban đầu được viết bởi nhóm Aztec cho các thuật toán đường cong elliptic cực kỳ tối ưu, trình biên dịch này sau đó được viết lại bằng TypeScript và sau đó là Rust.
Các hằng số phải được xác định tại thời điểm biên dịch, các giá trị bất biến hiện không được hỗ trợ và các biến lưu trữ liên tục không được xác định rõ ràng trong ngôn ngữ. Vì các biến lưu trữ được đặt tên là một khái niệm trừu tượng cấp cao, nên việc ghi vào bộ lưu trữ liên tục trong Huff được thực hiện thông qua opcodes "sstore" để ghi và "sload" để đọc. Bố cục lưu trữ tùy chỉnh có thể do người dùng xác định hoặc theo quy ước bắt đầu từ 0 và tăng từng biến bằng cách sử dụng "FREE_STORAGE_POINTER" nội tại của trình biên dịch. Tạo một biến được lưu trữ có thể truy cập từ bên ngoài yêu cầu xác định thủ công một đường dẫn mã có thể đọc và trả lại biến cho người gọi.
Các chức năng bên ngoài cũng là sự trừu tượng được giới thiệu bởi các ngôn ngữ cấp cao, vì vậy không có khái niệm về các chức năng bên ngoài trong Huff. Tuy nhiên, hầu hết các dự án tuân theo các mức độ khác nhau của thông số kỹ thuật ABI của các ngôn ngữ cấp cao khác, phổ biến nhất là Solidity. Một mẫu phổ biến là xác định một "bộ lập lịch" tải dữ liệu cuộc gọi thô và sử dụng dữ liệu đó để kiểm tra sự khớp với bộ chọn chức năng. Nếu nó khớp, mã tiếp theo của nó sẽ được thực thi. Vì các bộ lập lịch do người dùng xác định nên chúng có thể tuân theo các mẫu lập lịch khác nhau. Solidity sắp xếp các bộ chọn trong bộ lập lịch của nó theo thứ tự bảng chữ cái theo tên, Vyper sắp xếp theo số và thực hiện tìm kiếm nhị phân trong thời gian chạy và hầu hết các bộ lập lịch Huff sắp xếp theo tần suất sử dụng chức năng dự kiến, hiếm khi sử dụng bảng nhảy. Hiện tại, các bảng nhảy không được hỗ trợ nguyên bản trong EVM, do đó, cần có hướng dẫn nội quan như "bản mã" để triển khai chúng.
Các hàm nội tại được xác định bằng cách sử dụng chỉ thị "#define fn", có thể chấp nhận các tham số mẫu để linh hoạt và chỉ định độ sâu ngăn xếp dự kiến ở đầu và cuối hàm. Vì các chức năng này là nội bộ nên không thể truy cập chúng từ bên ngoài và truy cập nội bộ yêu cầu sử dụng lệnh "nhảy".
Luồng điều khiển khác như câu lệnh điều kiện và câu lệnh vòng lặp có thể được xác định bằng cách sử dụng đích nhảy. Mục tiêu nhảy được xác định bởi một mã định danh theo sau là dấu hai chấm. Việc nhảy tới các mục tiêu này có thể được thực hiện bằng cách đẩy một mã định danh lên ngăn xếp và thực hiện lệnh nhảy. Điều này giải quyết thành phần bù mã byte tại thời điểm biên dịch.
Macro được xác định bởi "#define macro", nếu không thì chúng giống như các hàm bên trong. Sự khác biệt chính là macro không tạo lệnh "nhảy" tại thời điểm biên dịch mà thay vào đó, sao chép trực tiếp phần thân của macro vào mỗi lệnh gọi trong tệp.
Thiết kế này cân nhắc mối quan hệ giữa việc giảm các bước nhảy tùy ý và chi phí Gas trong thời gian chạy, với chi phí tăng kích thước mã khi được gọi nhiều lần. Macro "MAIN" được coi là điểm vào của hợp đồng và lệnh đầu tiên trong phần thân của nó sẽ là lệnh đầu tiên trong mã byte thời gian chạy.
Các tính năng khác được tích hợp trong trình biên dịch bao gồm tạo hàm băm sự kiện để ghi nhật ký, bộ chọn chức năng để gửi đi, bộ chọn lỗi để xử lý lỗi, bộ kiểm tra kích thước mã cho các hàm và macro nội tại, v.v.
Lưu ý: các bình luận ngăn xếp như "// [đếm]" là không bắt buộc, chúng chỉ được sử dụng để biểu thị trạng thái của ngăn xếp khi kết thúc thực thi dòng.
Đây là một hợp đồng đơn giản được viết bằng Huff:

ETK
Bộ công cụ EVM (ETK) là một ngôn ngữ hợp ngữ với quản lý ngăn xếp thủ công và trừu tượng hóa tối thiểu. Mã có thể được sử dụng lại thông qua các chỉ thị "%include" và "%import" và trình biên dịch được viết bằng Rust.
Một điểm khác biệt đáng chú ý giữa Huff và ETK là Huff thêm một chút trừu tượng vào initcode, còn được gọi là mã hàm tạo, có thể bị ghi đè bằng cách xác định macro "CONSTRUCTOR" đặc biệt. Trong ETK, những thứ này không được trừu tượng hóa, mã initcode và mã thời gian chạy phải được xác định cùng nhau.
Tương tự như Huff, ETK đọc và ghi vào bộ lưu trữ liên tục thông qua lệnh "sload" và "sstore". Tuy nhiên, không có từ khóa hằng số hoặc bất biến, nhưng các hằng số có thể được mô phỏng bằng cách sử dụng một trong hai loại macro trong ETK, macro biểu thức. Các macro biểu thức không được phân tích cú pháp dưới dạng chỉ thị, nhưng tạo ra các giá trị số có thể được sử dụng trong các chỉ thị khác. Ví dụ: nó có thể không tạo chính xác lệnh "push" nhưng nó có thể tạo một số để đưa vào lệnh "push".
Như đã đề cập trước đó, các hàm ngoại là các khái niệm ngôn ngữ cấp cao, do đó, việc hiển thị các đường dẫn mã ra bên ngoài yêu cầu tạo một bộ điều phối bộ chọn hàm.
Các hàm nội tại không được xác định rõ ràng như trong các ngôn ngữ khác, thay vào đó, bạn có thể chỉ định các bí danh do người dùng xác định cho các mục tiêu nhảy và chuyển đến chúng theo tên của chúng. Điều này cũng cho phép các luồng điều khiển khác như vòng lặp và câu lệnh điều kiện.
ETK hỗ trợ hai loại macro. Đầu tiên là các macro biểu thức có thể chấp nhận bất kỳ số lượng đối số nào và trả về một giá trị số có thể được sử dụng trong các hướng dẫn khác. Các macro biểu thức không tạo ra các hướng dẫn, nhưng các giá trị hoặc hằng số ngay lập tức. Tuy nhiên, các macro chỉ thị chấp nhận bất kỳ số lượng đối số nào và tạo ra bất kỳ số lượng chỉ thị nào tại thời điểm biên dịch. Macro hướng dẫn trong ETK tương tự như macro Huff.
Đây là một hợp đồng đơn giản được viết bằng ETK:

Yul
Yul là một ngôn ngữ hợp ngữ với luồng điều khiển cấp cao và rất nhiều tính trừu tượng. Nó là một phần của chuỗi công cụ Solidity và có thể tùy chọn được sử dụng trong quy trình xây dựng Solidity. Yul không hỗ trợ tái sử dụng mã vì nó được dùng làm mục tiêu biên dịch hơn là một ngôn ngữ độc lập. Trình biên dịch của nó được viết bằng C++ và có kế hoạch di chuyển nó sang Rust cùng với phần còn lại của đường dẫn Solidity.
Trong Yul, mã được chia thành các đối tượng, có thể chứa mã, dữ liệu và các đối tượng lồng nhau. Do đó, không có hằng số hoặc chức năng bên ngoài nào trong Yul. Một bộ điều phối bộ chọn chức năng cần được xác định để hiển thị các đường dẫn mã ra thế giới bên ngoài.
Ngoại trừ các hướng dẫn luồng điều khiển và ngăn xếp, hầu hết các hướng dẫn được hiển thị dưới dạng các hàm trong Yul. Các chỉ thị có thể được lồng vào nhau để giảm kích thước mã và cũng có thể được gán cho các biến tạm thời rồi chuyển sang các chỉ thị khác để sử dụng. Các nhánh có điều kiện có thể sử dụng các khối "if", khối này được thực thi nếu giá trị khác 0, nhưng không có khối "else", do đó, việc xử lý nhiều đường dẫn mã yêu cầu sử dụng "switch" để xử lý bất kỳ số lượng trường hợp nào và một " mặc định" tùy chọn dự phòng. Các vòng lặp có thể được thực hiện bằng cách sử dụng vòng lặp "for"; trong khi cú pháp của nó khác với các ngôn ngữ cấp cao khác, nó cung cấp chức năng cơ bản giống nhau. Các hàm nội tại có thể được định nghĩa bằng cách sử dụng từ khóa "hàm" và tương tự như các định nghĩa hàm trong các ngôn ngữ cấp cao.
Hầu hết các chức năng trong Yul được hiển thị trong Solidity bằng cách sử dụng các khối lắp ráp nội tuyến. Điều này cho phép các nhà phát triển phá vỡ sự trừu tượng, viết chức năng tùy chỉnh hoặc sử dụng Yul trong chức năng không có sẵn trong cú pháp cấp cao. Tuy nhiên, việc sử dụng tính năng này đòi hỏi sự hiểu biết sâu sắc về hành vi của Solidity về calldata, bộ nhớ và lưu trữ.
Ngoài ra còn có một số chức năng độc đáo. Các hàm "datasize", "dataoffset" và "datacopy" thao tác các đối tượng Yul thông qua bí danh chuỗi của chúng. Các hàm "setimmutable" và "loadimmutable" cho phép đặt và tải các tham số không thay đổi trong hàm tạo, mặc dù việc sử dụng chúng bị hạn chế. Chức năng "bảo vệ bộ nhớ" có nghĩa là chỉ phân bổ một phạm vi bộ nhớ nhất định, cho phép trình biên dịch sử dụng bộ nhớ bên ngoài phạm vi được bảo vệ để tối ưu hóa bổ sung. Cuối cùng, "nguyên văn" cho phép sử dụng các lệnh mà trình biên dịch Yul không biết.
Đây là một hợp đồng đơn giản được viết bằng Yul:

Đặc điểm của DSL EVM tốt
Một EVM DSL tốt nên học hỏi từ những điểm mạnh và điểm yếu của từng ngôn ngữ được liệt kê ở đây và cũng phải bao gồm những điều cơ bản trong hầu hết các ngôn ngữ hiện đại, chẳng hạn như câu lệnh điều kiện, khớp mẫu, vòng lặp, hàm, v.v. Mã phải rõ ràng, thêm các trừu tượng ngầm tối thiểu để tăng tính thẩm mỹ hoặc khả năng đọc mã. Trong các môi trường quan trọng về tính chính xác, rủi ro cao, mọi dòng mã phải được giải thích rõ ràng. Ngoài ra, một hệ thống mô-đun được xác định rõ ràng sẽ là trung tâm của bất kỳ ngôn ngữ tuyệt vời nào. Nó phải nêu rõ mục nào được xác định trong phạm vi nào và mục nào có thể truy cập được. Theo mặc định, mọi mục trong mô-đun phải ở chế độ riêng tư, chỉ những mục công khai rõ ràng mới có thể truy cập công khai bên ngoài.
Trong một môi trường hạn chế về tài nguyên như EVM, hiệu quả rất quan trọng. Hiệu quả thường đạt được bằng cách cung cấp các khái niệm trừu tượng chi phí thấp, chẳng hạn như thực thi mã thời gian biên dịch thông qua macro, một hệ thống kiểu phong phú để tạo các thư viện có thể tái sử dụng được thiết kế tốt và các trình bao bọc cho các tương tác phổ biến trên chuỗi. Macro tạo mã tại thời điểm biên dịch, điều này rất tốt để giảm mã soạn sẵn cho các hoạt động thông thường và trong các trường hợp như Huff, nó có thể được sử dụng để đánh đổi kích thước mã so với hiệu quả thời gian chạy. Một hệ thống kiểu phong phú cho phép mã biểu cảm hơn, kiểm tra thời gian biên dịch nhiều hơn để bắt lỗi trước khi chạy và khi được kết hợp với nội tại của trình biên dịch được kiểm tra kiểu, có thể loại bỏ nhu cầu về nhiều hợp ngữ nội tuyến. Generics cũng cho phép các giá trị nullable (chẳng hạn như mã bên ngoài) được bao bọc trong các loại "tùy chọn" hoặc các hoạt động dễ bị lỗi (chẳng hạn như lệnh gọi bên ngoài) được bao bọc trong các loại "kết quả". Hai loại này là ví dụ về cách người viết thư viện có thể buộc nhà phát triển xử lý từng kết quả bằng cách xác định đường dẫn mã hoặc giao dịch khôi phục kết quả không thành công. Tuy nhiên, hãy nhớ rằng đây là những trừu tượng thời gian biên dịch giải quyết các bước nhảy có điều kiện đơn giản trong thời gian chạy. Việc buộc các nhà phát triển xử lý mọi kết quả tại thời điểm biên dịch sẽ làm tăng thời gian phát triển ban đầu, nhưng có lợi là ít bất ngờ hơn nhiều trong thời gian chạy.
Tính linh hoạt cũng rất quan trọng đối với các nhà phát triển, vì vậy mặc dù mặc định cho các hoạt động phức tạp phải là lộ trình an toàn và có thể kém hiệu quả hơn, nhưng đôi khi cần sử dụng các đường dẫn mã hiệu quả hơn hoặc các tính năng không được hỗ trợ. Đối với điều này, lắp ráp nội tuyến nên được mở cho các nhà phát triển và không có lan can. Việc lắp ráp nội tuyến của Solidity đặt một số lan can để đơn giản hóa và phân phối trình tối ưu hóa tốt hơn, nhưng các nhà phát triển nên được cấp các quyền này khi họ cần toàn quyền kiểm soát môi trường thực thi.
Tóm lại là
Tóm lại là
liên kết gốc


