Ngoài Solidity, những ngôn ngữ EVM nào khác đáng được quan tâm?
Được viết bởi: jtriley.ethjtriley.eth
Biên soạn bởi: 0x11, Tin tức tầm nhìn xa
Máy ảo Ethereum (EVM) là 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à 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 ta sẽ xem xét tính năng tiên tiến trong thiết kế EVM DSL, bao gồm sáu ngôn ngữ: Solidity, Vyper, Fe, Huff, Yul và ETK.
phiên bản ngôn ngữ
Độ rắn: 0,8,19
Vyper: 0.3.7
Fe: 0,21,0
Hù: 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, stack 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ó, cần giới thiệu một số tính năng chức năng.
Bởi vì EVM là "Turing Complete" nên nó sẽ gặp phải "sự cố dừng". Nói tóm lại, trước khi một chương trình được 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à đo các đơn vị tính toán thông qua "Gas", thường tỷ lệ thuận với tài nguyên vật lý cần thiết để thực hiện các lệnh. Lượng Gas trên mỗi giao dịch bị giới hạn và người khởi tạo giao dịch phải trả ETH theo tỷ lệ Gas mà giao dịch tiêu thụ. Một tác động 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 thì hợp đồng nào tiêu thụ ít gas hơn sẽ được áp dụng 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í đốt cực cao, trong đó các kỹ sư cố gắng giảm thiểu mức tiêu thụ khí đốt 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 bối 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 kho lưu trữ cố định cục bộ để đọc và ghi hợp đồng và dữ liệu được đính kèm với lệnh gọi "calldata" 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 bị giới hạn. Chi phí gas của việc mở rộng bộ nhớ là động: khi đạt đến ngưỡng, chi phí mở rộng bộ nhớ sẽ tăng theo phương trình bậc hai, nghĩa là chi phí gas tỷ lệ thuận với bình phương của việc phân bổ bộ nhớ bổ sung.
Hợp đồng cũng có thể gọi các hợp đồng khác bằng cách sử dụng một số hướng dẫn khác nhau. 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 bối cảnh thực thi của riêng nó cho đến khi việc thực thi hợp đồng mục tiêu dừng lại. Lệnh "staticcall" cũng giống như "call", nhưng thêm một kiểm tra xác nhận rằng không có phần nào của trạng thái chung được cập nhật trước khi lệnh gọi tĩnh hoàn tất. Cuối cùng, lệnh "delegatecall" hoạt động giống như "call" 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
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 trong các tình huống mà độ 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 chương trình rất quan trọng vì hợp đồng thông minh là bất biến theo mặc định và là lựa chọn phổ biến cho các ứng dụng tài chính dựa trên các đặc tính của máy ảo blockchain (VM). Mặc dù có một giải pháp có thể nâng cấp cho EVM, nhưng tốt nhất đây chỉ là một bản vá và tệ nhất là một lỗ hổng thực thi mã tùy ý.
Hiệu quả tính toán cũng rất quan trọng, vì việc giảm thiểu tính toán có lợi ích kinh tế nhưng không ảnh hưởng đến sự an toàn.
Nói tóm lại, DSL EVM phải cân bằng giữa tính chính xác của chương trình và hiệu quả sử dụng gas, đạt được cái này hoặc cái kia bằng cách thực hiện các đá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 sẽ 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 bằng lời nói được xác định dựa trên dữ liệu Tổng giá trị bị khóa (TVL) trên Defi Llama.
Độ rắn chắc
Solidity là ngôn ngữ cấp cao có cú pháp tương tự C, Java và Javascript. Đây là ngôn ngữ phổ biến nhất theo TVL, với TVL cao gấp 10 lần so với ngôn ngữ phổ biến tiếp theo. Để tái sử dụng mã, nó sử dụng mẫu hướng đối tượng, trong đó hợp đồng thông minh được coi là đối tượng lớp, tận dụng tính đa kế thừa. Trình biên dịch được viết bằng C++ và có 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 (hằng số) hoặc thời gian triển khai (không thay đổi). Các phương thức được khai báo trong hợp đồng có thể được khai báo thuần túy, xem, phải trả hoặc không thể thanh toán theo mặc định nhưng có trạng thái có thể sửa đổi. Các phương thức thuần túy không đọc dữ liệu từ môi trường thực thi và không thể đọc hoặc ghi vào bộ lưu trữ liên tục; nghĩa là, với cùng một đầu vào, các phương thức thuần túy sẽ luôn trả về cùng một đầu ra 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ộ lưu trữ liên tục cũng như không thể tạo ra các tác dụng phụ như nối thêm nhật ký giao dịch. Các phương thức phải trả 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ào cuộc 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 để xác nhận rằng không có ETH nào được đính kèm trong bối cảnh thực thi hiện tại.
Lưu ý: Việc đính kèm ETH vào một giao dịch khác với việc trả phí gas, ETH đính kèm sẽ được nhận theo hợp đồng và bạn có thể chọn chấp nhận hoặc từ chối bằng cách khôi phục ngữ cảnh.
Khi được khai báo trong phạm vi hợp đồng, các phương thức có thể chỉ định bốn công cụ sửa đổi mức độ 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 hướng dẫn "nhảy" trong hợp đồng hiện tại. Không có hợp đồng kế thừa nào có 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 trực tiếp các phương thức nội bộ. 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 bối cảnh thực thi mới và nội bộ thông qua các bước nhảy khi phương thức được gọi trực tiếp. Các phương thức công khai cũng có thể được truy cập từ cùng một hợp đồng trong bối cảnh thực thi mới bằng cách thêm "this" vào lệnh gọi phương thức. Chỉ có thể truy cập các phương thức bên ngoài thông qua lệnh "gọi". Cho dù chúng đến từ các hợp đồng khác nhau hay trong cùng một hợp đồng, "điều này" cần phải được thêm vào trước lệnh gọi phương thức.
Lưu ý: Lệnh "nhảy" vận hành bộ đếm chương trình và lệnh "gọi" tạo ra bối cảnh thực thi mới trong quá trình thực hiện hợp đồng đích. Khi có thể, hãy sử dụng "nhảy" thay vì "gọi" để tiết kiệm xăng.
Solidity cũng cung cấp ba cách để xác định thư viện. Đầu tiên là thư viện bên ngoài, là một hợp đồng không trạng thái được triển khai riêng trên chuỗi, đượ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 vì hỗ trợ công cụ cho các thư viện bên ngoài là không đủ, "delegatecall" tốn kém, phải tải mã bổ sung từ bộ lưu trữ liên tục và yêu cầu nhiều giao dịch để triển khai. Thư viện nội bộ được định nghĩa giống như thư viện bên ngoài, ngoại trừ việc mọi phương thức phải được định nghĩa là 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 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. Cách tiếp cận 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ằng cách sử dụng cấu trúc dữ liệu tùy chỉnh, áp dụng các hàm cho phạm vi toàn cầu và áp dụng các toán tử bí danh cho các hàm nhất định ở một mức độ hạn chế.
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 cấp lệnh, 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ự bổ sung gần đây của việc sử dụng ngôn ngữ Yul (sẽ nói thêm về điều này sau) làm biểu diễn trung gian (IR) trong quá trình biên dịch và sau đó thực hiện các hoạt động tối ưu hóa trên 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 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 DSL EVM. Các tiêu chuẩn Ethereum ERC chỉ định các giao diện bên ngoài được triển khai theo hướng dẫn về phong cách và đặc tả ABI của Solidity. Các ngôn ngữ khác cũng tuân theo đặc tả ABI của Solidity với rất ít sai lệch.
Solidity cũng cung cấp các khối Yul nội tuyến, cho phép truy cập ở mức độ thấp vào tập lệnh EVM. 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 cú pháp cấp cao hơn hỗ trợ và tùy chỉnh bộ 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, và Foundry nổi bật về mặt này.
Sau đây là một hợp đồng đơn giản được viết bằng Solidity:

Vyper
Vyper là ngôn ngữ cấp cao có cú pháp tương tự Python. Nó gần như là một tập hợp con của Python với một vài khác biệt nhỏ. Đây là DSL EVM phổ biến thứ hai. Vyper được tối ưu hóa về tính bảo mật, khả năng đọc, khả năng kiểm toán và hiệu quả sử dụng 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ẽ bị lộ; một hàm chỉ đọc cho biến. Các giá trị của hằng số và bất biến được truy cập nội bộ thông qua 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 "self" vào tên. Điều này rất hữu ích để ngăn chặn xung đột không gian tên giữa các biến được lưu trữ, tham số hàm và biến cục bộ.
Tương tự như Solidity, Vyper cũng sử dụng các thuộc tính hàm để thể hiện khả năng hiển thị và tính biến đổi của các hàm. Các chức năng được đánh dấu "@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 "@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". Các hàm được đánh dấu "@pure" không thể đọc dữ liệu từ môi trường thực thi hoặc bộ lưu trữ liên tục, ghi vào bộ lưu trữ liên tục hoặc tạo bất kỳ tác dụng phụ nào. Các hàm được đánh dấu "@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 ra tác dụng phụ. Các chức năng được đánh dấu "@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à nhận ETH. Các hàm không khai báo thuộc tính có thể thay đổi này mặc định là không thể thanh toán, tức là chú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 được 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, do phải biết bố cục bộ nhớ tại thời điểm biên dịch nên 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ụ gas 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 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 tính năng trong Solidity và Yul cũng có sẵn trong Vyper. Các hoạt động bit cấp thấp, lệnh 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à bố cục lưu trữ tùy chỉnh có thể được triển khai bằng cách cung cấp các 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ó các công cụ được tích hợp chặt chẽ hơn và cũng có thể kết nối với các công cụ phát triển Solidity. Các công cụ Vyper đáng chú ý bao gồm trình thông dịch Titanaboa, có nhiều công cụ tích hợp liên quan đến EVM và Vyper để thử nghiệm và phát triển, và Dasy, 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ữ cấp cao như Rust 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 có sẵn. 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 vào 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 chương trình phụ trợ gốc của Rust. Fe sử dụng các mô-đun để chia sẻ mã, do đó, nó không sử dụng mẫu hướng đối tượng mà sử dụng lại mã thông qua hệ thống dựa trên mô-đun, trong đó các biến, loại và hàm được khai báo trong các mô-đun và có thể được nhập theo cách tương tự như Rust.
Các biến lưu trữ liên tục đượ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ể truy cập được trong hợp đồng. Các 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 hợp đồng và theo mặc định là thuần túy và riêng tư. Để công khai một phương thức hợp đồng, từ khóa "pub" phải được thêm vào trước định nghĩa, điều này giúp nó 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". Thêm "self." trước tên biến để 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, tham số đầu tiên phải là "mut self". Từ khóa "mut" chỉ ra rằng bộ lưu trữ 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 hàm và kiểu 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 được trừ khi sử dụng từ khóa "pub". 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ột mô-đun chỉ có thể được truy cập 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 phân giải thành các hướng dẫn tại thời điểm biên dịch.
Fe tuân theo cú pháp và hệ thống kiểu của Rust, hỗ trợ các bí danh kiểu, enum với các kiểu con, đặc điểm và kiểu chung. Hỗ trợ cho việc này hiện còn hạn chế nhưng đang được thực hiện. 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 các ràng buộc chung và đặc điểm không được hỗ trợ. Các kiểu con và phương thức hỗ trợ liệt kê 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 kiểu 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 trong việc viết mã an toàn hơn, được kiểm tra thời gian biên dịch cho các nhà phát triển.
Đây là một hợp đồng đơn giản được viết bằng Fe:

giận dữ
Huff là ngôn ngữ hợp ngữ với khả năng kiểm soát ngăn xếp thủ công và mức độ trừu tượng tối thiểu của tập lệnh EVM. Thông qua lệnh "#include", mọi tệp Huff đi kèm đều có thể được phân tích cú pháp trong quá trình 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 elip cực kỳ tối ưu, trình biên dịch 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 biến 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 sự trừu tượng hóa 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 "sstore" opcode để ghi và "sload" để đọc. Người dùng có thể xác định bố cục lưu trữ tùy chỉnh hoặc bạn có thể tuân theo quy ước bắt đầu từ đầu và tăng dần từng biến bằng cách sử dụng "FREE_STORAGE_POINTER" tích hợp của trình biên dịch. Việc tạo một biến được lưu trữ có thể truy cập được từ bên ngoài đòi hỏi phải xác định thủ công một đường dẫn mã có thể đọc và trả về biến đó cho người gọi.
Các hàm bên ngoài cũng là sự trừu tượng hóa được giới thiệu bởi các ngôn ngữ cấp cao, do đó không có khái niệm về các hàm bên ngoài trong Huff. Tuy nhiên, hầu hết các dự án đều tuân theo thông số kỹ thuật ABI của các ngôn ngữ cấp cao khác ở các mức độ khác nhau, phổ biến nhất là Solidity. Mẫu phổ biến là xác định một "bộ điều phối" tải dữ liệu cuộc gọi thô và sử dụng nó để kiểm tra các bộ chọn chức năng phù hợp. Nếu nó khớp, mã tiếp theo của nó sẽ được thực thi. Vì 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 tên theo thứ tự bảng chữ cái, Vyper sắp xếp chúng theo số lượng 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ác hướng dẫn xem xét nội tâm như "bản mã" cần được sử dụng để triển khai chúng.
Các hàm bên trong được xác định bằng lệnh #definefn", lệnh này có thể chấp nhận các tham số mẫu để tăng tính 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 chúng không thể được truy cập từ bên ngoài. Việc truy cập nội bộ yêu cầu sử dụng lệnh "nhảy".
Các luồng điều khiển khác, chẳng hạn như câu lệnh có điều kiện và câu lệnh vòng lặp có thể sử dụng định nghĩa mục tiêu 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ã định danh vào ngăn xếp và thực hiện lệnh nhảy. Điều này được 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 #definemacro" và về mặt khác thì giống như các hàm bên trong. Điểm khác biệt chính là macro không tạo ra 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 nội dung của macro vào mỗi lệnh gọi trong tệp.
Thiết kế này đánh đổi việc giảm các bước nhảy tùy ý so với chi phí gas 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 bắt đầu của hợp đồng và lệnh đầu tiên trong phần nội dung của nó sẽ trở thành 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 hàm để lập lịch, bộ chọn lỗi để xử lý lỗi và bộ kiểm tra kích thước mã cho các hàm và macro nội bộ.
Lưu ý: Các nhận xét ngăn xếp như "//[count]" là không bắt buộc, chúng chỉ được sử dụng để biểu thị trạng thái ngăn xếp khi kết thúc quá trình 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 khả năng quản lý ngăn xếp thủ công và mức độ trừu tượng tối thiểu. Mã có thể được sử dụng lại thông qua các lệnh "%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 kể giữa Huff và ETK là Huff thêm một chút trừu tượng vào mã initcode, còn được gọi là mã hàm tạo, mã này có thể được ghi đè bằng cách xác định macro "XÂY DỰNG" đặc biệt. Trong ETK, những mã 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 bộ lưu trữ liên tục thông qua các lệnh "sload" và "sstore". Tuy nhiên, không có từ khóa cố định hoặc bất biến, nhưng các hằng số có thể được mô phỏng bằng một trong hai macro trong ETK, macro biểu thức. Macro biểu thức không phân giải theo hướng dẫn mà thay vào đó tạo ra các giá trị số có thể được sử dụng trong các hướng dẫn khác. Ví dụ: nó có thể không tạo ra lệnh "đẩy" hoàn toàn nhưng có thể tạo ra một số để đưa vào lệnh "đẩy".
Như đã đề cập trước đó, các hàm bên ngoài là một khái niệm ngôn ngữ cấp cao, do đó, việc hiển thị đường dẫn mã ra bên ngoài đòi hỏi phải tạo một bộ lập lịch bộ chọn hàm.
Các hàm bên trong không được xác định rõ ràng như trong các ngôn ngữ khác. Thay vào đó, các bí danh do người dùng xác định có thể được cung cấp cho các mục tiêu nhảy và nhảy tới 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 có điều kiện.
ETK hỗ trợ hai loại macro. Đầu tiên là macro biểu thức chấp nhận số lượng đối số bất kỳ và trả về một giá trị số có thể được sử dụng trong các lệnh khác. Macro biểu thức không tạo ra hướng dẫn mà thay vào đó tạo ra các giá trị hoặc hằng số ngay lập tức. Tuy nhiên, macro lệnh chấp nhận bất kỳ số lượng đối số nào và tạo ra bất kỳ số lượng lệnh 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.
Sau đây là một hợp đồng đơn giản được viết bằng ETK:

