Solidity可以通過兩種不同的方式生成EVM字節(jié)碼:直接從Solidity到EVM操作碼(“舊codegen”),或者通過Yul(“new codegen”或“IR-based codegen”)中的中間表示(“IR”)。
引入基于 IR 的代碼生成器,不僅使代碼生成更加透明和可審計,而且還實現(xiàn)了跨功能的更強大的優(yōu)化傳遞。
您可以使用標準 json 中的選項在命令行上啟用它,我們鼓勵每個人嘗試一下!--via-ir
{"viaIR": true}
出于幾個原因,舊的和基于IR的代碼生成器之間存在微小的語義差異,主要是在我們不希望人們依賴這種行為的領(lǐng)域。本節(jié)重點介紹舊的和基于 IR 的編碼機之間的主要區(qū)別。
本節(jié)列出了僅語義的更改,因此可能會在現(xiàn)有代碼中隱藏新的和不同的行為。
在繼承的情況下,狀態(tài)變量初始化的順序已更改。
順序曾經(jīng)是:
所有狀態(tài)變量在開始時都為零初始化。
評估從最派生到大多數(shù)基本協(xié)定的基本構(gòu)造函數(shù)參數(shù)。
初始化整個繼承層次結(jié)構(gòu)中的所有狀態(tài)變量,從最基本到最派生。
對線性化層次結(jié)構(gòu)中從最基本到最派生的所有協(xié)定運行構(gòu)造函數(shù)(如果存在)。
新訂單:
所有狀態(tài)變量在開始時都為零初始化。
評估從最派生到大多數(shù)基本協(xié)定的基本構(gòu)造函數(shù)參數(shù)。
對于線性化層次結(jié)構(gòu)中從最基本到最派生的每個合約:
初始化狀態(tài)變量。
運行構(gòu)造函數(shù)(如果存在)。
這會導(dǎo)致合約的差異,其中狀態(tài)變量的初始值依賴于另一個合約中構(gòu)造函數(shù)的結(jié)果:
// SPDX-License-Identifier: GPL-3.0 pragma solidity >=0.7.1; contract A { uint x; constructor() { x = 42; } function f() public view returns(uint256) { return x; } } contract B is A { uint public y = f(); }
以前,將設(shè)置為 0。這是因為我們將首先初始化狀態(tài)變量:首先,設(shè)置為0,并且在初始化時,將返回0,導(dǎo)致也為0。使用新規(guī)則,將設(shè)置為42。我們首先初始化為 0,然后調(diào)用 A 的構(gòu)造函數(shù),該構(gòu)造函數(shù)設(shè)置為 42。最后,在初始化時,返回 42,導(dǎo)致為 42。y
x
y
f()
y
y
x
x
y
f()
y
刪除存儲結(jié)構(gòu)時,包含結(jié)構(gòu)成員的每個存儲槽都將完全設(shè)置為零。以前,填充空間保持不變。因此,如果結(jié)構(gòu)中的填充空間用于存儲數(shù)據(jù)(例如,在合約升級的上下文中),您必須知道現(xiàn)在也會清除添加的成員(雖然過去不會清除它)。delete
// SPDX-License-Identifier: GPL-3.0 pragma solidity >=0.7.1; contract C { struct S { uint64 y; uint64 z; } S s; function f() public { // ... delete s; // s occupies only first 16 bytes of the 32 bytes slot // delete will write zero to the full slot } }
對于隱式刪除,我們具有相同的行為,例如,當結(jié)構(gòu)數(shù)組被縮短時。
函數(shù)修飾符的實現(xiàn)方式與函數(shù)參數(shù)和返回變量略有不同。如果在修飾符中多次計算占位符,這尤其有效。在舊的代碼生成器中,每個函數(shù)參數(shù)和返回變量在堆棧上都有一個固定的槽。如果函數(shù)由于多次使用或在循環(huán)中使用而多次運行,則在下次執(zhí)行函數(shù)時,對函數(shù)參數(shù)或返回變量值的更改可見。新的代碼生成器使用實際函數(shù)實現(xiàn)修飾符,并傳遞函數(shù)參數(shù)。這意味著對函數(shù)體的多次計算將獲得相同的參數(shù)值,并且對返回變量的影響是,對于每次執(zhí)行,它們都會重置為其默認(零)值。_;
_;
// SPDX-License-Identifier: GPL-3.0 pragma solidity >=0.7.0; contract C { function f(uint a) public pure mod() returns (uint r) { r = a++; } modifier mod() { _; _; } }
如果在舊代碼生成器中執(zhí)行,它將返回 ,而在使用新代碼生成器時將返回。f(0)
2
1
// SPDX-License-Identifier: GPL-3.0 pragma solidity >=0.7.1 <0.9.0; contract C { bool active = true; modifier mod() { _; active = false; _; } function foo() external mod() returns (uint ret) { if (active) ret = 1; // Same as ``return 1`` } }
該函數(shù)返回以下值:C.foo()
舊代碼生成器:因為返回變量在第一次求值之前僅初始化為一次,然后被 .它不會在第二次評估中再次初始化,也不會顯式分配它(由于 ),因此它保留其第一個值。1
0
_;
return 1;
_;
foo()
active == false
新的代碼生成器:因為所有參數(shù)(包括返回參數(shù))將在每次評估之前重新初始化。0
_;
將數(shù)組從內(nèi)存復(fù)制到存儲以不同的方式實現(xiàn)。舊的代碼生成器總是復(fù)制完整的單詞,而新的代碼生成器在其結(jié)束后剪切字節(jié)數(shù)組。舊行為可能導(dǎo)致在陣列結(jié)束后(但仍在同一存儲插槽中)復(fù)制臟數(shù)據(jù)。這會導(dǎo)致某些合約的差異,例如:bytes
// SPDX-License-Identifier: GPL-3.0 pragma solidity >=0.8.1; contract C { bytes x; function f() public returns (uint r) { bytes memory m = "tmp"; assembly { mstore(m, 8) mstore(add(m, 32), "deadbeef15dead") } x = m; assembly { r := sload(x.slot) } } }
以前會返回(它具有正確的長度,并且正確的前8個元素,但隨后它包含通過程序集設(shè)置的臟數(shù)據(jù))?,F(xiàn)在它正在返回(它具有正確的長度和正確的元素,但不包含多余的數(shù)據(jù))。f()
0x6465616462656566313564656164000000000000000000000000000000000010
0x6465616462656566000000000000000000000000000000000000000000000010
對于舊代碼生成器,表達式的計算順序是未指定的。對于新的代碼生成器,我們嘗試按源代碼順序(從左到右)進行評估,但不保證。這可能導(dǎo)致語義差異。
例如:
// SPDX-License-Identifier: GPL-3.0 pragma solidity >=0.8.1; contract C { function preincr_u8(uint8 a) public pure returns (uint8) { return ++a + a; } }
該函數(shù)返回以下值:preincr_u8(1)
舊代碼生成器:3(),但返回值通常未指定1 + 2
新代碼生成器:4()但不保證返回值2 + 2
另一方面,函數(shù)參數(shù)表達式由兩個代碼生成器以相同的順序計算,但全局函數(shù)和 .例如:addmod
mulmod
// SPDX-License-Identifier: GPL-3.0 pragma solidity >=0.8.1; contract C { function add(uint8 a, uint8 b) public pure returns (uint8) { return a + b; } function g(uint8 a, uint8 b) public pure returns (uint8) { return add(++a + ++b, a + b); } }
該函數(shù)返回以下值:g(1, 2)
舊代碼生成器:()但返回值通常未指定10
add(2 + 3, 2 + 3)
新代碼生成器:但不能保證返回值10
全局函數(shù)的參數(shù),由舊代碼生成器從右到左計算,由新代碼生成器從左到右計算。例如:addmod
mulmod
// SPDX-License-Identifier: GPL-3.0 pragma solidity >=0.8.1; contract C { function f() public pure returns (uint256 aMod, uint256 mMod) { uint256 x = 3; // Old code gen: add/mulmod(5, 4, 3) // New code gen: add/mulmod(4, 5, 5) aMod = addmod(++x, ++x, x); mMod = mulmod(++x, ++x, x); } }
該函數(shù)返回以下值:f()
舊代碼生成器:和aMod = 0
mMod = 2
新的代碼生成器:和aMod = 4
mMod = 0
新的代碼生成器對可用內(nèi)存指針施加了 () 的硬限制。如果分配的值超過此限制,則會恢復(fù)。舊的代碼生成器沒有此限制。type(uint64).max
0xffffffffffffffff
例如:
// SPDX-License-Identifier: GPL-3.0 pragma solidity >0.8.0; contract C { function f() public { uint[] memory arr; // allocation size: 576460752303423481 // assumes freeMemPtr points to 0x80 initially uint solYulMaxAllocationBeforeMemPtrOverflow = (type(uint64).max - 0x80 - 31) / 32; // freeMemPtr overflows UINT64_MAX arr = new uint[](solYulMaxAllocationBeforeMemPtrOverflow); } }
函數(shù) f() 的行為如下:
舊代碼生成器:在大內(nèi)存分配后將數(shù)組內(nèi)容清零時耗盡氣體
新的代碼生成器:由于可用內(nèi)存指針溢出而恢復(fù)(不會耗盡氣體)
舊的代碼生成器使用代碼偏移量或標記作為內(nèi)部函數(shù)指針的值。這尤其復(fù)雜,因為這些偏移量在構(gòu)造時和部署后是不同的,并且值可以通過存儲跨越此邊界。因此,兩個偏移量在構(gòu)造時被編碼為相同的值(不同的字節(jié))。
在新的代碼生成器中,函數(shù)指針使用按順序分配的內(nèi)部 ID。由于無法通過跳轉(zhuǎn)進行調(diào)用,因此通過函數(shù)指針的調(diào)用始終必須使用內(nèi)部調(diào)度函數(shù),該函數(shù)使用語句來選擇正確的函數(shù)。switch
該 ID 是為未初始化的函數(shù)指針保留的,這些指針在調(diào)用時會導(dǎo)致調(diào)度函數(shù)中的死機。0
在舊的代碼生成器中,內(nèi)部函數(shù)指針使用一個特殊函數(shù)進行初始化,該函數(shù)總是會導(dǎo)致死機。這會導(dǎo)致在構(gòu)造時對存儲中的內(nèi)部函數(shù)指針進行存儲寫入。
舊代碼生成器僅在操作之前執(zhí)行清理,其結(jié)果可能受臟位值的影響。新的代碼生成器在任何可能導(dǎo)致臟位的操作后執(zhí)行清理。希望優(yōu)化器足夠強大,可以消除冗余的清理操作。
例如:
// SPDX-License-Identifier: GPL-3.0 pragma solidity >=0.8.1; contract C { function f(uint8 a) public pure returns (uint r1, uint r2) { a = ~a; assembly { r1 := a } r2 = a; } }
該函數(shù)返回以下值:f(1)
舊代碼生成器:(,fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe
00000000000000000000000000000000000000000000000000000000000000fe
)
新的代碼生成器:(,00000000000000000000000000000000000000000000000000000000000000fe
00000000000000000000000000000000000000000000000000000000000000fe
)
請注意,與新代碼生成器不同,舊代碼生成器在位不賦值 () 之后不執(zhí)行清理。這會導(dǎo)致分配不同的值(在內(nèi)聯(lián)程序集塊內(nèi))以在舊代碼生成器和新代碼生成器之間返回值。但是,在將 的新值 分配給 之前,這兩個代碼生成器都會執(zhí)行清理。a = ~a
r1
a
r2
更多建議: