Yul
Yul
Yul(이전에는 JULIA 또는 IULIA라고도 불림)은 다양한 백엔드의 바이트코드로 컴파일될 수 있는 중간 언어예요. 독립 실행 모드와 Solidity 안의 "인라인 어셈블리"로 사용될 수 있어요. 컴파일러는 IR 기반 코드 생성기("new codegen" 또는 "IR-based codegen")에서 Yul을 중간 언어로 사용해요. Yul은 모든 대상 플랫폼에 동등하게 이로울 수 있는 고수준 최적화 단계의 좋은 대상이에요.
출처: 문서
본문
Yul(이전에는 JULIA 또는 IULIA라고도 불림)은 다양한 백엔드의 바이트코드로 컴파일될 수 있는 중간 언어예요. 독립 실행 모드와 Solidity 안의 "인라인 어셈블리"로 사용될 수 있어요. 컴파일러는 IR 기반 코드 생성기("new codegen" 또는 "IR-based codegen")에서 Yul을 중간 언어로 사용해요. Yul은 모든 대상 플랫폼에 동등하게 이로울 수 있는 고수준 최적화 단계의 좋은 대상이에요.
동기와 고수준 설명 (Motivation and High-level Description)
Yul의 설계는 몇 가지 목표를 달성하려 해요:
- Yul로 작성된 프로그램은 코드가 컴파일러에 의해 Solidity나 다른 고수준 언어에서 생성되더라도 읽을 수 있어야 해요.
- 제어 흐름은 수동 검사, 형식 검증, 최적화를 돕기 위해 이해하기 쉬워야 해요.
- Yul에서 바이트코드로의 변환은 가능한 한 간단해야 해요.
- Yul은 전체-프로그램 최적화에 적합해야 해요.
첫 번째와 두 번째 목표를 달성하기 위해, Yul은 for 루프, if와 switch 문, 함수 호출 같은 고수준 구문을 제공해요. 이것들은 어셈블리 프로그램의 제어 흐름을 적절히 나타내기에 충분해야 해요. 따라서 SWAP, DUP, JUMPDEST, JUMP, JUMPI에 대한 명시적 문은 제공되지 않아요. 처음 두 개는 데이터 흐름을 난독화하고 마지막 두 개는 제어 흐름을 난독화하기 때문이에요. 게다가 mul(add(x, y), 7) 형태의 함수형 문은 순수 opcode 문 7 y x add mul보다 선호돼요. 첫 번째 형태에서는 어떤 피연산자가 어떤 opcode에 사용되는지 훨씬 쉽게 볼 수 있기 때문이에요.
스택 머신을 위해 설계됐지만, Yul은 스택 자체의 복잡성을 드러내지 않아요. 프로그래머나 감사자는 스택에 대해 걱정할 필요가 없어야 해요.
세 번째 목표는 고수준 구문을 매우 규칙적인 방식으로 바이트코드로 컴파일함으로써 달성돼요. 어셈블러가 수행하는 유일한 비-로컬 연산은 사용자 정의 식별자(함수, 변수 등)의 이름 조회와 스택에서 로컬 변수의 정리예요.
값과 참조 같은 개념 사이의 혼란을 피하기 위해, Yul은 정적으로 타입이 지정돼요. 동시에 가독성을 돕기 위해 항상 생략될 수 있는 기본 타입(보통 대상 머신의 정수 워드)이 있어요.
언어를 단순하고 유연하게 유지하기 위해, Yul은 순수한 형태로 어떤 내장 연산, 함수, 타입도 가지지 않아요. 이것들은 Yul의 방언(dialect)을 지정할 때 의미와 함께 추가돼요. 이는 서로 다른 대상 플랫폼과 기능 집합의 요구에 Yul을 특화할 수 있게 해요.
현재 지정된 Yul 방언은 하나뿐이에요. 이 방언은 EVM opcode를 내장 함수로 사용하고(아래 참고) EVM의 네이티브 256비트 타입인 u256 타입만 정의해요. 그 때문에 우리는 아래 예시에서 타입을 제공하지 않을 거예요.
간단한 예시 (Simple Example)
다음 예시 프로그램은 EVM 방언으로 작성됐고 지수 계산을 해요. solc --strict-assembly로 컴파일할 수 있어요. 내장 함수 mul과 div는 각각 곱셈과 나눗셈을 계산해요.
open in Remix
{
function power(base, exponent) -> result
{
switch exponent
case 0 { result := 1 }
case 1 { result := base }
default
{
result := power(mul(base, base), div(exponent, 2))
switch mod(exponent, 2)
case 1 { result := mul(base, result) }
}
}
}
재귀 대신 for-loop를 사용해 같은 함수를 구현하는 것도 가능해요. 여기서 lt(a, b)는 a가 b보다 작은지 계산해요.
open in Remix
{
function power(base, exponent) -> result
{
result := 1
for { let i := 0 } lt(i, exponent) { i := add(i, 1) }
{
result := mul(result, base)
}
}
}
이 절의 끝에 ERC-20 표준의 완전한 구현을 찾을 수 있어요.
독립 실행 사용 (Stand-Alone Usage)
Solidity 컴파일러를 사용해 EVM 방언에서 Yul을 독립 실행 형태로 사용할 수 있어요. 이것은 Yul 객체 표기법을 사용해 계약을 배포하기 위한 코드를 데이터로 참조할 수 있게 해요. 이 Yul 모드는 명령줄 컴파일러(--strict-assembly 사용)와 표준-json 인터페이스 둘 다에서 사용할 수 있어요:
{
"language": "Yul",
"sources": { "input.yul": { "content": "{ sstore(0, 1) }" } },
"settings": {
"outputSelection": { "*": { "*": ["*"], "": [ "*" ] } },
"optimizer": { "enabled": true, "details": { "yul": true } }
}
}
경고 (Warning)
Yul은 활발히 개발 중이며 바이트코드 생성은 대상이 EVM 1.0인 Yul의 EVM 방언에 대해서만 완전히 구현돼 있어요.
Yul의 비공식 설명 (Informal Description of Yul)
다음에서 Yul 언어의 각 개별 측면에 대해 이야기할 거예요. 예시에서 기본 EVM 방언을 사용할 거예요.
문법 (Syntax)
Yul은 Solidity와 같은 방식으로 주석, 리터럴, 식별자를 파싱해요. 예를 들어 주석을 나타내는 데 //과 /* */을 사용할 수 있어요. 한 가지 예외: Yul의 식별자는 점(.)을 포함할 수 있어요. Yul은 코드, 데이터, 하위 객체로 구성된 "객체"를 지정할 수 있어요. 자세한 내용은 아래의 Yul 객체 참고. 이 절에서는 그런 객체의 코드 부분만 다뤄요.
이 코드 부분은 항상 중괄호로 구분된 블록으로 구성돼요. 대부분의 도구는 객체가 기대되는 곳에 코드 블록만 지정하는 것을 지원해요.
코드 블록 안에서 다음 요소를 사용할 수 있어요(자세한 내용은 이후 절 참고):
- 리터럴, 예:
0x123,42또는"abc"(최대 32자의 문자열) - 내장 함수 호출, 예:
add(1, mload(0)) - 변수 선언, 예:
let x := 7,let x := add(y, 3)또는let x(초기 값 0이 할당됨) - 식별자(변수), 예:
add(3, x) - 할당, 예:
x := add(y, 3) - 로컬 변수가 안에서 스코프를 갖는 블록, 예:
{ let x := 3 { let y := add(x, 1) } } if문, 예:if lt(a, b) { sstore(0, 1) }switch문, 예:switch mload(0) case 0 { revert() } default { mstore(0, 1) }for루프, 예:for { let i := 0} lt(i, 10) { i := add(i, 1) } { mstore(i, 7) }- 함수 정의, 예:
function f(a, b) -> c { c := add(a, b) }
여러 구문 요소는 공백으로 분리해 그냥 서로 이어질 수 있어요. 즉 종료 ;이나 줄바꿈이 필요 없어요.
리터럴 (Literals)
리터럴로 다음을 사용할 수 있어요:
- 십진 또는 16진 표기법의 정수 상수.
- ASCII 문자열(예:
"abc").\xNN16진 이스케이프와\uNNNN유니코드 이스케이프(N은 16진 숫자)를 포함할 수 있어요. - 16진 문자열(예:
hex"616263").
Yul의 EVM 방언에서 리터럴은 다음과 같이 256비트 워드를 나타내요:
- 십진 또는 16진 상수는
2**256보다 작아야 해요. 그것들은 big endian 인코딩의 부호 없는 정수로 그 값을 가진 256비트 워드를 나타내요. - ASCII 문자열은 먼저 바이트 시퀀스로 보여져요. 비-이스케이프 ASCII 문자는 ASCII 코드가 값인 단일 바이트로, 이스케이프
\xNN은 그 값을 가진 단일 바이트로, 이스케이프\uNNNN은 그 코드 포인트에 대한 UTF-8 바이트 시퀀스로 봐요. 바이트 시퀀스는 32바이트를 초과하면 안 돼요. 바이트 시퀀스는 오른쪽에 0으로 패딩되어 길이 32바이트에 도달해요. 즉 문자열은 왼쪽 정렬로 저장돼요. 패딩된 바이트 시퀀스는 가장 중요한 8비트가 첫 번째 바이트의 것인 256비트 워드를 나타내요. 즉 바이트는 big endian 형태로 해석돼요. - 16진 문자열은 먼저 바이트 시퀀스로 보여져요. 연속된 16진 숫자 각 쌍을 바이트로 봐요. 바이트 시퀀스는 32바이트(즉 64 16진 숫자)를 초과하면 안 되고, 위와 같이 취급돼요.
EVM용으로 컴파일할 때 이것은 적절한 PUSHi 명령으로 변환돼요. 다음 예시에서 3과 2가 더해져 5가 되고, 그 다음 문자열 "abc"와의 비트 AND가 계산돼요. 최종 값은 x라는 로컬 변수에 할당돼요.
위의 32바이트 제한은 리터럴 인자를 요구하는 내장 함수(예: setimmutable 또는 loadimmutable)에 전달되는 문자열 리터럴에는 적용되지 않아요. 그런 문자열은 생성된 바이트코드에 결코 들어가지 않아요.
open in Remix
let x := and("abc", add(3, 2))
기본 타입이 아니면, 리터럴의 타입은 콜론 뒤에 지정해야 해요:
open in Remix
// This will not compile (u32 and u256 type not implemented yet)
let x := and("abc":u32, add(3:u256, 2:u256))
함수 호출 (Function Calls)
내장 함수와 사용자 정의 함수(아래 참고) 모두 이전 예시에서 보여 준 것과 같은 방식으로 호출될 수 있어요. 함수가 단일 값을 반환하면 표현식 안에서 직접 다시 사용될 수 있어요. 여러 값을 반환하면 로컬 변수에 할당해야 해요.
open in Remix
function f(x, y) -> a, b { /* ... */ }
mstore(0x80, add(mload(0x80), 3))
// Here, the user-defined function `f` returns two values.
let x, y := f(1, mload(0))
EVM의 내장 함수의 경우 함수형 표현식이 opcode의 스트림으로 직접 변환될 수 있어요. opcode를 얻으려면 표현식을 오른쪽에서 왼쪽으로 읽으면 돼요. 예시의 두 번째 줄의 경우 이것은 PUSH1 3 PUSH1 0x80 MLOAD ADD PUSH1 0x80 MSTORE이에요. 사용자 정의 함수 호출의 경우 인자도 오른쪽에서 왼쪽으로 스택에 놓이며, 이것이 인자 목록이 평가되는 순서예요. 그러나 반환 값은 왼쪽에서 오른쪽으로 스택에서 기대돼요. 즉 이 예시에서 y가 스택 상단에 있고 x가 그 아래에 있어요.
변수 선언 (Variable Declarations)
let 키워드를 사용해 변수를 선언할 수 있어요. 변수는 그것이 정의된 {...}-블록 안에서만 보여요. EVM으로 컴파일할 때 변수를 위해 예약된 새 스택 슬롯이 만들어지고 블록의 끝에 도달하면 자동으로 다시 제거돼요. 변수에 초기 값을 제공할 수 있어요. 값을 제공하지 않으면 변수는 0으로 초기화돼요.
변수는 스택에 저장되므로 메모리나 스토리지에 직접 영향을 주지 않지만, 내장 함수 mstore, mload, sstore, sload에서 메모리나 스토리지 위치에 대한 포인터로 사용될 수 있어요. 미래의 방언은 그런 포인터에 대한 특정 타입을 도입할 수 있어요.
변수가 참조될 때 그 현재 값이 복사돼요. EVM의 경우 이것은 DUP 명령으로 변환돼요.
open in Remix
{
let zero := 0
let v := calldataload(zero)
{
let y := add(sload(v), 1)
v := y
} // y is "deallocated" here
sstore(v, zero)
} // v and zero are "deallocated" here
선언된 변수가 기본 타입과 다른 타입을 가져야 하면, 콜론 뒤에 그것을 나타내요. 여러 값을 반환하는 함수 호출에서 할당할 때 하나의 문에서 여러 변수를 선언할 수도 있어요.
open in Remix
// This will not compile (u32 and u256 type not implemented yet)
{
let zero:u32 := 0:u32
let v:u256, t:u32 := f()
let x, y := g()
}
최적화 프로그램 설정에 따라, 컴파일러는 변수가 마지막으로 사용된 후, 아직 스코프 안에 있더라도 스택 슬롯을 이미 해제할 수 있어요.
할당 (Assignments)
변수는 정의 후 := 연산자로 할당될 수 있어요. 동시에 여러 변수를 할당하는 것이 가능해요. 이를 위해 값의 수와 타입이 일치해야 해요.
여러 반환 매개변수를 가진 함수가 반환한 값을 할당하려면 여러 변수를 제공해야 해요. 같은 변수는 할당의 왼쪽에 여러 번 나타날 수 없어요. 예: x, x := f()은 유효하지 않아요.
open in Remix
let v := 0
// re-assign v
v := 2
let t := add(v, 2)
function f() -> a, b { }
// assign multiple values
v, t := f()
If
if 문은 코드를 조건부로 실행하는 데 사용될 수 있어요. "else" 블록은 정의될 수 없어요. 여러 대안이 필요하면 "switch"를 사용하는 것을 고려해요(아래 참고).
open in Remix
if lt(calldatasize(), 4) { revert(0, 0) }
본문의 중괄호는 필수예요.
Switch
switch 문을 if 문의 확장 버전으로 사용할 수 있어요. 표현식의 값을 취해 몇 가지 리터럴 상수와 비교해요. 일치하는 상수에 해당하는 분기를 선택해요. 다른 프로그래밍 언어와 달리, 안전을 위해 제어 흐름이 한 case에서 다음으로 계속되지 않아요. 리터럴 상수 중 어떤 것도 일치하지 않으면 수행되는 default라는 폴백 또는 기본 case가 있을 수 있어요.
open in Remix
{
let x := 0
switch calldataload(4)
case 0 {
x := calldataload(0x24)
}
default {
x := calldataload(0x44)
}
sstore(0, div(x, 2))
}
case 목록은 중괄호로 묶이지 않지만, case의 본문은 중괄호를 요구해요.
루프 (Loops)
Yul은 초기화 부분, 조건, 반복-후 부분, 본문을 포함하는 헤더로 구성된 for-loop를 지원해요. 조건은 표현식이어야 하는 반면, 다른 세 개는 블록이에요. 초기화 부분이 최상위에서 어떤 변수를 선언하면, 이 변수들의 스코프는 루프의 모든 다른 부분으로 확장돼요.
break와 continue 문은 각각 루프를 종료하거나 반복-후 부분으로 건너뛰기 위해 본문에서 사용될 수 있어요.
다음 예시는 메모리 영역의 합을 계산해요.
open in Remix
{
let x := 0
for { let i := 0 } lt(i, 0x100) { i := add(i, 0x20) } {
x := add(x, mload(i))
}
}
for 루프는 while 루프의 대체로도 사용될 수 있어요. 초기화와 반복-후 부분을 그냥 비워 두면 돼요.
open in Remix
{
let x := 0
let i := 0
for { } lt(i, 0x100) { } { // while(i < 0x100)
x := add(x, mload(i))
i := add(i, 0x20)
}
}
함수 선언 (Function Declarations)
Yul은 함수의 정의를 허용해요. 이것들은 결코 컨트랙트의 외부 인터페이스의 일부가 아니고 Solidity 함수용과 분리된 네임스페이스의 일부이므로, Solidity의 함수와 혼동해서는 안 돼요. EVM의 경우 Yul 함수는 스택에서 그 인자(와 반환 PC)를 취하고 결과도 스택에 놓아요. 사용자 정의 함수와 내장 함수는 정확히 같은 방식으로 호출돼요.
함수는 어디서든 정의될 수 있고 그것이 선언된 블록에서 보여요. 함수 안에서 그 함수 밖에서 정의된 로컬 변수에 접근할 수 없어요.
함수는 Solidity와 비슷하게 매개변수와 반환 변수를 선언해요. 값을 반환하려면 반환 변수에 할당해요. 여러 값을 반환하는 함수를 호출하면 a, b := f(x) 또는 let a, b := f(x)을 사용해 여러 변수에 할당해야 해요.
leave 문을 사용해 현재 함수를 종료할 수 있어요. 이것은 반환할 값을 취하지 않는다는 점만 빼면 다른 언어의 return 문처럼 동작해요. 그냥 함수를 종료하고 함수는 현재 반환 변수에 할당된 어떤 값이든 반환할 거예요.
EVM 방언에는 return이라는 내장 함수가 있다는 점에 주의해요. 그것은 전체 실행 컨텍스트(내부 메시지 호출)를 종료하고 현재 yul 함수만 종료하지 않아요.
다음 예시는 제곱-그리고-곱셈(square-and-multiply)으로 power 함수를 구현해요.
open in Remix
{
function power(base, exponent) -> result {
switch exponent
case 0 { result := 1 }
case 1 { result := base }
default {
result := power(mul(base, base), div(exponent, 2))
switch mod(exponent, 2)
case 1 { result := mul(base, result) }
}
}
}
Yul의 명세 (Specification of Yul)
이 장은 Yul 코드를 공식적으로 설명해요. Yul 코드는 보통 자체 장에서 설명하는 Yul 객체 안에 놓여요.
Block = '{' Statement* '}'
Statement =
Block |
FunctionDefinition |
VariableDeclaration |
Assignment |
If |
Expression |
Switch |
ForLoop |
BreakContinue |
Leave
FunctionDefinition =
'function' Identifier '(' TypedIdentifierList? ')'
( '->' TypedIdentifierList )? Block
VariableDeclaration =
'let' TypedIdentifierList ( ':=' Expression )?
Assignment =
IdentifierList ':=' Expression
Expression =
FunctionCall | Identifier | Literal
If =
'if' Expression Block
Switch =
'switch' Expression ( Case+ Default? | Default )
Case =
'case' Literal Block
Default =
'default' Block
ForLoop =
'for' Block Expression Block Block
BreakContinue =
'break' | 'continue'
Leave = 'leave'
FunctionCall =
Identifier '(' ( Expression ( ',' Expression )* )? ')'
Identifier = [a-zA-Z_$] [a-zA-Z_$0-9.]*
IdentifierList = Identifier ( ',' Identifier)*
TypeName = Identifier
TypedIdentifierList = Identifier ( ':' TypeName )? ( ',' Identifier ( ':' TypeName )? )*
Literal =
(NumberLiteral | StringLiteral | TrueLiteral | FalseLiteral) ( ':' TypeName )?
NumberLiteral = HexNumber | DecimalNumber
StringLiteral = '"' ([^"\r\n\\] | '\\' .)* '"'
TrueLiteral = 'true'
FalseLiteral = 'false'
HexNumber = '0x' [0-9a-fA-F]+
DecimalNumber = [0-9]+
문법에 대한 제한 (Restrictions on the Grammar)
문법이 직접 부과하는 것 외에 다음 제한이 적용돼요:
Switch는 최소 하나의 case(기본 case 포함)를 가져야 해요.- 모든 case 값은 같은 타입과 구별되는 값을 가져야 해요.
- 표현식 타입의 모든 가능한 값이 덮이는 경우, 기본 case는 허용되지 않아요(즉
true와falsecase 둘 다 가진 bool 표현식 switch는 기본 case를 허용하지 않아요). - 모든 표현식은 0개 이상의 값을 평가해요. 식별자와 리터럴은 정확히 하나의 값으로 평가되고, 함수 호출은 호출된 함수의 반환 변수 수와 같은 수의 값으로 평가돼요.
- 변수 선언과 할당에서, 오른쪽 표현식(있으면)은 왼쪽 변수의 수와 같은 수의 값으로 평가되어야 해요. 이것은 하나 이상의 값으로 평가되는 표현식이 허용되는 유일한 상황이에요.
- 같은 변수 이름은 할당이나 변수 선언의 왼쪽에 한 번 이상 나타날 수 없어요.
- 문이기도 한 표현식(즉 블록 수준)은 0개의 값으로 평가해야 해요.
- 다른 모든 상황에서 표현식은 정확히 하나의 값으로 평가해야 해요.
continue또는break문은 다음과 같이 for-loop의 본문 안에서만 사용될 수 있어요. 그 문을 포함하는 가장 안쪽 루프를 고려해요. 루프와 문은 같은 함수 안에 있거나, 둘 다 최상위에 있어야 해요. 문은 루프의 본문 블록에 있어야 해요. 루프의 초기화 블록이나 갱신 블록에 있을 수 없어요. 이 제한이continue나break문을 포함하는 가장 안쪽 루프에만 적용된다는 것을 강조할 가치가 있어요. 이 가장 안쪽 루프와 그continue/break문은 외부 루프의 아무 곳에나, 아마도 외부 루프의 초기화 블록이나 갱신 블록에 나타날 수 있어요.- 예를 들어 다음은
break가 내부 루프의 본문 블록에 있으므로 (외부 루프의 갱신 블록에도 있지만) 합법이에요:
open in Remix
for {} true { for {} true {} { break } }
{
}
- for-loop의 조건 부분은 정확히 하나의 값으로 평가해야 해요.
leave문은 함수 안에서만 사용될 수 있어요.- 함수는 for 루프 초기화 블록 안 어디에서도 정의될 수 없어요.
- 리터럴은 그 타입보다 클 수 없어요. 정의된 가장 큰 타입은 256비트 너비예요.
- 할당과 함수 호출 동안, 각 값의 타입이 일치해야 해요. 암시적 타입 변환은 없어요. 타입 변환은 일반적으로 방언이 한 타입의 값을 취해 다른 타입의 값을 반환하는 적절한 내장 함수를 제공할 때에만 달성될 수 있어요.
스코프 규칙 (Scoping Rules)
Yul의 스코프는 Block에 묶여 있고(예외는 함수와 아래에서 설명하는 for 루프), 모든 선언(FunctionDefinition, VariableDeclaration)이 이 스코프에 새 식별자를 도입해요. 식별자는 그들이 정의된 블록(모든 하위 노드와 하위 블록 포함)에서 보여요. 함수는 전체 블록에서(정의 전에도) 보이는 반면, 변수는 VariableDeclaration 뒤의 문부터만 보여요. 특히 변수는 그 자신의 변수 선언의 오른쪽에서 참조될 수 없어요. 함수는 선언 전에 이미 참조될 수 있어요(보인다면).
일반 스코프 규칙의 예외로, for-loop의 "init" 부분(첫 번째 블록)의 스코프는 for 루프의 모든 다른 부분으로 확장돼요. 이는 init 부분에서(하지만 init 부분 안의 블록 안에서가 아니라) 선언된 변수(와 함수)가 for-loop의 모든 다른 부분에서 보인다는 뜻이에요. for 루프의 다른 부분에서 선언된 식별자는 일반적인 구문 스코프 규칙을 따른다.
즉 for { I... } C { P... } { B... } 형태의 for-loop는 { I... for {} C { P... } { B... } }와 동등해요.
함수의 매개변수와 반환 매개변수는 함수 본문에서 보이고 그 이름은 구별되어야 해요. 함수 안에서 그 함수 밖에서 선언된 변수를 참조하는 것은 불가능해요.
섀도잉은 허용되지 않아요. 즉 같은 이름의 다른 식별자도 보이는 지점에서 식별자를 선언할 수 없어요. 현재 함수 밖에서 선언됐기 때문에 그것을 참조하는 것이 불가능하더라도요.
형식 명세 (Formal Specification)
우리는 AST의 다양한 노드에 오버로드된 평가 함수 E를 제공해 Yul을 공식적으로 명시해요. 내장 함수는 부작용을 가질 수 있으므로, E는 두 개의 상태 객체와 AST 노드를 취하고 두 개의 새 상태 객체와 가변적인 수의 다른 값을 반환해요.
두 상태 객체는 전역 상태 객체(EVM의 맥락에서 블록체인의 메모리, 스토리지, 상태)와 로컬 상태 객체(로컬 변수의 상태, 즉 EVM의 스택의 세그먼트)예요.
AST 노드가 문이면 E는 두 상태 객체와 break, continue, leave 문에 사용되는 "mode"를 반환해요. AST 노드가 표현식이면 E는 두 상태 객체와 표현식이 평가하는 만큼의 값을 반환해요.
이 고수준 설명에서 전역 상태의 정확한 성질은 불특정이에요. 로컬 상태 L은 식별자 i를 값 v에 매핑하는 것으로, L[i] = v로 나타내요.
식별자 v에 대해 $v를 그 식별자의 이름이라 해요. 우리는 AST 노드에 대해 구조 분해 표기법을 사용할 거예요.
E(G, L, <{St1, ..., Stn}>: Block) =
let G1, L1, mode = E(G, L, St1, ..., Stn)
let L2 be a restriction of L1 to the identifiers of L
G1, L2, mode
E(G, L, St1, ..., Stn: Statement) =
if n is zero:
G, L, regular
else:
let G1, L1, mode = E(G, L, St1)
if mode is regular then
E(G1, L1, St2, ..., Stn)
otherwise
G1, L1, mode
E(G, L, FunctionDefinition) =
G, L, regular
E(G, L, <let var_1, ..., var_n := rhs>: VariableDeclaration) =
E(G, L, <var_1, ..., var_n := rhs>: Assignment)
E(G, L, <let var_1, ..., var_n>: VariableDeclaration) =
let L1 be a copy of L where L1[$var_i] = 0 for i = 1, ..., n
G, L1, regular
E(G, L, <var_1, ..., var_n := rhs>: Assignment) =
let G1, L1, v1, ..., vn = E(G, L, rhs)
let L2 be a copy of L1 where L2[$var_i] = vi for i = 1, ..., n
G1, L2, regular
E(G, L, <for { i1, ..., in } condition post body>: ForLoop) =
if n >= 1:
let G1, L1, mode = E(G, L, i1, ..., in)
// mode has to be regular or leave due to the syntactic restrictions
if mode is leave then
G1, L1 restricted to variables of L, leave
otherwise
let G2, L2, mode = E(G1, L1, for {} condition post body)
G2, L2 restricted to variables of L, mode
else:
let G1, L1, v = E(G, L, condition)
if v is false:
G1, L1, regular
else:
let G2, L2, mode = E(G1, L, body)
if mode is break:
G2, L2, regular
otherwise if mode is leave:
G2, L2, leave
else:
G3, L3, mode = E(G2, L2, post)
if mode is leave:
G3, L3, leave
otherwise
E(G3, L3, for {} condition post body)
E(G, L, break: BreakContinue) =
G, L, break
E(G, L, continue: BreakContinue) =
G, L, continue
E(G, L, leave: Leave) =
G, L, leave
E(G, L, <if condition body>: If) =
let G0, L0, v = E(G, L, condition)
if v is true:
E(G0, L0, body)
else:
G0, L0, regular
E(G, L, <switch condition case l1:t1 st1 ... case ln:tn stn>: Switch) =
E(G, L, switch condition case l1:t1 st1 ... case ln:tn stn default {})
E(G, L, <switch condition case l1:t1 st1 ... case ln:tn stn default st'>: Switch) =
let G0, L0, v = E(G, L, condition)
// i = 1 .. n
// Evaluate literals, context doesn't matter
let _, _, v1 = E(G0, L0, l1)
...
let _, _, vn = E(G0, L0, ln)
if there exists smallest i such that vi = v:
E(G0, L0, sti)
else:
E(G0, L0, st')
E(G, L, <name>: Identifier) =
G, L, L[$name]
E(G, L, <fname(arg1, ..., argn)>: FunctionCall) =
G1, L1, vn = E(G, L, argn)
...
G(n-1), L(n-1), v2 = E(G(n-2), L(n-2), arg2)
Gn, Ln, v1 = E(G(n-1), L(n-1), arg1)
Let <function fname (param1, ..., paramn) -> ret1, ..., retm block>
be the function of name $fname visible at the point of the call.
Let L' be a new local state such that
L'[$parami] = vi and L'[$reti] = 0 for all i.
Let G'', L'', mode = E(Gn, L', block)
G'', Ln, L''[$ret1], ..., L''[$retm]
E(G, L, l: StringLiteral) = G, L, str(l),
where str is the string evaluation function,
which for the EVM dialect is defined in the section 'Literals' above
E(G, L, n: HexNumber) = G, L, hex(n)
where hex is the hexadecimal evaluation function,
which turns a sequence of hexadecimal digits into their big endian value
E(G, L, n: DecimalNumber) = G, L, dec(n),
where dec is the decimal evaluation function,
which turns a sequence of decimal digits into their big endian value
EVM 방언 (EVM Dialect)
Yul의 기본 방언은 현재 선택된 EVM 버전에 대한 EVM 방언이에요. 이 방언에서 사용할 수 있는 유일한 타입은 이더리움 가상 머신의 256비트 네이티브 타입인 u256이에요. 이것은 이 방언의 기본 타입이므로 생략될 수 있어요.
다음 표는 모든 내장 함수를 나열하고(EVM 버전에 따라 다름) 함수/opcode의 의미에 대한 짧은 설명을 제공해요. 이 문서는 이더리움 가상 머신의 완전한 설명이 되려는 것이 아니에요. 정확한 의미에 관심이 있다면 다른 문서를 참고하세요.
-로 표시된 opcode는 결과를 반환하지 않고, 다른 모든 것은 정확히 하나의 값을 반환해요. F, H, B, C, I, L, P, N, O, A로 표시된 opcode는 각각 Frontier, Homestead, Byzantium, Constantinople, Istanbul, London, Paris, Cancun, Osaka, Amsterdam부터 존재해요.
다음에서 mem[a...b)는 위치 a에서 시작해 위치 b를 포함하지 않는 메모리의 바이트를 나타내고, storage[p]는 슬롯 p의 스토리지 내용을, 마찬가지로 transientStorage[p]는 슬롯 p의 트랜지언트 스토리지 내용을 나타내요.
Yul은 로컬 변수와 제어 흐름을 관리하므로, 이 기능을 간섭하는 opcode는 사용할 수 없어요. 여기에는 dup과 swap 명령뿐 아니라 점프 명령, 레이블, push 명령이 포함돼요.
| 명령 | 설명 | ||
|---|---|---|---|
stop() |
- | F | 실행 중지, return(0, 0)과 동일 |
add(x, y) |
F | x + y | |
sub(x, y) |
F | x - y | |
mul(x, y) |
F | x * y | |
div(x, y) |
F | x / y 또는 y == 0이면 0 | |
sdiv(x, y) |
F | 2의 보수의 부호 있는 숫자에 대한 x / y, y == 0이면 0 | |
mod(x, y) |
F | x % y, y == 0이면 0 | |
smod(x, y) |
F | 2의 보수의 부호 있는 숫자에 대한 x % y, y == 0이면 0 | |
exp(x, y) |
F | x의 y 제곱 | |
not(x) |
F | x의 비트 단위 "not" (x의 모든 비트가 부정됨) | |
lt(x, y) |
F | x < y이면 1, 그 외 0 | |
gt(x, y) |
F | x > y이면 1, 그 외 0 | |
slt(x, y) |
F | 2의 보수의 부호 있는 숫자에 대해 x < y이면 1, 그 외 0 | |
sgt(x, y) |
F | 2의 보수의 부호 있는 숫자에 대해 x > y이면 1, 그 외 0 | |
eq(x, y) |
F | x == y이면 1, 그 외 0 | |
iszero(x) |
F | x == 0이면 1, 그 외 0 | |
and(x, y) |
F | x와 y의 비트 단위 "and" | |
or(x, y) |
F | x와 y의 비트 단위 "or" | |
xor(x, y) |
F | x와 y의 비트 단위 "xor" | |
byte(n, x) |
F | x의 n번째 바이트, 가장 중요한 바이트가 0번째 바이트 | |
shl(x, y) |
C | y를 x 비트만큼 논리 왼쪽 시프트 | |
shr(x, y) |
C | y를 x 비트만큼 논리 오른쪽 시프트 | |
sar(x, y) |
C | y를 x 비트만큼 부호 있는 산술 오른쪽 시프트 | |
clz(x) |
O | x의 선행 0 비트 수, x == 0이면 256 | |
addmod(x, y, m) |
F | 임의 정밀도 산술의 (x + y) % m, m == 0이면 0 | |
mulmod(x, y, m) |
F | 임의 정밀도 산술의 (x * y) % m, m == 0이면 0 | |
signextend(i, x) |
F | 가장 덜 중요한 것에서 세는 (i*8+7)번째 비트부터 부호 확장 | |
keccak256(p, n) |
F | keccak(mem[p…(p+n))) | |
pop(x) |
- | F | 값 x 폐기 |
mload(p) |
F | mem[p…(p+32)) | |
mstore(p, v) |
- | F | mem[p…(p+32)) := v |
mstore8(p, v) |
- | F | mem[p] := v & 0xff (단일 바이트만 수정) |
sload(p) |
F | storage[p] | |
sstore(p, v) |
- | F | storage[p] := v |
tload(p) |
N | transientStorage[p] | |
tstore(p, v) |
- | N | transientStorage[p] := v |
msize() |
F | 메모리 크기, 즉 가장 큰 접근된 메모리 인덱스 | |
gas() |
F | 실행에 여전히 사용 가능한 가스 | |
address() |
F | 현재 컨트랙트 / 실행 컨텍스트의 주소 | |
balance(a) |
F | 주소 a의 wei 잔액 | |
selfbalance() |
I | balance(address())와 동일하지만 더 저렴 | |
caller() |
F | 호출 발신자 (delegatecall 제외) | |
callvalue() |
F | 현재 호출과 함께 보내진 wei | |
calldataload(p) |
F | 위치 p에서 시작하는 호출 데이터 (32바이트) | |
calldatasize() |
F | 호출 데이터의 바이트 크기 | |
calldatacopy(t, f, s) |
- | F | 콜데이터의 위치 f에서 s 바이트를 mem의 위치 t에 복사 |
codesize() |
F | 현재 컨트랙트 / 실행 컨텍스트의 코드 크기 | |
codecopy(t, f, s) |
- | F | 코드의 위치 f에서 s 바이트를 mem의 위치 t에 복사 |
extcodesize(a) |
F | 주소 a의 코드 크기 | |
extcodecopy(a, t, f, s) |
- | F | codecopy(t, f, s)와 같지만 주소 a의 코드를 취함 |
returndatasize() |
B | 마지막 returndata의 크기 | |
returndatacopy(t, f, s) |
- | B | returndata의 위치 f에서 s 바이트를 mem의 위치 t에 복사 |
mcopy(t, f, s) |
- | N | mem 위치 f에서 s 바이트를 mem 위치 t에 복사 |
extcodehash(a) |
C | 주소 a의 코드 해시 | |
create(v, p, n) |
F | 코드 mem[p…(p+n))로 새 컨트랙트를 만들고 v wei를 보내며 새 주소를 반환; 오류 시 0 반환 | |
create2(v, p, n, s) |
C | 주소 keccak256(0xff . this . s . keccak256(mem[p…(p+n)))에서 코드 mem[p…(p+n))로 새 컨트랙트를 만들고 v wei를 보내며 새 주소를 반환. 여기서 0xff는 1바이트 값, this는 20바이트 값의 현재 컨트랙트 주소, s는 big-endian 256비트 값; 오류 시 0 반환 | |
call(g, a, v, in, insize, out, outsize) |
F | 입력 mem[in…(in+insize))로 주소 a의 컨트랙트를 호출해 g 가스와 v wei를 제공하고 출력 영역 mem[out…(out+outsize))로, 오류(예: 가스 부족) 시 0, 성공 시 1 반환 | |
callcode(g, a, v, in, insize, out, outsize) |
F | call과 동일하지만 a의 코드만 사용하고 그 외에는 현재 컨트랙트의 컨텍스트에 머묾 | |
delegatecall(g, a, in, insize, out, outsize) |
H | callcode와 동일하지만 caller와 callvalue도 유지 | |
staticcall(g, a, in, insize, out, outsize) |
B | call(g, a, 0, in, insize, out, outsize)와 동일하지만 상태 수정을 허용하지 않음 | |
return(p, s) |
- | F | 실행 종료, 데이터 mem[p…(p+s)) 반환 |
revert(p, s) |
- | B | 실행 종료, 상태 변경 되돌림, 데이터 mem[p…(p+s)) 반환 |
selfdestruct(a) |
- | F | 실행 종료, 현재 컨트랙트 파괴, 자금을 a에 보냄 (비권장) |
invalid() |
- | F | 유효하지 않은 명령으로 실행 종료 |
log0(p, s) |
- | F | 데이터 mem[p…(p+s)) 기록 |
log1(p, s, t1) |
- | F | 토픽 t1과 함께 데이터 mem[p…(p+s)) 기록 |
log2(p, s, t1, t2) |
- | F | 토픽 t1, t2와 함께 데이터 mem[p…(p+s)) 기록 |
log3(p, s, t1, t2, t3) |
- | F | 토픽 t1, t2, t3와 함께 데이터 mem[p…(p+s)) 기록 |
log4(p, s, t1, t2, t3, t4) |
- | F | 토픽 t1, t2, t3, t4와 함께 데이터 mem[p…(p+s)) 기록 |
chainid() |
I | 실행 중인 체인의 ID (EIP-1344) | |
basefee() |
L | 현재 블록의 기본 수수료 (EIP-3198과 EIP-1559) | |
blobbasefee() |
N | 현재 블록의 blob 기본 수수료 (EIP-7516과 EIP-4844) | |
slotnum() |
A | 현재 비콘 체인 슬롯 번호 (EIP-7843) | |
origin() |
F | 트랜잭션 발신자 | |
gasprice() |
F | 트랜잭션의 가스 가격 | |
blockhash(b) |
F | 블록 nr b의 해시 - 현재를 제외한 마지막 256개 블록만 | |
blobhash(i) |
N | 트랜잭션의 i번째 blob의 버전 해시, blob이 존재하지 않으면 0 | |
coinbase() |
F | 현재 채굴 수혜자 | |
timestamp() |
F | epoch 이후 초 단위의 현재 블록 타임스탬프 | |
number() |
F | 현재 블록 번호 | |
difficulty() |
F | 현재 블록의 난이도 (아래 참고 참고) | |
prevrandao() |
P | 비콘 체인이 제공하는 난수 (아래 참고 참고) | |
gaslimit() |
F | 현재 블록의 블록 가스 한도 |
참고 (Note)
call*명령은 반환 또는 실패 데이터가 놓이는 메모리 영역을 정의하기 위해out과outsize매개변수를 사용해요. 이 영역은 호출된 컨트랙트가 반환하는 바이트 수에 따라 쓰여져요. 더 많은 데이터를 반환하면 처음outsize바이트만 쓰여져요. 나머지 데이터는returndatacopyopcode로 접근할 수 있어요. 더 적은 데이터를 반환하면 나머지 바이트는 전혀 건드려지지 않아요.returndatasizeopcode를 사용해 이 메모리 영역의 어느 부분이 반환 데이터를 포함하는지 확인해야 해요. 나머지 바이트는 호출 전의 값을 유지해요.
참고 (Note)
difficulty()명령은 EVM 버전 >= Paris에서 허용되지 않아요. Paris 네트워크 업그레이드로 이전에difficulty라고 불렸던 명령의 의미가 바뀌었고 명령이prevrandao로 개명됐어요. 이제 전체 256비트 범위에서 임의의 값을 반환할 수 있는 반면, Ethash 안의 기록된 최고 난이도 값은 ~54비트였어요. 이 변경은 EIP-4399에 설명돼 있어요. 컴파일러에서 어떤 EVM 버전이 선택되는지와 무관하게, 명령의 의미는 최종 배포 체인에 의존한다는 점을 주의하세요.
경고 (Warning)
버전 0.8.18부터 Solidity와 Yul 둘 다에서
selfdestruct를 사용하면 비권장 경고가 트리거돼요. EIP-6049에 명시된 대로SELFDESTRUCTopcode가 결국 동작에 breaking change를 겪을 것이기 때문이에요.
일부 내부 방언에는 추가 함수가 있어요:
datasize, dataoffset, datacopy
datasize(x), dataoffset(x), datacopy(t, f, l) 함수는 Yul 객체의 다른 부분에 접근하는 데 사용돼요. datasize와 dataoffset은 문자열 리터럴(다른 객체의 이름)만 인자로 취하고 각각 데이터 영역의 크기와 오프셋을 반환해요. EVM의 경우 datacopy 함수는 codecopy와 동일해요.
setimmutable, loadimmutable
setimmutable(offset, "name", value)과 loadimmutable("name") 함수는 Solidity의 immutable 메커니즘에 사용되며 순수 Yul에 깔끔하게 매핑되지 않아요. setimmutable(offset, "name", value) 호출은 주어진 명명된 immutable을 포함하는 컨트랙트의 런타임 코드가 오프셋 offset에 메모리로 복사됐다고 가정하고, 런타임 코드에서 loadimmutable("name") 호출을 위해 생성된 플레이스홀더를 포함하는 모든 메모리 위치(offset에 상대적인)에 value를 쓸 거예요.
linkersymbol
linkersymbol("library_id") 함수는 링커가 대체할 주소 리터럴의 플레이스홀더예요. 그것의 첫 번째이자 유일한 인자는 문자열 리터럴이어야 하고 삽입될 주소를 고유하게 나타내요. 식별자는 임의적일 수 있지만, 컴파일러가 Solidity 소스에서 Yul 코드를 만들 때는 그 라이브러리를 정의하는 소스 유닛의 이름으로 한정된 라이브러리 이름을 사용해요. 코드를 특정 라이브러리 주소와 링크하려면 명령줄의 --libraries 옵션에 같은 식별자를 제공해야 해요.
예를 들어 이 코드는
open in Remix
let a := linkersymbol("file.sol:Math")
링커가 --libraries "file.sol:Math=0x1234567890123456789012345678901234567890 옵션으로 호출될 때 다음과 동등해요.
open in Remix
let a := 0x1234567890123456789012345678901234567890
Solidity 링커에 대한 자세한 내용은 명령줄 컴파일러 사용하기를 참고해요.
memoryguard
이 함수는 객체와 함께 EVM 방언에서 사용할 수 있어요. let ptr := memoryguard(size)(여기서 size는 리터럴 숫자여야 함)의 호출자는 메모리를 [0, size) 범위나 ptr에서 시작하는 무한 범위 중 하나에서만 사용한다고 약속해요. memoryguard 호출의 존재가 모든 메모리 접근이 이 제한을 따른다는 것을 나타내므로, 최적화 프로그램이 추가 최적화 단계를 수행할 수 있게 해줘요. 예를 들어 그렇지 않으면 도달할 수 없는 스택 변수를 메모리로 옮기려고 시도하는 스택 한도 회피자(stack limit evader) 같은 것.
Yul 최적화 프로그램은 메모리 범위 [size, ptr)만 자신의 목적에 사용한다고 약속해요. 최적화 프로그램이 메모리를 예약할 필요가 없으면 ptr == size를 유지해요. memoryguard는 여러 번 호출될 수 있지만, 하나의 Yul 하위 객체 안에서 같은 리터럴을 인자로 가져야 해요. 하위 객체에서 memoryguard 호출을 하나 이상 찾으면 추가 최적화 단계가 그것에 실행될 거예요.
verbatim
verbatim... 내장 함수 집합은 Yul 컴파일러가 알지 못하는 opcode에 대한 바이트코드를 만들 수 있게 해줘요. 또한 최적화 프로그램이 수정하지 않을 바이트코드 시퀀스를 만들 수 있게 해줘요. 함수들은 verbatim_<n>i_<m>o("<data>", ...) 형태예요. 여기서:
n은 입력 스택 슬롯/변수의 수를 지정하는 0과 99 사이의 십진수.m은 출력 스택 슬롯/변수의 수를 지정하는 0과 99 사이의 십진수.data는 바이트 시퀀스를 포함하는 문자열 리터럴.
예를 들어 최적화 프로그램이 상수 2를 건드리지 않고 입력에 2를 곱하는 함수를 정의하고 싶다면 다음을 사용할 수 있어요.
open in Remix
let x := calldataload(0)
let double := verbatim_1i_1o(hex"600202", x)
이 코드는 x를 검색하기 위한 dup1 opcode(최적화 프로그램이 calldataload의 결과를 직접 재사용할 수도 있지만) 바로 뒤에 600202로 이어질 거예요. 코드는 x의 복사된 값을 소비하고 결과를 스택 상단에 생성한다고 가정돼요. 컴파일러는 그런 다음 double을 위한 스택 슬롯을 할당하고 거기에 결과를 저장하는 코드를 생성해요.
모든 opcode와 마찬가지로 인자는 가장 왼쪽 인자가 위에 있게 스택에 배치되는 반면, 반환 값은 가장 오른쪽 변수가 스택 상단에 있게 배치된다고 가정돼요.
verbatim은 임의 opcode나 Solidity 컴파일러가 알지 못하는 opcode를 생성하는 데 사용될 수 있으므로, verbatim을 최적화 프로그램과 함께 사용할 때 주의해야 해요. 최적화 프로그램이 꺼져 있어도 코드 생성기는 스택 레이아웃을 결정해야 하며, 이는 예를 들어 verbatim을 사용해 스택 높이를 수정하면 정의되지 않은 동작으로 이어질 수 있다는 뜻이에요.
다음은 컴파일러가 확인하지 않는 verbatim 바이트코드에 대한 제한의 완전하지 않은 목록이에요. 이 제한을 위반하면 정의되지 않은 동작이 발생할 수 있어요.
- 제어 흐름은 verbatim 블록 안팎으로 점프해서는 안 되지만, 같은 verbatim 블록 안에서 점프할 수 있어요. 특히 블록에서 revert하거나 return하는 것은 허용되지 않아요.
- 입력과 출력 매개변수 외의 스택 내용에 접근해서는 안 돼요.
- 스택 높이 차이는 정확히
m - n(출력 슬롯 빼기 입력 슬롯)이어야 해요. - Verbatim 바이트코드는 주변 바이트코드에 대한 어떤 가정도 할 수 없어요. 모든 필요한 매개변수는 스택 변수로 전달되어야 해요.
최적화 프로그램은 verbatim 바이트코드를 분석하지 않고 항상 모든 상태 측면을 수정한다고 가정하므로, verbatim 함수 호출을 가로질러 매우 적은 최적화만 할 수 있어요. 최적화 프로그램은 verbatim 바이트코드를 불투명한 코드 블록으로 취급해요. 그것을 분할하지 않지만 이동, 복제하거나 동일한 verbatim 바이트코드 블록과 결합할 수 있어요. verbatim 바이트코드 블록에 제어 흐름으로 도달할 수 없으면 그것을 제거할 수 있어요.
경고 (Warning)
EVM 개선이 기존 스마트 컨트랙트를 깨뜨릴지에 대한 논의 중에, verbatim 안의 기능은 Solidity 컴파일러 자체가 사용하는 것과 같은 고려를 받지 못할 수 있어요.
참고 (Note)
혼란을 피하기 위해
verbatim문자열로 시작하는 모든 식별자는 예약되어 있고 사용자 정의 식별자에 사용될 수 없어요.
Yul 객체의 명세 (Specification of Yul Object)
Yul 객체는 명명된 코드와 데이터 섹션을 그룹화하는 데 사용돼요. datasize, dataoffset, datacopy 함수는 코드 안에서 이 섹션들에 접근하는 데 사용될 수 있어요. 16진 문자열을 사용해 16진 인코딩의 데이터를 지정할 수 있고, 일반 문자열은 네이티브 인코딩으로 지정해요. 코드의 경우 datacopy는 그것의 조립된 이진 표현에 접근할 거예요.
Object = 'object' StringLiteral '{' Code ( Object | Data )* '}'
Code = 'code' Block
Data = 'data' StringLiteral ( HexLiteral | StringLiteral )
HexLiteral = 'hex' ('"' ([0-9a-fA-F]{2})* '"' | '\'' ([0-9a-fA-F]{2})* '\'')
StringLiteral = '"' ([^"\r\n\\] | '\\' .)* '"'
위에서 Block은 이전 장에서 설명한 Yul 코드 문법의 Block을 참조해요.
참고 (Note)
_deployed로 끝나는 이름을 가진 객체는 Yul 최적화 프로그램에 의해 배포된 코드로 취급돼요. 유일한 결과는 최적화 프로그램의 다른 가스 비용 휴리스틱이에요.
참고 (Note)
이름에
.을 포함하는 데이터 객체나 하위 객체는 정의될 수 있지만,.이 다른 객체 안의 객체에 접근하는 구분자로 사용되므로datasize,dataoffset,datacopy로 접근하는 것은 불가능해요.
참고 (Note)
.metadata라고 불리는 데이터 객체는 특별한 의미가 있어요. 코드에서 접근할 수 없고, 객체 안의 위치와 무관하게 항상 바이트코드의 매우 끝에 덧붙여져요. 특별한 의미를 가진 다른 데이터 객체가 미래에 추가될 수 있지만, 그 이름은 항상.로 시작할 거예요.
Yul 객체의 예시가 아래에 나와 있어요:
open in Remix
// A contract consists of a single object with sub-objects representing
// the code to be deployed or other contracts it can create.
// The single "code" node is the executable code of the object.
// Every (other) named object or data section is serialized and
// made accessible to the special built-in functions datacopy / dataoffset / datasize
// The current object, sub-objects and data items inside the current object
// are in scope.
object "Contract1" {
// This is the constructor code of the contract.
code {
function allocate(size) -> ptr {
ptr := mload(0x40)
// Note that Solidity generated IR code reserves memory offset ``0x60`` as well, but a pure Yul object is free to use memory as it chooses.
if iszero(ptr) { ptr := 0x60 }
mstore(0x40, add(ptr, size))
}
// first create "Contract2"
let size := datasize("Contract2")
let offset := allocate(size)
// This will turn into codecopy for EVM
datacopy(offset, dataoffset("Contract2"), size)
// constructor parameter is a single number 0x1234
mstore(add(offset, size), 0x1234)
pop(create(0, offset, add(size, 32)))
// now return the runtime object (the currently
// executing code is the constructor code)
size := datasize("Contract1_deployed")
offset := allocate(size)
// This will turn into a codecopy for EVM
datacopy(offset, dataoffset("Contract1_deployed"), size)
return(offset, size)
}
data "Table2" hex"4123"
object "Contract1_deployed" {
code {
function allocate(size) -> ptr {
ptr := mload(0x40)
// Note that Solidity generated IR code reserves memory offset ``0x60`` as well, but a pure Yul object is free to use memory as it chooses.
if iszero(ptr) { ptr := 0x60 }
mstore(0x40, add(ptr, size))
}
// runtime code
mstore(0, "Hello, World!")
return(0, 0x20)
}
}
// Embedded object. Use case is that the outside is a factory contract,
// and Contract2 is the code to be created by the factory
object "Contract2" {
code {
// code here ...
}
object "Contract2_deployed" {
code {
// code here ...
}
}
data "Table1" hex"4123"
}
}
Yul 최적화 프로그램 (Yul Optimizer)
Yul 최적화 프로그램은 Yul 코드에 대해 동작하며 입력, 출력, 중간 상태에 같은 언어를 사용해요. 이는 최적화 프로그램의 쉬운 디버깅과 검증을 허용해요. 다른 최적화 단계와 최적화 프로그램 사용 방법에 대한 자세한 내용은 일반 최적화 프로그램 문서를 참고해요.
Solidity를 독립 실행 Yul 모드로 사용하고 싶다면 --optimize로 최적화 프로그램을 활성화하고 선택적으로 --optimize-runs로 기대하는 컨트랙트 실행 수를 지정해요:
solc --strict-assembly --optimize --optimize-runs 200
Solidity 모드에서는 Yul 최적화 프로그램이 일반 최적화 프로그램과 함께 활성화돼요.
최적화 단계 순서 (Optimization Step Sequence)
최적화 순서와 약어 목록에 대한 자세한 정보는 최적화 프로그램 문서에서 사용할 수 있어요.
완전한 ERC20 예시 (Complete ERC20 Example)
open in Remix
object "Token" {
code {
// Store the creator in slot zero.
sstore(0, caller())
// Deploy the contract
datacopy(0, dataoffset("runtime"), datasize("runtime"))
return(0, datasize("runtime"))
}
object "runtime" {
code {
// Protection against sending Ether
require(iszero(callvalue()))
// Dispatcher
switch selector()
case 0x70a08231 /* "balanceOf(address)" */ {
returnUint(balanceOf(decodeAsAddress(0)))
}
case 0x18160ddd /* "totalSupply()" */ {
returnUint(totalSupply())
}
case 0xa9059cbb /* "transfer(address,uint256)" */ {
transfer(decodeAsAddress(0), decodeAsUint(1))
returnTrue()
}
case 0x23b872dd /* "transferFrom(address,address,uint256)" */ {
transferFrom(decodeAsAddress(0), decodeAsAddress(1), decodeAsUint(2))
returnTrue()
}
case 0x095ea7b3 /* "approve(address,uint256)" */ {
approve(decodeAsAddress(0), decodeAsUint(1))
returnTrue()
}
case 0xdd62ed3e /* "allowance(address,address)" */ {
returnUint(allowance(decodeAsAddress(0), decodeAsAddress(1)))
}
case 0x40c10f19 /* "mint(address,uint256)" */ {
mint(decodeAsAddress(0), decodeAsUint(1))
returnTrue()
}
default {
revert(0, 0)
}
function mint(account, amount) {
require(calledByOwner())
mintTokens(amount)
addToBalance(account, amount)
emitTransfer(0, account, amount)
}
function transfer(to, amount) {
executeTransfer(caller(), to, amount)
}
function approve(spender, amount) {
revertIfZeroAddress(spender)
setAllowance(caller(), spender, amount)
emitApproval(caller(), spender, amount)
}
function transferFrom(from, to, amount) {
decreaseAllowanceBy(from, caller(), amount)
executeTransfer(from, to, amount)
}
function executeTransfer(from, to, amount) {
revertIfZeroAddress(to)
deductFromBalance(from, amount)
addToBalance(to, amount)
emitTransfer(from, to, amount)
}
/* ---------- calldata decoding functions ----------- */
function selector() -> s {
s := div(calldataload(0), 0x100000000000000000000000000000000000000000000000000000000)
}
function decodeAsAddress(offset) -> v {
v := decodeAsUint(offset)
if iszero(iszero(and(v, not(0xffffffffffffffffffffffffffffffffffffffff)))) {
revert(0, 0)
}
}
function decodeAsUint(offset) -> v {
let pos := add(4, mul(offset, 0x20))
if lt(calldatasize(), add(pos, 0x20)) {
revert(0, 0)
}
v := calldataload(pos)
}
/* ---------- calldata encoding functions ---------- */
function returnUint(v) {
mstore(0, v)
return(0, 0x20)
}
function returnTrue() {
returnUint(1)
}
/* -------- events ---------- */
function emitTransfer(from, to, amount) {
let signatureHash := 0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef
emitEvent(signatureHash, from, to, amount)
}
function emitApproval(from, spender, amount) {
let signatureHash := 0x8c5be1e5ebec7d5bd14f71427d1e84f3dd0314c0f7b2291e5b200ac8c7c3b925
emitEvent(signatureHash, from, spender, amount)
}
function emitEvent(signatureHash, indexed1, indexed2, nonIndexed) {
mstore(0, nonIndexed)
log3(0, 0x20, signatureHash, indexed1, indexed2)
}
/* -------- storage layout ---------- */
function ownerPos() -> p { p := 0 }
function totalSupplyPos() -> p { p := 1 }
function accountToStorageOffset(account) -> offset {
offset := add(0x1000, account)
}
function allowanceStorageOffset(account, spender) -> offset {
offset := accountToStorageOffset(account)
mstore(0, offset)
mstore(0x20, spender)
offset := keccak256(0, 0x40)
}
/* -------- storage access ---------- */
function owner() -> o {
o := sload(ownerPos())
}
function totalSupply() -> supply {
supply := sload(totalSupplyPos())
}
function mintTokens(amount) {
sstore(totalSupplyPos(), safeAdd(totalSupply(), amount))
}
function balanceOf(account) -> bal {
bal := sload(accountToStorageOffset(account))
}
function addToBalance(account, amount) {
let offset := accountToStorageOffset(account)
sstore(offset, safeAdd(sload(offset), amount))
}
function deductFromBalance(account, amount) {
let offset := accountToStorageOffset(account)
let bal := sload(offset)
require(lte(amount, bal))
sstore(offset, sub(bal, amount))
}
function allowance(account, spender) -> amount {
amount := sload(allowanceStorageOffset(account, spender))
}
function setAllowance(account, spender, amount) {
sstore(allowanceStorageOffset(account, spender), amount)
}
function decreaseAllowanceBy(account, spender, amount) {
let offset := allowanceStorageOffset(account, spender)
let currentAllowance := sload(offset)
require(lte(amount, currentAllowance))
sstore(offset, sub(currentAllowance, amount))
}
/* ---------- utility functions ---------- */
function lte(a, b) -> r {
r := iszero(gt(a, b))
}
function gte(a, b) -> r {
r := iszero(lt(a, b))
}
function safeAdd(a, b) -> r {
r := add(a, b)
if or(lt(r, a), lt(r, b)) { revert(0, 0) }
}
function calledByOwner() -> cbo {
cbo := eq(owner(), caller())
}
function revertIfZeroAddress(addr) {
require(addr)
}
function require(condition) {
if iszero(condition) { revert(0, 0) }
}
}
}
}