Yul
Yul là ngôn ngữ hợp ngữ có luồng điều khiển cấp cao và số lượng lớn các phần trừu tượng. Nó là một phần của chuỗi công cụ Solidity và có thể được sử dụng tùy ý trong các bước xây dựng Solidity. Yul không hỗ trợ tái sử dụng mã vì nó được coi là mục tiêu biên dịch chứ không phải là 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 kênh 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 hàm bên ngoài trong Yul. Bộ điều phối bộ chọn chức năng cần được xác định để hiển thị đường dẫn mã ra thế giới bên ngoài.
Ngoại trừ các lệnh ngăn xếp và luồng điều khiển, hầu hết các lệnh đều được hiển thị dưới dạng các hàm trong Yul. Các lệnh có thể được lồng vào nhau để rút ngắn độ dài mã hoặc gán cho các biến tạm thời rồi chuyển sang các lệnh khác để sử dụng. Các nhánh có điều kiện có thể sử dụng khối "if", đượ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 và một tùy chọn dự phòng "mặc định". Vòng lặp có thể được thực thi bằng vòng lặp "for"; mặc dù cú pháp của nó khác với các ngôn ngữ cấp cao khác nhưng nó cung cấp chức năng cơ bản giống nhau. Các hàm bên trong có thể được xác định bằng từ khóa "hàm" và tương tự như định nghĩa hàm trong các ngôn ngữ cấp cao.
Hầu hết chức năng trong Yul đều được thể hiện 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ỡ tính trừu tượng và 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 liên quan đến dữ liệu cuộc gọi, bộ nhớ và bộ 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" hoạt động trên các đối tượng Yul thông qua các bí danh chuỗi của chúng. Các hàm "có thể thay đổi" và "có thể tải" cho phép thiết lập và tải các tham số không thể 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ớ" cho biết rằng chỉ một phạm vi bộ nhớ nhất định được phân bổ, cho phép trình biên dịch sử dụng bộ nhớ 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 hướng dẫn 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ác tính năng của DSL EVM tốt
Một DSL EVM tốt phải học hỏi từ những ưu và nhược điểm 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 có trong hầu hết các ngôn ngữ hiện đại, chẳng hạn như điều kiện, khớp mẫu, vòng lặp, hàm, v.v. Mã phải rõ ràng, với mức độ trừu tượng ngầm tối thiểu được thêm vào để đảm bảo vẻ đẹp hoặc khả năng đọc của mã. Trong các môi trường có tính chính xác cao và yêu cầu cao, mọi dòng mã phải có thể hiểu được một cách rõ ràng. Hơn nữa, một hệ thống mô-đun được xác định rõ ràng phải là cốt lõi của bất kỳ ngôn ngữ tuyệt vời nào. Cần 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ư và chỉ các mục công khai rõ ràng mới có thể truy cập công khai từ bên ngoài.
Trong môi trường hạn chế về tài nguyên như EVM, hiệu quả là rất quan trọng. Hiệu quả thường đạt được bằng cách cung cấp các tính năng trừu tượng hóa với chi phí thấp 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 hữu ích để 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, macro có thể được sử dụng để cân bằng kích thước mã với hiệu quả thời gian chạy. Một hệ thống kiểu phong phú cho phép mã có tính biểu cảm cao hơn, kiểm tra thời gian biên dịch nhiều hơn để phát hiện lỗi trước khi chạy và khi 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 lắp ráp nội tuyến. Generics cũng cho phép các giá trị null (chẳng hạn như mã bên ngoài) được gói trong các loại "tùy chọn" hoặc các hoạt động dễ xảy ra lỗi (chẳng hạn như các lệnh gọi bên ngoài) được gói 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 buộc các nhà phát triển phải 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ả bị lỗi. Tuy nhiên, hãy nhớ rằng đây là những tóm tắt trong thời gian biên dịch nhằm 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 phải xử lý mọi kết quả trong thời gian biên dịch sẽ làm tăng thời gian phát triển ban đầu, nhưng lợi ích là sẽ có ít điều bất ngờ hơn 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, do đó, 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ó khả năng kém hiệu quả hơn, nhưng đôi khi cần phải sử dụng các đường dẫn mã hiệu quả hơn hoặc chức năng không được hỗ trợ. Để làm được điều này, việc lắp ráp nội tuyến phải được mở cho các nhà phát triển mà không có rào cản. Quá trình lắp ráp nội tuyến của Solidity đặt ra một số biện pháp bảo vệ để đơn giản hóa và vượt qua trình tối ưu hóa tốt hơn, nhưng khi các nhà phát triển cần toàn quyền kiểm soát môi trường thực thi, họ phải được cấp các quyền này.
Một số tính năng hữu ích tiềm năng bao gồm khả năng thao tác các thuộc tính của hàm và các mục khác tại thời điểm biên dịch. Ví dụ: thuộc tính "nội tuyến" có thể sao chép nội dung của một hàm đơn giản vào mỗi lệnh gọi, thay vì tạo thêm bước nhảy để đạt hiệu quả. Thuộc tính "abi" cho phép bạn ghi đè thủ công ABI được tạo bởi một hàm bên ngoài nhất định để thích ứng với các ngôn ngữ có các kiểu mã hóa khác nhau. Ngoài ra, có thể xác định bộ lập lịch chức năng tùy chọn, cho phép tùy chỉnh trong ngôn ngữ cấp cao để tối ưu hóa bổ sung trên các đường dẫn mã dự kiến sẽ được sử dụng phổ biến hơn. Ví dụ: kiểm tra xem bộ chọn là "transfer" hay "transferFrom" trước khi thực thi "name".
Tóm lại là
Thiết kế EVM DSL còn một chặng đường dài phía trước. Mỗi ngôn ngữ đều có những quyết định thiết kế riêng và tôi mong muốn được xem chúng phát triển như thế nào trong tương lai. Với tư cách là nhà phát triển, lợi ích tốt nhất của chúng tôi là học càng nhiều ngôn ngữ càng tốt. Đầu tiên, việc học nhiều ngôn ngữ và hiểu được sự khác biệt cũng như điểm tương đồng của chúng sẽ giúp chúng ta hiểu sâu hơn về lập trình và kiến trúc máy cơ bản. Thứ hai, ngôn ngữ có hiệu ứng mạng lưới sâu sắc và đặc tính lưu giữ mạnh mẽ. Không còn nghi ngờ gì nữa, những ông lớn đang xây dựng ngôn ngữ lập trình của riêng họ, từ C#, Swift và Kotlin đến Solidity, Sway và Cairo. Học cách chuyển đổi liền mạch giữa các ngôn ngữ này mang lại sự linh hoạt vô song cho sự nghiệp kỹ sư phần mềm. Cuối cùng, điều quan trọng là phải hiểu rằng có rất nhiều công việc đằng sau mỗi ngôn ngữ. Không ai là hoàn hảo, nhưng vô số con người tài năng đã nỗ lực rất nhiều để tạo ra những trải nghiệm an toàn và thú vị cho những nhà phát triển như chúng tôi.
