Smalltalk80LanguageImplementationKor:Chapter 26
- 제 26 장 구현
구현
Smalltalk-80 시스템은 두 가지 주요 구성요소로 구별할 수 있는데, "virtual image"(가상 이미지)와 "virtual machine"(가상 머신)이 그것이다.
- 가상 이미지는 시스템 내 모든 객체로 구성된다.
- 가상 머신은 가상 이미지에서 객체에 동력학을 제공하는 기계어(또는 마이크로코드) 루틴과 하드웨어 장치로 구성된다.
시스템 구현자가 할 일은 가상 머신을 생성하는 일이다. 그리고 나면 가상 머신으로 가상 이미지를 로딩할 수 있고, Smalltalk-80 시스템은 앞의 여러 장에서 설명한 바와 같이 대화형 개체가 된다.
이번 장에서 제공하는 Smalltalk-80 구현의 개요는 프로그래머가 작성한 소스 메서드부터 시작해 하향식으로 구성된다. 이러한 메서드는 "compiler"(컴파일러)에 의해 "bytecodes"(바이트코드)라 불리는 8 자릿수 명령의 시퀀스로 해석된다. 컴파일러와 바이트코드는 이번 장의 첫 번째 절에서 살펴볼 주제이다. 컴파일러가 생성한 바이트코드는 "interpreter"(인터프리터)에 대한 명령으로, 이는 두 번째 절에서 살펴볼 주제에 해당한다. 구현에서 인터프리터 아래에는 가상 이미지를 구성하는 객체를 저장하는 "object memory"(객체 메모리)가 있다. 객체 메모리는 세 번째 절에서 설명하겠다. 구현의 젤 밑에는 "hardware"(하드웨어)가 위치한다. 따라서 마지막 절에서는 인터프리터와 객체 메모리를 구현하는 데 필요한 하드웨어를 논할 것이다. 제 27-30장은 가상 머신의 인터프리터와 객체 메모리에 대한 상세한 명세를 제공한다.
컴파일러
프로그래머가 작성한 소스 메서드는 Smalltalk-80 시스템에서 String의 인스턴스로서 표현된다. Strings는 본 서적의 제 1부에 소개된 구문에 따른 문자 시퀀스를 포함한다. 가령, 아래의 소스 메서드는 Rectangle 클래스의 인스턴스가 어떻게 단항 메시지 center에 응답하는지 설명할 것이다. Rectangle의 사면 중에서 등거리에 있는 Point를 찾기 위해 center 메시지가 사용된다.
center
↑orgin + corner / 2
소스 메서드는 시스템의 "compiler"(컴파일러)에 의해 스택 위주의 인터프리터를 위한 명령의 시퀀스로 해석된다. 명령은 "bytecodes"(바이트코드)라고 불리는 8자릿수 숫자로 되어 있다. 예를 들어, 위에서 소개한 소스 메서드에 해당하는 바이트코드는 아래와 같다.
0, 1, 176, 119, 185, 124
바이트코드의 값이 그 의미와 관련해 인터프리터에게 제공하는 바는 거의 없기 때문에 이번 장에서는 바이트코드의 목록과 함께 그 기능에 대한 주석을 덧붙일 것이다. 바이트코드의 주석 중에서 그것이 나타나는 메서드의 컨텍스트에 의존하는 부분은 괄호로 표기될 것이다. 괄호에 포함되지 않은 주석 부분은 일반 기능을 설명한다. 가령 바이트코드 0은 항상 인터프리터에게 수신자의 첫 번째 인스턴스 변수 값을 스택으로 밀어넣도록 명령한다. 변수가 origin으로 명명된다는 사실은 이러한 메서드가 Rectangles에 의해 사용된다는 사실에 의존하므로, origin에 괄호가 매겨진다. Rectangle center에 대한 바이트코드의 주석 형태는 다음과 같다.
Rectangle | center |
0 | 수신자의 첫 번째 인스턴스 변수(origin) 값을 스택의 맨 위로 밀어넣는다. |
1 | 수신자의 두 번째 인스턴스 변수(corner) 값을 스택의 맨 위로 밀어넣는다. |
176 | 선택자가 +인 이항 메시지를 전송한다. |
119 | SmallInteger 2를 스택에 밀어넣는다. |
185 | 선택자가 /인 이항 메시지를 전송한다. |
124 | 스택의 상단에 있는 객체를 메시지의 (center) 값으로 리턴한다. |
일부 바이트코드에서 언급한 스택은 여러 목적으로 사용된다. 이번 메서드에서는 수신자, 인자, 그리고 전송된 두 메시지의 결과를 보유하는 데 사용된다. 스택은 center 메서드로부터 리턴되어야 할 결과의 소스로서 사용되기도 한다. 스택은 인터프리터에 의해 사용되는데, 다음 절에서 상세히 설명하겠다. 이번 절의 끝부분에서 모든 유형의 바이트코드에 대한 설명을 제공하겠다.
프로그래머는 컴파일러와 직접적으로 상호작용하지 않는다. 새로운 소스 메서드가 클래스로 추가되면 (이번 예제에서는 Rectangle) 클래스가 컴파일러에게 소스 메서드의 바이트코드 해석을 포함하는 CompiledMethod의 인스턴스를 요청한다. 클래스는 소스 메서드에서 제공되지 않은 필수 정보를 컴파일러에게 제공하는데, 접근 가능한 공유 변수(전역, 클래스, pool 변수)를 포함하는 사전과 수신자의 인스턴스 변수의 이름이 그러한 정보에 해당한다. 컴파일러는 소스 텍스트를 CompiledMethod로 해석하고, 클래스는 메시지 사전에 메서드를 저장한다. 가령 위에서 소개한 CompiledMethod는 선택자 center와 연관된 Rectangle의 메시지 사전에 저장된다.
소스 메서드로부터 컴파일된 바이트코드의 또 다른 예는 store 바이트코드의 사용을 설명한다. Rectangle로 전송되는 extent: to 메시지는 수신자의 너비와 높이를 인자의 (Point) x와 y 좌표와 동일하게 변경한다. 수신자의 상단 좌측 모서리(origin)는 동일하게 유지되고, 하단 우측 모서리(corner)는 이동한다.
extent: newExtent
corner ← origin + newExtent
Rectangle | extent: |
0 | 수신자의 첫 번째 인스턴스 변수(origin)를 스택의 맨 위로 밀어넣는다. |
16 | 인자(newExtent)를 스택의 맨 위로 밀어넣는다. |
176 | 선택자가 +인 이항 메시지를 전송한다. |
97 | 스택에서 맨 위의 객체를 꺼내 수신자의 두 번째 인스턴스 변수(corner)에 저장한다. |
120 | 수신자를 메시지(extent:)의 값으로 리턴한다. |
소스 메서드와 컴파일된 바이트코드의 형태는 여러 면에서 다르다. 소스 메서드에서 변수명은 스택에 객체를 밀어넣기 위한 명령으로 변환되고, 선택자는 메시지를 전송하기 위한 명령으로 변환되며, 윗방향 화살표는 결과를 리턴하기 위한 명령으로 반환된다. 그에 해당하는 구성요소의 정렬 또한 소스 메서드와 컴파일된 바이트코드에서 차이가 있다. 이렇게 소스 메서드와 컴파일된 바이트코드는 형태에는 차이가 있을지 몰라도 동일한 액션을 설명한다.
컴파일된 메서드
컴파일러는 소스 메서드의 바이트코드 해석을 보유하기 위해 CompiledMethod의 인스턴스를 생성한다. 바이트코드 외에도 CompiledMethod는 "literal frame"(리터럴 프레임)이라는 객체 집합을 포함한다. 리터럴 프레임은 바이트코드가 직접 참조할 수 없는 객체는 무엇이든 포함할 수 있다. Rectangle center와 Rectangle extent:에 있는 모든 객체는 바이트코드에 의해 직접 참조되었기 때문에 이러한 메서드에 대한 CompiledMethods는 리터럴 프레임을 필요로 하지 않는다. 리터럴 프레임이 있는 CompiledMethod의 예로, Rectangle intersects: 에 대한 메서드를 고려해보자. intersects: 메시지는 하나의 Rectangle(수신자)이 다른 Rectangle(인자)과 겹치는지 질의한다.
intersects: aRectangle
↑(origin max: aRectangle origin) < (corner min: aRectangle corner)
바이트코드가 직접 참조할 수 있는 집합에 포함되지 않은 메시지 선택자가 네 개 있는데, max:, origin, min:, corner이다. 이러한 선택자들은 CompiledMethod의 리터럴 프레임에 포함되어 있고, 두 번째 바이트코드는 리터럴 프레임에서 그 위치에 따라 선택자를 참조한다. CompiledMethod의 리터럴 프레임은 바이트코드 다음에 표시될 것이다.
Rectangle | intersects: |
0 | 수신자의 첫 번째 인스턴스 변수의 (origin) 값을 스택의 맨 위로 밀어넣는다. |
16 | 인자를 (aRectangle) 밀어넣는다. |
209 | 두 번째 리터럴 프레임 위치에서 (origin) 선택자가 있는 단항 메시지를 전송한다. |
224 | 첫 번째 리터럴 프레임 위치에서 (max:) 선택자가 있는 단일 인자 메시지를 전송한다. |
1 | 수신자의 두 번째 인스턴스 변수의 (corner) 값을 스택 맨 위로 밀어넣는다. |
16 | 인자를 (aRectangle) 스택 맨 위로 밀어넣는다. |
211 | 네 번째 리터럴 프레임 위치에서 (corner) 선택자가 있는 unary 메시지를 전송한다. |
226 | 세 번째 리터럴 프레임 위치에서 (min:) 선택자가 있는 단일 인자 메시지를 전송한다. |
178 | 선택자가 < 인 이항 메시지를 전송한다. |
124 | 스택 맨 위의 객체를 메시지의 값으로 리턴한다 (intersects:). |
literal frame
#max:
#origin
#min:
#corner
|
바이트코드가 직접 참조할 수 있는 객체의 범주는 다음과 같다:
- 호출하는 메시지의 인자와 수신자
- 수신자의 인스턴스 변수 값
- 메서드가 필요로 하는 임시 변수의 값
- 7개의 특수 상수 (true, false, nil, -1, 0, 1, 2)
- 32개의 특수 메시지 선택자
32개의 특수 메시지 선택자는 다음과 같다.
+ | - | < | > | |
< = | > = | = | ~= | |
* | / | \ | @ | |
bitShift: | \\ | bitAnd: | bitOr: | |
(at:) | (at:put:) | (size) | (next) | |
(nextPut:) | (atEnd) | = = | class | |
blockCopy: | value | value: | (do:) | |
(new) | (new:) | (x) | (y) |
괄호 안의 선택자는 컴파일러를 수정하고 시스템 내 모든 메서드를 재컴파일하여 다른 선택자로 대체할 수 있다. 그 외 선택자는 가상 머신에 빌드된다.
CompiledMethod의 바이트코드에서 참조되는 객체 중에 위의 범주에 속하지 않는 객체는 리터럴 프레임에 나타날 것이다. 리터럴 프레임에 주로 포함된 객체는 다음과 같다.
- 공유 변수 (전역, 클래스, pool)
- 대부분의 리터럴 상수 (숫자, 문자, 문자열, 배열, 부호)
- 대부분의 메시지 선택자 (특수에 해당하지 않는 메시지 선택자)
이 세 가지 유형의 객체는 리터럴 프레임에서 혼합되기도 한다. 리터럴 프레임에 있는 객체가 동일한 메서드에서 두 번 참조될 경우 리터럴 프레임에는 한 번만 나타나야 한다. 그러한 객체를 참조하는 두 개의 바이트코드는 리터럴 프레임에서 동일한 위치를 참조할 것이다.
위에서 참조된 두 가지 유형의 객체, 즉 임시 변수와 공유 변수는 메서드 예제에서 사용되지 않았다. 아래에 Rectangle merge: 에 대한 메서드 예제에서는 두 가지를 사용한다. merge: 메시지를 이용해 수신자와 인자에서 모두 영역을 포함하는 Rectangle을 찾는다.
merge: aRectangle
| minPoint maxPoint |
minPoint ← origin min: aRectangle origin.
maxPoint ← corner max: aRectangle corner.
↑Rectangle origin: minPoint
corner: maxPoint
CompiledMethod가 임시 변수(이번 예제에서는 maxPoint와 minPoint)를 사용할 때는 필요한 개수가 출력된 폼의 첫행에 명시된다. CompiledMethod가 공유 변수를 (이번 예제에서 Rectangle) 사용할 경우, Association의 인스턴스가 그 리터럴 프레임에 포함된다. 특정 공유 변수명을 참조하는 모든 CompiledMethods는 리터럴 프레임에 동일한 Association을 포함한다.
Rectangle | merge: 는 2개의 임시 변수가 필요 |
0 | 수신자의 첫 번째 인스턴스 변수의 (origin) 값을 스택의 맨 위로 밀어넣는다. |
16 | 첫 번째 임시 프레임 위치의 (인자 aRectangle) 내용을 스택의 맨 위로 밀어넣는다. |
209 | 두 번째 리터럴 프레임 위치에서 (origin) 선택자가 있는 단항 메시지를 전송한다. |
224 | 첫 번째 리터럴 프레임 위치에서 (min:) 선택자가 있는 단일 인자 메시지를 전송한다. |
105 | 스택에서 맨 위의 객체를 꺼내 두 번째 임시 프레임 위치에 (minPoint) 저장한다. |
1 | 수신자의 두 번째 인스턴스 변수의 (corner) 값을 스택의 맨 위로 밀어넣는다. |
16 | 첫 번째 임시 프레임 위치의 (인자 aRectangle) 내용을 스택 맨 위로 밀어넣는다. |
211 | 네 번째 프레임 위치에서 (corner) 선택자가 있는 단항 메시지를 전송한다. |
226 | 세 번째 리터럴 프레임 위치에서 (max:) 선택자가 있는 인자 메시지를 전송한다. |
106 | 스택에서 맨 위의 객체를 꺼내 세 번째 임시 프레임 위치에 (maxPoint) 저장한다. |
69 | 여섯 번째 리터럴 프레임 위치에서 (Rectangle) 공유 변수의 값을 스택 맨 위로 밀어넣는다. |
17 | 두 번째 임시 프레임 위치의 (minPoint) 내용을 스택의 맨 위로 밀어넣는다. |
18 | 세 번째 임시 프레임 위치의 (maxPoint) 내용을 스택의 맨 위로 밀어넣는다. |
244 | 다섯 번째 리터럴 프레임 위치에서 (origin:corner:) 선택자가 있는 2 인자 메시지를 전송한다. |
124 | 스택에서 맨 위의 객체를 메시지의 (merge:) 값으로 리턴한다. |
literal frame
#min:
#origin
#max:
#corner
#origin:corner:
Association: #Rectangle ➛ Rectangle
|
❏ 임시 변수. 임시 변수는 CompiledMethod의 특정 실행을 위해 생성되고 실행이 완료되면 존재도 멈춘다. CompiledMethod는 얼마나 많은 임시 변수가 필요할 것인지 인터프리터에게 표시한다. 호출하는 메시지의 인자와 임시 변수의 값은 모두 "temporary frame"(임시 프레임)에 저장된다. 인자가 먼저 저장되고, 직후에 임시 변수값이 저장된다. 이는 동일한 유형의 바이트코드(주석이 임시 프레임 위치를 참조하는)에 의해 접근된다. merge:은 단일 인자를 취하므로 두 임시 변수는 임시 프레임에서 두 번째와 세 번째 위치를 사용한다. 컴파일러는 인자가 위치한 임시 프레임 부분을 참조하는 저장 바이트코드를 전혀 issue하지 않음으로써 인자명의 값을 변경할 수 없다는 사실을 강요한다.
❏ 공유 변수. 공유 변수는 사전에서 찾을 수 있다.
- 사전 내 "global variables"(전역 변수)의 이름은 어떤 메서드에서든 표시된다
- 사전 내 "class variables"(클래스 변수)의 이름은 단일 클래스와 그 서브클래스의 메서드에서만 표시된다
- 사전 내 "pool variables"(pool 변수)의 이름은 몇몇 클래스의 메서드에 표시된다
공유 변수는 이러한 사전을 구성하는 각 연관이다. 시스템은 Association의 인스턴스와의 일반적인 연관, 특히 공유 변수와의 연관을 나타낸다. 컴파일러가 소스 메서드에서 공유 변수의 이름을 마주치면 똑같은 이름으로 된 Association이 CompiledMethod의 리터럴 프레임에 포함된다. 공유 변수로 접근하는 바이트코드는 리터럴 프레임에서 Association의 위치를 나타낸다. 변수의 실제 값은 Association의 인스턴스 변수에 저장된다. 위에서 표시한 Rectangle merge: 에 대한 CompiledMethod에서 Rectangle 클래스는 부호 #Rectangle을 이름으로 갖고 Rectangle 클래스를 값으로 가진 전역 사전으로부터 Association을 포함시킴으로써 참조된다.
바이트코드
인터프리터는 5개의 범주, pushes, stores, sends, returns, jumps에 해당하는 256개 바이트코드 명령을 이해한다. 이번 절에서는 어떤 바이트코드가 어떤 명령을 나타내는지 상세히 살펴보는 대신 각 바이트코드 유형에 대해 일반적인 설명만 제공할 것이다. 각 바이트코드의 정확한 의미는 28장에 설명되어 있다. 인터프리터에 필요한 명령은 256개가 넘기 때문에 일부 바이트코드는 확장(extensions)을 취한다. 확장은 바이트코드 다음에 따라오는 1 또는 2 바이트로, 명령을 추가로 명시한다. 확장은 따로 명령으로 사용할 수 없으며, 명령의 일부일 뿐이다.
❏ Push 바이트코드. Push 바이트코드는 인터프리터의 스택 맨 위로 추가해야 할 객체의 소스를 나타낸다. 소스는 다음을 포함한다.
- CompiledMethod를 호출한 메시지의 수신자
- 수신자의 인스턴스 변수
- 임시 프레임 (임시 변수와 메시지의 인자)
- CompiledMethod의 리터럴 프레임
- 스택의 맨 위 (예: 해당 바이트코드는 스택의 맨 위를 복사한다)
Push 바이트코드 대부분 유형은 예제에 포함되어 있다. 스택의 맨 위를 복사하는 바이트코드를 이용하여 계단식(cascaded) 메시지가 구현된다.
두 가지 유형의 push 바이트코드가 리터럴 프레임을 소스로 사용한다. 하나는 리터럴 상수를 밀어넣는 데 사용되고, 나머지는 공유 변수의 값을 밀어넣는 데 사용된다. 리터럴 상수는 리터럴 프레임에 직접 저장되지만 공유 변수의 값은 리터럴 프레임이 가리키는 Association에 저장된다. 다음으로 소개할 메서드 예제는 하나의 공유 변수와 하나의 리터럴 상수를 사용한다.
incrementIndex
↑Index + Index + 4
ExampleClass | incrementIndex |
64 | 첫 번째 리터럴 프레임 위치에서 (Index) 공유 변수의 값을 스택의 맨 위로 밀어넣는다. |
33 | 두 번째 리터럴 프레임 위치에서 (4) 상수를 스택의 맨 위로 밀어넣는다. |
176 | 선택자가 +인 이항 메시지를 전송한다. |
129,192 | 공유 변수에서 스택의 맨 위에 위치한 객체를 첫 번째 리터럴 프레임 위치에 (Index) 저장한다. |
124 | 스택의 맨 위에 객체를 메시지의 (incrementIndex) 값으로 리턴한다. |
literal frame
Association: #Index ➛ 260
4
|
❏ Store 바이트코드. 할당 표현식으로부터 컴파일된 바이트코드는 store 바이트코드로 끝난다. Store 바이트코드 이전의 바이트코드는 변수의 새로운 값을 계산하고 스택의 맨 위에 남겨둔다. Store 바이트코드는 값이 변경되어야 하는 변수를 나타낸다. 변경이 가능한 변수는 다음과 같다.
- 수신자의 인스턴스 변수
- 임시 변수
- 공유 변수
몇몇 store 바이트코드는 저장할 객체를 스택으로부터 제거하고, 그 외는 객체를 저장한 다음 스택의 맨 위에 남겨둔다.
❏ Send 바이트코드. Send 바이트코드는 전송할 메시지의 선택자, 그리고 메시지가 가져야 하는 인자 개수를 명시한다. 메시지의 인자와 수신자가 인터프리터의 스택으로부터 꺼내지는데 수신자는 인자 아래에 위치한다. send 이후의 바이트코드가 실행될 무렵이면 메시지의 결과는 스택의 맨 위에 있는 인자와 수신자를 교체했을 것이다. 메시지 전송과 결과 리턴에 대한 상세한 내용은 다음 절에서 다루겠다. 23개의 send 바이트코드 집합은 앞서 열거한 특수 선택자를 직접적으로 참조한다. 그 외 send 바이트코드는 리터럴 프레임에서 선택자를 참조한다.
❏ Return 바이트코드. Return 바이트코드를 마주치면 그것이 발견된 CompiledMethod가 완전히 실행된다. 따라서 CompiledMethod를 호출한 메시지에 대한 값이 리턴된다. 값은 주로 스택의 맨 위에서 발견된다. 네 가지 특수 return 바이트코드가 메시지 수신자(self), true, false, nil을 리턴한다.
❏ Jump 바이트코드. 보통 인터프리터는 CompiledMethod에 나타나는 순서대로 바이트코드를 실행한다. Jump 바이트코드는 다음으로 실행할 바이트코드가 jump 다음에 오는 바이트코드가 아님을 나타낸다. Jump의 종류에는 두 가지가 있는데, "unconditional"(무조건적) jump와 "conditional"(조건적) jump이다. 무조건적 jump는 스택의 맨 위가 명시된 값일 경우 제어를 전송할 것이다. 일부 조건적 jump는 스택의 맨 위 객체가 true일 경우 전송하고, 나머지 조건적 jump는 false일 경우 전송한다. Jump 바이트코드는 효율적인 제어 구조체를 구현하는 데 사용된다.
컴파일러에 의해 최적화된 제어 구조체로는 Booleans로 전송되는 조건부 선택 메시지(ifTrue:, ifFalse:, ifTrue:ifFalse:), Booleans로 전송되는 일부 논리 연산 메시지(and:, or:), 블록으로 전송되는 조건부 반복 메시지(whileTrue:, whileFalse:)가 있다. Jump 바이트코드는 jump의 위치를 기준으로 다음으로 실행할 바이트코드를 나타낸다. 다시 말해, 건너뛰어야 할 바이트코드 수를 인터프리터에게 알려준다. Rectangle includesPoint: 에 대한 다음 메서드는 조건부 jump를 사용한다.
includesPoint: aPoint
origin < = aPoint
ifTrue: [↑aPoint < corner]
ifFalse: [↑false]
Rectangle | includesPoint: |
0 | 수신자의 첫 번째 인스턴스 변수 (origin) 값을 스택 맨 위로 밀어넣는다. |
16 | 첫 번째 임시 프레임 위치의 (인자 aPoint) 내용을 스택 맨 위로 밀어넣는다. |
180 | 선택자가 <=인 이항 메시지를 전송한다. |
155 | 스택 맨 위의 객체가 false일 경우 4 바이트코드를 앞으로 건너뛴다. |
16 | 첫 번째 임시 프레임 위치의 (인자 aPoint) 내용을 스택 맨 위로 밀어넣는다. |
1 | 수신자의 두 번째 인스턴스 변수의 (corner) 값을 스택 맨 위로 밀어넣는다. |
178 | 선택자가 <인 이항 메시지를 전송한다 |
124 | 스택에서 맨 위의 객체를 메시지의 (includesPoint:) 값으로 리턴한다 |
122 | false를 메시지의 (includesPoint:) 값으로 리턴한다 |
인터프리터
Smalltalk-80 인터프리터는 CompiledMethods에 발견된 바이트코드 명령을 실행한다. 인터프리터는 5개의 정보 조각을 사용하고 3단계 주기를 반복하여 실행한다.
- 인터프리터의 상태
- 바이트코드가 실행 중인 CompiledMethod.
- 해당 CompiledMethod에서 실행될 다음 바이트코드의 위치. 이는 인터프리터의 "instruction pointer"(명령 포인터)이다.
- CompiledMethod를 호출한 메시지의 인자와 수신자.
- CompiledMethod가 필요로 하는 임시 변수.
- 스택
대부분의 바이트코드 실행은 인터프리터의 스택을 수반한다. Push 바이트코드는 스택으로 추가해야 할 객체를 어디서 찾을 것인지 알려준다. Store 바이트코드는 발견된 객체를 스택의 어디에 놓을 것인지 알려준다. Send 바이트코드는 스택으로부터 메시지의 인자와 수신자를 제거한다. 메시지 결과가 계산되면 스택으로 밀어넣는다.
- 인터프리터의 주기
- 명령 포인터가 가리키는 바이트코드를 CompiledMethod로부터 꺼낸다.
- 명령 포인터를 증가시킨다.
- 바이트코드가 명시한 함수를 실행한다.
인터프리터의 기능에 대한 예로 Rectangle center에 대한 CompiledMethod의 실행을 추적할 것이다. 인터프리터의 상태는 각 주기 다음에 표시한다. 명령 포인터는 CompiledMethod에서 다음으로 실행될 바이트코드를 가리키는 화살표로 표시된다.
➧ 0 | 수신자의 첫 번째 인스턴스 변수의 (origin) 값을 스택 맨 위로 밀어넣는다. |
수신자, 인자, 임시 변수, 스택 위의 객체는 일반적으로 출력되듯이 표시할 것이다 (printString에 대한 응답에 따라). 가령, Rectangle로 메시지가 전송되면 수신자는 아래와 같이 표시될 것이다.
Receiver||100@100 corner: 200@200
실행이 시작될 때 스택은 비어 있고 명령 포인터는 CompiledMethod에서 첫 번째 바이트코드를 나타낸다. 이러한 CompiledMethod는 임시 변수를 필요로 하지 않으며 호출하는 메시지는 인자를 갖고 있지 않으므로 이 두 범주 또한 비어 있다.
Rectangle center에 대한 메서드 | |
➧ 0 | 수신자의 첫 번째 인스턴스 변수의 (origin) 값을 스택 맨 위로 밀어넣는다. |
1 | 수신자의 두 번째 인스턴스 변수의 (corner) 값을 스택 맨 위로 밀어넣는다. |
176 | 선택자가 +인 이항 메시지를 전송한다. |
119 | SmallInteger 2를 스택 맨 위로 밀어넣는다. |
185 | 선택자가 /인 이항 메시지를 전송한다. |
124 | 스택에서 맨 위의 객체를 메시지의 (center) 값으로 리턴한다. |
Receiver | 100@100 corner: 200@200 |
Arguments | |
Temporary Variables | |
Stack |
인터프리터 주기 1회가 끝나면 명령 포인터가 앞서고 수신자의 첫 번째 인스턴스 변수 값이 스택으로 복사된다.
Rectangle center에 대한 메서드 | |
0 | 수신자의 첫 번째 인스턴스 변수의 (origin) 값을 스택 맨 위로 밀어넣는다. |
➧ 1 | 수신자의 두 번째 인스턴스 변수의 (corner) 값을 스택 맨 위로 밀어넣는다. |
176 | 선택자가 +인 이항 메시지를 전송한다. |
119 | SmallInteger 2를 스택 맨 위로 밀어넣는다. |
185 | 선택자가 / 인 이항 메시지를 전송한다. |
124 | 스택에서 맨 위에 있는 객체를 메시지의 (center) 값으로 리턴한다. |
Receiver | 100@100 corner: 200@200 |
Arguments | |
Temporary Variables | |
Stack | 100@100 |
인터프리터의 두 번째 주기는 첫 번째 주기와 동일한 효과가 있다. 스택의 상단은 페이지의 하단으로 표시된다. 이는 페이지의 아래로 향할수록 어드레스가 증가하는 메모리 위치에서 일반적으로 사용되는 규칙이다.
Rectangle center에 대한 메서드 | |
0 | 수신자의 첫 번째 인스턴스 변수의 (origin) 값을 스택 맨 위로 밀어넣는다. |
1 | 수신자의 두 번째 인스턴스 변수의 (corner) 값을 스택의 맨 위로 밀어넣는다. |
➧ 176 | 선택자가 +인 이항 메시지를 전송한다. |
119 | SmallInteger 2를 스택 맨 위로 밀어넣는다. |
185 | 선택자가 /인 이항 메시지를 전송한다. |
124 | 스택에서 맨 위의 객체를 메시지의 (center) 값으로 리턴한다. |
Receiver | 100@100 corner: 200@200 |
Arguments | |
Temporary Variables | |
Stack | 100@100 200@200 |
인터프리터의 세 번째 주기에서는 바이트코드 전송을 마주치게 된다. 이는 스택으로부터 두 개의 객체를 제거하여 + 선택자가 있는 메시지의 인자와 수신자로 사용한다. 메시지를 전송하는 절차는 상세히 다루지 않겠다. 현재로선 + 메시지의 결과를 스택 맨 위로 밀어넣을 것이란 사실만 이해해도 충분하다. 메시지의 전송은 이후 절에서 설명할 것이다.
Rectangle center에 대한 메서드 | |
0 | 수신자의 첫 번째 인스턴스 변수의 (origin) 값을 스택의 맨 위로 밀어넣는다. |
1 | 수신자의 두 번째 인스턴스 변수의 (corner) 값을 스택의 맨 위로 밀어넣는다. |
176 | 선택자가 +인 이항 메시지를 전송한다. |
➧ 119 | SmallInteger 2를 스택 맨 위로 밀어넣는다. |
185 | 선택자가 /인 이항 메시지를 전송한다. |
124 | 스택에서 맨 위의 객체를 메시지의 (center) 값으로 리턴한다. |
Receiver | 100@100 corner: 200@200 |
Arguments | |
Temporary Variables | |
Stack | 300@300 |
인터프리터의 다음 주기는 상수 2를 스택 맨 위로 밀어넣는다.
Rectangle center에 대한 메서드 | |
0 | 수신자의 첫 번째 인스턴스 변수의 (origin) 값을 스택 맨 위로 밀어넣는다. |
1 | 수신자의 두 번째 인스턴스 변수의 (corner) 값을 스택의 맨 위로 밀어넣는다. |
176 | 선택자가 +인 이항 메시지를 전송한다. |
119 | SmallInteger 2를 스택 맨 위로 밀어넣는다. |
➧ 185 | 선택자가 /인 이항 메시지를 전송한다. |
124 | 스택에서 맨 위의 객체를 메시지의 (center) 값으로 리턴한다. |
Receiver | 100@100 corner: 200@200 |
Arguments | |
Temporary Variables | |
Stack | 2 |
인터프리터의 다음 주기는 또 다른 메시지를 전송하는데, 그 결과 스택에서 인자와 수신자를 교체한다.
Rectangle center에 대한 메서드 | |
0 | 수신자의 첫 번째 인스턴스 변수의 (origin) 값을 스택의 맨 위로 밀어넣는다. |
1 | 수신자의 두 번째 인스턴스 변수의 (corner) 값을 스택의 맨 위로 밀어넣는다. |
176 | 선택자가 +인 이항 메시지를 전송한다. |
119 | SmallInteger 2를 스택 맨 위로 밀어넣는다. |
185 | 선택자가 /인 이항 메시지를 전송한다. |
➧ 124 | 스택에서 맨 위의 객체를 메시지의 (center) 값으로 리턴한다. |
Receiver | 100@100 corner: 200@200 |
Arguments | |
Temporary Variables | |
Stack | 150@150 |
마지막 바이트코드는 center 메시지로 결과를 리턴한다. 결과는 스택에서 (150@150) 찾을 수 있다. 이 시점에서 return 바이트코드는 다른 스택 위에 결과를 밀어넣는 과정을 수반하는 것이 분명해진다. 메시지에 값을 리턴하는 것과 관련된 상세한 내용은 메시지의 전송을 설명한 후에 설명하겠다.
컨텍스트(Contexts)
Push, store, jump 바이트코드는 인터프리터 상태에 아주 작은 변화만 필요로 한다. 객체는 스택으로 이동하거나 스택으로부터 제거가 가능하며, 명령 포인터는 항상 변경되지만, 상태 대부분은 동일하게 유지된다. Send와 return 바이트코드는 인터프리터 상태에 좀 더 큰 변화를 요구할지도 모른다. 메시지가 전송되면 이러한 새로운 메시지에 대한 응답으로 다른 CompiledMethod를 실행하기 위해서는 인터프리터 상태의 5개 부분 모두 변경되어야 할 것이다. 인터프리터의 기존 상태는 기억되어야 하는데, send 이후의 바이트코드는 메시지의 값이 리턴된 후에 실행되어야 하기 때문이다.
인터프리터는 "contexts"(컨텍스트라고 불리는 객체에 그 상태를 저장한다. 시스템에는 언제든 다수의 컨텍스트가 존재할 것이다. 인터프리터의 현재 상태를 나타내는 컨텍스트를 "active context"(활성 컨텍스트)라고 부른다. 활성 컨텍스트의 CompiledMethod에서 send 바이트코드가 새로운 CompiledMethod의 실행을 요구할 경우, 활성 컨텍스트가 "suspended"(보류)되고 새로운 컨텍스트가 생성되어 활성화된다. 보류된 컨텍스트는 다시 활성화될 때까지 원래 CompiledMethod와 연관된 상태를 유지한다. 컨텍스트는 그것이 보류시킨 컨텍스트를 기억해야만 결과가 리턴되었을 때 보류된 컨텍스트를 재개할 수 있다. 보류된 컨텍스트는 새로운 컨텍스트의 "sender"(전송자)라고 부른다.
마지막 절에서 인터프리터의 상태를 표시하는 데 사용된 형태를 이용해 컨텍스트 또한 표시할 것이다. 활성 컨텍스트는 최상단 구분문자에 Active란 단어로 표시할 것이다. 보류된 컨텍스트는 Suspended라고 표시될 것이다. 100@100 corner: 200@200을 수신자로 가진 Rectangle rightCenter에 대해 CompiledMethod의 실행을 나타내는 컨텍스트를 예로 들어보자. Rectangle rightCenter에 대한 소스 메서드는 다음이 된다.
rightCenter
↑ self right @ self center y
첫 번째 바이트코드의 실행 이후 인터프리터의 상태는 다음과 같다. 전송자는 시스템 내 다른 컨텍스트에 해당한다.
활성(Active) | |
Rectangle rightCenter에 대한 메서드 | |
112 | 수신자를 (self) 스택 맨 위로 밀어넣는다. |
➧ 208 | 첫 번째 리터럴에서 (right) 선택자가 있는 단항 메시지를 전송한다. |
112 | 수신자를 (self) 스택 맨 위로 밀어넣는다. |
209 | 두 번째 리터럴에서 (center) 선택자가 있는 단항 메시지를 전송한다. |
207 | 선택자가 y인 단항 메시지를 전송한다. |
187 | 선택자가 @인 단항 메시지를 전송한다. |
124 | 스택에서 맨 위의 객체를 메시지의 (rightCenter) 값으로 리턴한다. |
literal frame
#right
#center
| |
수신자(Receiver) | 100@100 corner: 200@200 |
인자(Arguments) | |
임시변수(Temporary Variables) | |
스택(Stack) | 100@100 corner: 200@200 |
전송자(Sender) ⇩ |
다음 바이트코드가 실행되고 나면 해당 컨텍스트는 보류될 것이다. 첫 번째 바이트코드가 밀어넣은 객체는 활성화된 새로운 컨텍스트의 수신자로 사용되기 위해 제거되었다. 새로운 활성 컨텍스트는 보류된 컨텍스트 위에 표시된다.
활성화(Active) | |
Rectangle right에 대한 메서드 | |
➧ 1 | 수신자의 두 번째 인스턴스 변수의 (corner) 값을 스택의 맨 위로 밀어넣는다. |
206 | 선택자가 x인 단항 메시지를 전송한다. |
124 | 스택에서 맨 위의 객체를 메시지의 (right) 값으로 리턴한다. |
수신자(Receiver) | 100@100 corner: 200@200 |
인자(Arguments) | |
임시변수(Temporary Variables) | |
스택(Stack) | 100@100 corner: 200@200 |
전송자(Sender) ⇩ |
보류됨(Suspended) | |
Rectangle rightCenter에 대한 메서드 | |
112 | 수신자를 (self) 스택 맨 위로 밀어넣는다. |
208 | 첫 번째 리터럴에서 (right) 선택자가 있는 단항 메시지를 전송한다. |
➧ 112 | 수신자를 (self) 스택 맨 위로 밀어넣는다. |
209 | 두 번째 리터럴에서 (center) 선택자가 있는 단항 메시지를 전송한다. |
207 | 선택자가 y인 단항 메시지를 전송한다. |
187 | 선택자가 @인 단항 메시지를 전송한다. |
124 | 스택에서 맨 위의 객체를 메시지의 (rightCenter) 값으로 리턴한다. |
literal frame
#right
#center
| |
수신자(Receiver) | 100@100 corner: 200@200 |
인자(Arguments) | |
임시변수(Temporary Variables) | |
스택(Stack) | 100@100 corner: 200@200 |
전송자(Sender) ⇩ |
인터프리터의 다음 주기가 이전 컨텍스트 대신 새로운 컨텍스트를 앞선다.
활성(Active) | |
Rectangle right에 대한 메서드 | |
1 | 수신자의 두 번째 인스턴스 변수의 (corner) 값을 스택의 맨 위로 밀어넣는다. |
➧ 206 | 선택자가 x인 단항 메시지를 전송한다. |
124 | 스택에서 맨 위의 객체를 메시지의 (right) 값으로 리턴한다. |
수신자(Receiver) | 100@100 corner: 200@200 |
인자(Arguments) | |
임시변수(Temporary Variables) | |
스택(Stack) | 200@200 |
전송자(Sender) ⇩ |
Suspended | |
Rectangle rightCenter에 대한 메서드 | |
112 | 수신자를 (self) 스택 맨 위로 밀어넣는다. |
208 | 첫 번째 리터럴에서 (right) 선택자가 있는 단항 메시지를 전송한다. |
➧ 112 | 수신자를 (self) 스택 맨 위로 밀어넣는다. |
209 | 두 번째 리터럴에서 (center) 선택자가 있는 단항 메시지를 전송한다. |
207 | 선택자가 y인 단항 메시지를 전송한다. |
187 | 선택자가 @인 단항 메시지를 전송한다. |
124 | 스택에서 맨 위의 객체를 메시지의 (rightCenter) 값으로 리턴한다. |
literal frame
#right
#center
| |
수신자(Receiver) | 100@100 corner: 200@200 |
인자(Arguments) | |
임시변수(Temporary Variables) | |
스택(Stack) | |
전송자(Sender) ⇩ |
다음 주기에서는 또 다른 메시지가 전송되어 어쩌면 또 다른 컨텍스트가 생성할지도 모른다. 이러한 새로운 메시지(x)의 응답을 따르는 대신 이 컨텍스트가 (right로) 값을 리턴하는 시점으로 건너뛸 것이다. x의 결과가 리턴되면 새로운 컨텍스트는 아래와 같은 모습을 한다:
활성(Active) | |
Rectangle right에 대한 메서드 | |
1 | 수신자의 두 번째 인스턴스 변수의 (corner) 값을 스택의 맨 위로 밀어넣는다. |
206 | 선택자가 x인 단항 메시지를 전송한다. |
➧ 124 | 스택에서 맨 위의 객체를 메시지의 (right) 값으로 리턴한다. |
수신자(Receiver) | 100@100 corner: 200@200 |
인자(Arguments) | |
임시변수(Temporary Variables) | |
스택(Stack) | 200 |
전송자(Sender) ⇩ |
보류됨(Suspended) | |
Rectangle rightCenter에 대한 메서드 | |
112 | 수신자를 (self) 스택 맨 위로 밀어넣는다. |
208 | 첫 번째 리터럴에서 (right) 선택자가 있는 단항 메시지를 전송한다. |
➧ 112 | 수신자를 (self) 스택 맨 위로 밀어넣는다. |
209 | 두 번째 리터럴에서 (center) 선택자가 있는 단항 메시지를 전송한다. |
207 | 선택자가 y인 단항 메시지를 전송한다. |
187 | 선택자가 @인 단항 메시지를 전송한다. |
124 | 스택에서 맨 위의 객체를 메시지의 (rightCenter) 값으로 리턴한다. |
literal frame
#right
#center
| |
수신자(Receiver) | 100@100 corner: 200@200 |
인자(Arguments) | |
임시변수(Temporary Variables) | |
스택(Stack) | |
전송자(Sender) ⇩ |
다음 바이트코드는 활성 컨텍스트의 스택 (200) 맨 위에 있는 값을 컨텍스트를 (right) 생싱시킨 메시지의 값으로 리턴한다. 활성 컨텍스트의 전송자는 다시 활성 컨텍스트가 되고, 리턴된 값은 스택으로 밀어넣어진다.
활성(Active) | |
Rectangle rightCenter에 대한 메서드 | |
112 | 수신자를 (self) 스택 맨 위로 밀어넣는다. |
208 | 첫 번째 리터럴에서 (right) 선택자가 있는 단항 메시지를 전송한다. |
➧ 112 | 수신자를 (self) 스택 맨 위로 밀어넣는다. |
209 | 두 번째 리터럴에서 (center) 선택자가 있는 단항 메시지를 전송한다. |
207 | 선택자가 y인 단항 메시지를 전송한다. |
187 | 선택자가 @인 단항 메시지를 전송한다. |
124 | 스택에서 맨 위의 객체를 메시지의 (rightCenter) 값으로 리턴한다. |
literal frame
#right
#center
| |
수신자(Receiver) | 100@100 corner: 200@200 |
인자(Arguments) | |
임시변수(Temporary Variables) | |
스택(Stack) | 200 |
전송자(Sender) ⇩ |
블록 컨텍스트(Block Contexts)
마지막 절에 설명된 컨텍스트는 시스템에서 MethodContext의 인스턴스에 의해 표현된다. MethodContext는 메시지에 대한 응답으로 실행되는 CompiledMethod를 나타낸다. 시스템에서 또 다른 컨텍스트 유형이 있는데, 이는 BlockContext의 인스턴스에 의해 표현된다. BlockContext는 최적화된 제어 구조체에 속하지 않는 소스 메서드 내 블록을 나타낸다. 최적화된 제어 구조체의 컴파일은 앞절에서 설명한 jump 바이트코드에서 찾을 수 있다. 비최적화된 제어 구조체로부터 컴파일된 바이트코드는 Collection 내에 아래의 가상(hypothetical) 메서드로 설명된다. 이러한 메서드는 수신자의 요소의 클래스로 된 컬렉션을 리턴한다.
classes
↑self collect: [ :element | element class]
활성(Active) | |
컬렉션 클래스는 1개의 임시 변수가 필요 | |
112 | 수신자를 (self) 스택 맨 위로 밀어넣는다. |
137 | 활성 컨텍스트를 (thisContext) 스택 맨 위로 밀어넣는다. |
118 | SmallInteger 1을 스택 맨 위로 밀어넣는다. |
200 | 선택자가 blockCopy: 인 단일 인자 메시지를 전송한다. |
164,4. | 다음 4 바이트 주위로 건너뛴다. |
104 | 스택에서 맨 위의 객체를 꺼내 첫 번째 임시 프레임 위치에 (element) 저장한다. |
16 | 첫 번째 임시 프레임 위치의 (element) 내용을 스택 맨 위로 밀어넣는다. |
199 | 선택자가 class인 단항 메시지를 전송한다. |
125 | 스택의 맨 위에 객체를 블록의 값으로 리턴한다. |
224 | 첫 번째 리터럴 프레임 위치에서 (collect:) 선택자가 있는 단일 인자 메시지를 전송한다. |
124 | 스택에서 맨 위의 객체를 메시지의 (classes) 값으로 리턴한다. |
literal frame
#collect:
|
새로운 BlockContext는 활성 컨텍스트로 blockCopy: 메시지를 전송하여 생성된다. 활성 컨텍스트를 밀어넣는 바이트코드는 나머지 push 바이트코드에 대한 설명과 함께 다루지 않을 것인데, 컨텍스트의 함수는 이 시점에서 설명하지 않을 것이기 때문이다. blockCopy: 에 대한 인자는 (이번 예제에서는 1에 해당) 블록이 필요로 하는 블록 인자의 개수를 나타낸다. BlockContext는 그것을 생성한 활성 컨텍스트의 상태 대부분을 공유한다. 수신자, 인자, 임시 변수, CompiledMethod, 전송자는 모두 동일하다. BlockContext는 고유의 명령 포인터와 스택을 가진다. blockCopy: 메시지로부터 리턴되면 새로 생성된 BlockContext가 활성 컨텍스트의 스택 위에 위치하고 다음 명령은 블록의 액션을 설명하는 바이트코드 주위로 건너뛴다. 활성 컨텍스트는 이러한 jump 이후 바이트코드를 가리키는 초기 명령 포인터를 BlockContext에게 제공하였다. 컴파일러는 blockCopy: 이후 항상 확장된 (2 바이트) jump를 사용하여 BlockContext의 초기 명령 포인트가 blockCopy: 메시지를 수신할 때 활성 컨텍스트의 명령 포인트보다 항상 2만큼 크도록 확보한다.
Collection classes에 대한 메서드는 BlockContext를 생성하지만 그 바이트코드를 실행하지는 않는다. 컬렉션이 collect: 메시지를 수신하면 컬렉션의 요소를 인자로 하여 value: 메시지를 BlockContext에 반복하여 전송한다. BlockContext는 활성 컨텍스트가 되어 value: 메시지에 응답하는데, 이는 인터프리터가 바이트코드를 실행하는 결과를 야기한다. BlockContext가 활성화되기 전에 value: 에 대한 인자가 BlockContext의 스택 위로 들어간다. BlockContext가 실행한 첫 번째 바이트코드는 블록 인자에 사용된 임시 변수에 이 값을 저장한다.
BlockContext는 두 가지 방식으로 값을 리턴할 수 있다. 블록 내 바이트코드가 실행된 후 스택에서 마지막 값이 메시지 value 또는 value: 의 값으로 리턴된다. 블록은 BlockContext를 생성시킨 CompiledMethod를 호출하게 된 메시지로 값을 리턴하기도 한다. 이는 일반 return 바이트코드를 이용해 이루어진다. Collection containsInstanceOf: 에 대한 가상 메서드는 BlockContext로부터 두 가지 return 타입을 모두 사용한다.
containsInstanceOf: aClass
self do: [ :element | (element isKindOf: aClass) ifTrue: [↑true]].
↑false
Collection containsInstanceOf:은 1개의 임시 변수가 필요 | |
112 | 수신자를 (self) 스택 맨 위로 밀어넣는다. |
137 | 활성 컨텍스트를 (thisContext) 스택 맨 위로 밀어넣는다. |
118 | SmallInteger 1을 스택 맨 위로 밀어넣는다. |
200 | 선택자가 blockCopy: 인 단일 인자 메시지를 전송한다. |
164,8 | 다음 8 바이트 주변으로 건너뛴다. |
105 | 스택에서 맨 위의 객체를 꺼내 두 번째 임시 프레임 위치로 (element) 저장한다. |
17 | 두 번째 임시 프레임 위치의 (element) 내용을 스택 맨 위로 밀어넣는다. |
16 | 첫 번째 임시 프레임 위치의 (aClass) 내용을 스택 맨 위로 밀어넣는다. |
224 | 첫 번째 리터럴 프레임 위치에서 (isKindOf:) 선택자가 있는 단일 인자 메시지를 전송한다. |
152 | 스택에서 맨 위의 객체를 꺼내 false일 경우 1 바이트 주위로 건너뛴다. |
121 | 메시지의 (containsInstanceOf:) 값으로 true를 리턴한다. |
115 | 스택 맨 위로 nil을 밀어넣는다. |
125 | 스택에서 맨 위의 객체를 블록의 값으로 리턴한다. |
203 | 선택자가 do: 인 단일 인자 메시지를 전송한다. |
135 | 스택에서 맨 위의 객체를 꺼낸다. |
122 | 메시지의 (containsInstanceOf:) 값으로 false를 리턴한다. |
literal frame
#isKindOf:
|
메시지(Messages)
Send 바이트코드를 마주치면 인터프리터는 다음과 같이 메시지가 나타내는 CompiledMethod를 찾는다.
- 메시지 수신자를 찾는다. 수신자는 스택에서 인자 아래에 있다. 인자의 개수는 send 바이트코드에 표시된다.
- 메시지 사전으로 접근한다. 원본 메시지 사전은 수신자의 클래스에서 찾을 수 있다.
- 메시지 사전에서 메시지 선택자를 검색한다. 선택자는 send 바이트코드에서 표시된다.
- 선택자가 발견될 경우, 연관된 CompiledMethod가 메시지에 대한 응답을 설명한다.
- 선택자가 발견되지 않을 경우, 새로운 메시지 사전을 검색해야 한다 (단계 3으로 돌아간다). 메시지 사전을 마지막으로 검색한 클래스의 슈퍼클래스에서 새로운 메시지 사전을 찾을 것이다. 이 주기는 슈퍼클래스 사슬까지 거슬러 올라가 여러 번 반복될 수 있다.
선택자를 수신자의 클래스나 그 슈퍼클래스에서 찾을 수 없는 경우 오류가 보고되고, send 이후의 바이트코드 실행은 보류된다.
❏ Superclass Sends. "Supercsends"라고 불리는 send 바이트코드의 variation은 메시지와 연관된 CompiledMethod를 찾기 위해 약간 다른 알고리즘을 사용한다. 두 번째 단계, 즉 검색할 원본 메시지 사전을 명시하는 단계를 제외한 모든 것은 동일하다. Super-send를 마주치면 다음과 같이 두 번째 단계가 대체된다.
- 2. 메시지 사전으로 접근한다. 원본 메시지 사전은 현재 실행 중인 CompiledMethod가 발견된 클래스의 슈퍼클래스에서 찾을 수 있다.
Super-send 바이트코드는 super가 소스 메서드에서 메시지의 수신자로 사용될 때 사용된다. 수신자를 밀어 넣는 데 사용된 바이트코드는 self가 사용된 것과 동일하겠지만 선택자를 설명하는 데에 super-send 바이트코드가 사용될 것이다.
Super-send의 사용 예제로, shade라는 인스턴스 변수를 추가하는 ShadedRectangle이라 불리는 Rectangle의 서브클래스가 있다고 가정해보자. Rectangle은 새로운 ShadedRectangle을 생성함으로써 shade: 메시지에 응답할 수 있다. ShadedRectangle은 intersect: 메시지에 대한 새로운 메서드를 제공하여 Rectangle 대신 ShadedRectangle을 리턴한다. 이러한 메서드는 교차점을 실제로 계산하는 자체 기능으로 접근하기 위해 super를 사용해야 한다.
intersect: aRectangle
↑(super intersect: aRectangle)
shade: shade
활성(Active) | |
ShadedRectangle intersect: | |
112 | 수신자를 (self) 스택 맨 위로 밀어넣는다. |
16 | 첫 번째 임시 프레임 위치의 (인자 aRectangle) 내용을 스택 맨 위로 밀어넣는다. |
133,33 | 두 번째 리터럴 프레임 위치에서 (intersect:) 선택자가 있는 단일 인자 메시지를 super로 전송한다. |
2 | 수신자의 세 번째 인스턴스 변수의 (shade) 값을 스택 맨 위로 밀어넣는다. |
224 | 첫 번째 리터럴 프레임 위치에서 (shade:) 선택자가 있는 단일 인자 메시지를 전송한다. |
124 | 스택에서 맨 위의 객체를 메시지의 (intersect:) 값으로 리턴한다. |
literal frame
#shade:
#intersect:
Association: #ShadedRectangle ➛ ShadedRectangle
|
Super-send에 대한 응답으로 검색한 초기 클래스는 super-send를 포함하는 CompiledMethod가 본래 수신자의 클래스에서 발견된 경우에만 수신자의 클래스의 슈퍼클래스가 될 것임을 주목할 필요가 있다. CompiledMethod가 원래 수신자의 클래스의 슈퍼클래스에서 발견되었다면 검색은 그 클래스의 슈퍼클래스에서 시작할 것이다. 인터프리터의 상태는 그것이 각 CompiledMethod를 발견한 클래스를 포함하지 않고, 그러한 정보는 CompiledMethod 자체에 포함된다. Super-send 바이트코드를 포함하는 모든 CompiledMethod는 그것이 발견된 메시지 사전의 클래스를 참조한다. 그러한 CompiledMethods의 리터럴 프레임에서 마지막 엔트리는 클래스를 참조하는 연관을 포함한다.
프리미티브 메서드(Primitive Methods)
CompiledMethod를 찾고 난 후 인터프리터의 액션은 CompiledMethod가 "primitive method"(프리미티브 메서드)의 메시지 응답 가능성 여부를 나타내는지에 따라 좌우된다. 프리미티브 메서드가 전혀 표시되지 않으면 새로운 MethodContext가 생성되고 앞절에서 설명된 바와 같이 활성화로 만들어진다. 프리미티브 메서드가 CompiledMethod에 표시되면 인터프리터는 실제로 바이트코드를 실행하지 않고 메시지에 응답할 수 있을 것이다. 예를 들어 프리미티브 메서드 중 하나는 SmallInteger의 인스턴스에 대한 + 메시지와 연관되어 있다.
+ addend
<primitive: 1>
↑super + addend
primitive #1과 연관된 SmallInteger + | |
112 | 수신자를 (self) 스택 맨 위로 밀어넣는다. |
16 | 첫 번째 임시 프레임 위치의 (인자 addend) 내용을 스택 맨 위로 밀어넣는다. |
133,32 | 첫 번째 리터럴 프레임 위치에서 (+) 선택자가 있는 단일 인자 메시지를 super로 전송한다. |
124 | 스택에서 맨 위의 객체를 메시지의 (+) 값으로 리턴한다. |
literal frame
#+
|
CompiledMethod에 대한 프리미티브 메서드가 표시된다 하더라도 인터프리터가 성공적으로 응답하지 못할지도 모른다. 가령, + 메시지의 인자는 또 다른 SmallInteger의 인스턴스가 아닐지도 모르고, 총계(sum)를 SmallInteger로 표현할 수 없을지도 모른다. 인터프리터가 어떤 이유 때문에 프리미티브를 실행할 수 없을 경우, 프리미티브는 "fail"(실패)한다고 말한다. 프리미티브가 실패하면 프리미티브 메서드가 나타나지 않은 것처럼 CompiledMethod 내의 바이트코드가 실행된다. SmallInteger + 에 대한 메서드는 프리미티브가 실패할 경우 슈퍼클래스 (Integer) 내의 + 메서드가 사용될 것임을 나타낸다.
시스템에는 네 가지 유형의 연산을 실행하는 프리미티브 메서드가 약 100개 정도 있다. 모든 프리미티브의 정확한 기능은 제 29장에 설명되어 있다.
- 산술
- 저장공간 관리(Storage management)
- 제어
- 입-출력
객체 메모리(The Object Memory)
객체 메모리는 인터프리터에게 Smalltalk-80 가상 이미지를 구성하는 객체에 대한 인터페이스를 제공한다. 각 객체는 "object pointer"(객체 포인터)라고 불리는 유일한 식별자와 연관되어 있다. 객체 메모리와 인터프리터는 객체 포인터를 이용해 객체에 관해 통신한다. 객체 포인터의 크기는 Smalltalk-80 시스템이 포함할 수 있는 객체의 최대 개수를 결정한다. 이러한 숫자가 언어와 관련해 고정된 것은 아니지만 본 저서에 설명된 구현에서는 16-bit 객체 포인터를 사용하여 65536개 객체의 참조를 허용한다. 객체 참조의 수가 더 큰 Smalltalk-80 시스템을 구현하기 위해서는 가상 머신 특정적인 부분들을 변경해야 한다. 하지만 그와 관련된 부분들은 본 서적의 주제 범위를 벗어난다.
객체 메모리는 각 객체 포인터를 다른 객체 포인터의 집합과 연관시킨다. 모든 객체 포인터는 클래스에 대한 객체 포인터와 연관된다. 객체에 인스턴스 변수가 있을 경우 그 객체 포인터 또한 그러한 값의 객체 포인터와 연관된다. 각 인스턴스 변수는 zero-relative 정수 색인에 의해 참조된다. 인스턴스 변수의 값은 변경될 수 있지만 객체와 연관된 클래스는 변경될 수 없다. 객체 메모리는 인터프리터에게 다음과 같은 다섯 가지 기본 기능을 제공한다.
- 1. 객체의 인스턴스 변수의 값으로 접근한다. 인스턴스의 객체 포인터와 인스턴스 변수의 색인이 제공되어야 한다. 인스턴스 변수의 값에 대한 객체 포인터가 리턴된다.
- 2. 객체의 인스턴스 변수의 값을 변경한다. 인스턴스의 객체 포인터와 인스턴스 변수의 색인이 제공되어야 한다. 새 값에 대한 객체 포인터 또한 제공되어야 한다.
- 3. 객체의 클래스로 접근한다. 인스턴스의 객체 포인터가 제공되어야 한다. 인스턴스의 클래스에 대한 객체 포인터가 리턴된다.
- 4. 새로운 객체를 생성한다. 새로운 객체의 클래스에 대한 객체 포인터와 객체가 가져야 하는 인스턴스 변수의 개수가 제공되어야 한다.
- 5. 객체가 가진 인스턴스 변수의 개수를 찾는다. 객체의 포인터가 제공되어야 한다. 인스턴스 변수의 개수가 리턴된다.
더 이상 사용되지 않는 객체를 제거하기 위한 객체 메모리의 명시적인 기능은 없는데, 이러한 객체는 자동으로 회수(reclaim)되기 때문이다. 객체는 다른 객체로부터 그것을 향하는 객체 포인터가 없을 때 회수된다. 이러한 회수는 참조 계수 또는 쓰레기 수집을 통해 이루어진다.
객체 메모리에는 수치적 정보의 효율적인 표현을 제공하는 추가 기능이 두 가지 있다. 첫 번째는 SmallInteger 클래스의 인스턴스에 대한 특정 객체 포인터를 따로 확보하는 기능이다. 두 번째는 객체가 객체 포인터 대신 정수값을 포함하도록 허용하는 것이다.
❏ 작은 정수의 표현. SmallInteger 클래스의 인스턴스는 -16384부터 16383까지 정수를 표현한다. 이러한 인스턴스마다 유일한 객체 포인터가 할당된다. 이러한 객체 포인터는 모두 최하위 비트 위치에는 1을, 최상위 15 비트에는 그 값에 대한 2의 보수 표현을 갖는다. SmallInteger의 인스턴스는 어떠한 인스턴스 저장도 필요로 하지 않는데, 그 클래스와 값 모두 객체 포인터로부터 결정되기 때문이다. 객체 포인터는 SmallInteger 객체 포인터와 수치값을 양방향으로 변환하도록 두 가지 추가 기능을 제공한다.
- 6. SmallInteger가 표현한 수치값을 찾는다. SmallInteger의 객체 포인터가 제공되어야 한다. 2의 보수값이 리턴된다.
- 7. 수치값을 나타내는 SmallInteger를 찾는다. 2의 보수값이 제공되어야 한다. SmallInteger 객체 포인터가 리턴된다.
SmallIntegers에 대한 이러한 표현은 시스템 내 다른 클래스에 32768개의 인스턴스가 존재할 수 있음을 암시한다. 또한 SmallInteger의 인스턴스에서는 상등성(=)과 동등성(==)이 동일할 것임을 의미하기도 한다. -16384부터 16383까지 범위를 벗어나는 정수는 LargePositiveInteger 또는 LargeNegativeInteger 클래스의 인스턴스로 표현된다. 상등성과 동등성이 서로 다르도록 여러 개의 인스턴스가 같은 값을 표현하는 경우도 있다.
❏ 정수값의 컬렉션. 정수의 컬렉션을 표현하는 객체를 위해 또 다른 특수 표현이 포함되어 있다. 컬렉션의 내용을 표현하는 SmallIntegers의 객체 포인터를 저장하는 대신 실제 수치값이 저장된다. 이러한 특수 컬렉션 내 값은 양수로 제한된다. 컬렉션에는 두 가지 종류가 있는데, 하나는 값을 256 미만으로 제한하고, 나머지는 65536 미만으로 제한한다. 객체 메모리는 이번 절에 열거된 기능 중 첫 5개와 동일한 기능을 제공하지만, 객체 포인터가 아니라 수치값을 내용으로 가진 객체에 한한다.
Smalltalk-80 프로그래머는 객체 포인터를 포함하는 객체와 정수값을 포함하는 객체 간 차이를 결코 확인할 수 없다. 메시지를 전송하여 이러한 특수 수치형 컬렉션들 중 하나로 접근하면 값을 나타내는 객체의 객체 포인터가 리턴된다. 이러한 특수 컬렉션의 특징은 적절한 범위 내에서 정수를 표현하지 않는 객체의 저장을 거부할 수도 있다는 점에서 뚜렷하게 나타난다.
하드웨어(The Hardware)
불필요한 하드웨어 의존성을 피하기 위해 Smalltalk-80 구현은 가상 머신으로 설명되어왔다. 본래 하드웨어는 인터프리터와 객체 메모리를 시뮬레이트하는 기계어 루틴과 가상 이미지를 저장하기에 충분한 메모리 및 하드웨어를 포함할 것으로 가정한다. 가상 이미지의 현재 크기는 메모리에서 최소 절반 메가바이트를 필요로 한다.
프로세서의 크기와 메모리의 조직은 사실상 가상 머신 사양의 제약을 받지 않는다. 객체 포인터는 16 비트이므로 가장 편리한 배열은 16-bit 프로세서와 16-bit 워드의 메모리가 되겠다. 여느 시스템의 프로세스나 메모리와 마찬가지로 빠를수록 좋다.
그 외 하드웨어 요구사항은 가상 이미지가 의존하는 프리미티브에서 요구한다. 이러한 입-출력 장치와 시계를 아래 열거하겠다.
- 표시되는 비트맵을 객체 메모리에 위치시킬 수 있다면 가장 편리하겠지만 무조건 필요한 것은 아니다.
- 지시 장치.
- 지시 장치와 연관된 세 개의 버튼. 장치에 물리적으로 위치되어 있을 경우 가장 편리하다.
- 디코딩된 ASCII 또는 언더코딩된 ALTO로 된 키보드.
- 디스크. 표준 Smalltalk-80 가상 이미지는 실제 사용되는 디스크에 맞춰야 하는 skeleton disk system만 포함한다.
- 밀리초 타이머.
- 1초 해상도의 실시간(real time) 시계.