Smalltalk80LanguageImplementationKor:Chapter 28
- 제 28 장 인터프리터의 형식적 명세
인터프리터의 형식적 명세
Smalltalk-80 인터프리터의 메인 루프는 CompiledMethod로부터 바이트코드를 순차적으로 인출하여 바이트코드가 나타내는 연산을 실행하는 루틴으로 전송한다. fetchByte 루틴은 활성 컨텍스트의 명령 포인터가 나타내는 바이트를 인출하여 명령 포인터를 증가시킨다.
fetchByte
| byte |
byte ← memory fetchByte: instructionPointer
ofObject: method.
instructionPointer ← instructionPointer + 1.
↑byte
프로세스 전환(process switch)은 바이트코드 간에서만 허용되므로 인터프리터의 메인 루프에서는 필요 시 프로세스를 전환하는 루틴을 호출하는 것이 첫 번째 액션이 된다. checkProcessSwitch 루틴은 다음 장에서 프리미티브 루틴을 스케줄링하는 프로세스와 함께 설명할 것이다. 프로세스 전환을 검사한 후에는 (아마 새로운 프로세스로부터) 바이트코드가 인출되고 적절한 루틴으로 전송될 것이다.
interpret
[true] whileTrue: [self cycle]
cycle
self checkProcessSwitch.
currentBytecode ← self fetchByte.
self dispatchOnThisBytecode
595 페이지에 실린 표에 Smalltalk-80 바이트코드가 열거되어 있다. 바이트코드는 비슷한 기능을 가진 범위에서 열거하였다. 가령 첫 번째 범위는 0부터 15까지 바이트코드를 포함하고 그 엔트리는 아래에 표시한다.
0-15 0000iiii Push Receiver Variable #iiii
각 바이트코드의 범위에는 비트 패턴과 바이트코드의 기능에 관한 주석이 함께 열거되어 있다. 비트 패턴은 각 범위 내 바이트코드의 이항 표현을 표시한다. 해당 범위에 모든 바이트코드에 대해 동일한 값을 가진 비트 위치에 0과 1이 사용되었다. 0부터 15까지 모든 숫자는 상위 비트에 네 개의 0을 갖기 때문에 이러한 비트는 0000과 같은 방식으로 표시된다. 범위 내에서 다양한 값을 가진 비트 위치에는 소문자를 이용했다. 각 문자의 값은 0이나 1로 되어 있다. 패턴에 사용된 문자는 주석에 포함되어 범위 내 특정 바이트코드에서 비트의 값을 참조한다. 첫 번째 바이트코드의 범위에 대한 주석이 나타내는 바에 따르면 바이트코드의 하위 4개 비트는 스택으로 밀어넣을 수신자의 변수들 중 하나의 색인을 명시한다.
비트 패턴에서 변수 비트 또한 때로는 주석에 포함된 리스트에 zero-relative 색인으로 사용된다. 가령, 아래의 엔트리는
120-123 011110ii Return (receiver, true, false, nil) [ii] From Message
바이트코드 120은 수신자를, 바이트코드 121은 true를, 바이트코드 122는 false를, 바이트코드 123은 nil을 리턴함을 명시한다.
확장을 취하는 바이트코드에 대한 엔트리는 1 비트 패턴 이상을 포함할 것이다. 다음을 예로 들 수 있겠다.
131 10000011 Send Literal Selector #kkkkk With jjj Arguments
jjjkkkkk
바이트코드에는 기본적으로 네 가지 유형이 있다.
- stack 바이트코드는 객체 메모리와 활성 컨텍스트의 평가 스택 사이에서 객체 포인터를 이동시킨다. 제 26장에 설명된 push 바이트코드와 store 바이트코드가 이에 포함된다.
- jump 바이트코드는 활성 컨텍스트의 명령 포인터를 변경시킨다.
- send 바이트코드는 CompiledMethods 또는 프리미티브 루틴을 호출한다.
- return 바이트코드는 CompiledMethods의 실행을 종료한다.
한 가지 유형의 바이트코드가 모두 근접한 것은 아니므로 main dispatch는 네 가지 루틴 중 (stackBytecode, jumpBytecode, sendBytecode, returnBytecode) 하나를 호출하는 branch를 7개 갖고 있다. 이러한 네 가지 루틴은 다음 네 개의 절에 걸쳐 설명하겠다.
dispatchOnThisBytecode
(currentBytecode between: 0 and: 119) ifTrue: [↑self stackBytecode].
(currentBytecode between: 120 and: 127) ifTrue: [↑self returnBytecode].
(currentBytecode between: 128 and: 130) ifTrue: [↑self stackBytecode].
(currentBytecode between: 131 and: 134) ifTrue: [↑self sendBytecode].
(currentBytecode between: 135 and: 137) ifTrue: [↑self stackBytecode].
(currentBytecode between: 144 and: 175) ifTrue: [↑self jumpBytecode].
(currentBytecode between: 176 and: 255) ifTrue: [↑self sendBytecode]
바이트코드 176-191은 산술 메시지를 참조하며 다음과 같다.
- +,-,<,>,< =, > =, =, ~=, *, /, \\, @, bitShift:, //, bitAnd:, bitOr:
바이트코드 192-207은 특수 메시지를 참조하며 다음과 같다.
- at:*, at:put:*, size*, next*, nextPut:*, atEnd*, = =, class, blockCopy:, value, value:, do:*, new*, new:*, x*, y*
별 모양 기호(*)로 표시된 선택자는 컴파일러 수정으로 변경이 가능하다.
범위 | 비트(Bits) | 기능(Function) |
0-15 | 0000iiii | 수신자 변수 #iiii 를 밀어넣는다. |
16-31 | 0001iiii | 임시 위치 #iiii 를 밀어넣는다. |
32-63 | 001iiiii | 리터럴 상수 #iiiii 를 밀어넣는다. |
64-95 | 010iiiii | 리터럴 변수 #iiiii 를 밀어넣는다. |
96-103 | 01100iii | 수신자 변수 #iii 를 꺼내 저장한다. |
104-111 | 01101iii | 임시 위치 #iii 를 꺼내 저장한다. |
112-119 | 01110iii | (수신자, true, false, nil, -1, 0, 1, 2) [iii] 를 밀어넣는다. |
120-123 | 011110ii | 메시지로부터 (수신자 , true, false, nil) [ii] 를 리턴한다. |
124-125 | 0111110i | (Message, Block) [i] 로부터 스택 맨 위를 리턴한다. |
126-127 | 0111111i | 사용되지 않음(unused). |
128 | 10000000 jjkkkkkk |
(수신자 변수, 임시 위치, 리터럴 상수, 리터럴 변수) [jj] #kkkkkk 를 밀어넣는다. |
129 | 10000001 jjkkkkkk |
(수신자 변수, 임시 위치, Illegal, 리터럴 변수) [jj] #kkkkkk 를 저장한다. |
130 | 10000010 jjkkkkkk |
(수신자 변수, 임시 위치, Illegal, 리터럴 변수) [jj] #kkkkkk 를 꺼내 저장한다. |
131 | 10000011 jjjkkkkk |
jjj 인자와 함께 리터럴 선택자 #kkkkk 를 전송한다. |
132 | 10000100 jjjjjjjj kkkkkkkk |
jjjjjjjj 인자와 함께 리터럴 선택자 #kkkkkkkk 를 전송한다. |
133 | 10000101 jjjkkkkk |
리터럴 선택자 * kkkkk 를 jjj 인자와 함께 슈퍼클래스로 전송한다. |
134 | 10000110 jjjjjjjj kkkkkkkk |
리터럴 선택자 #kkkkkkkk 를 jjjjjjjj 인자와 함께 슈퍼클래스로 전송한다. |
135 | 10000111 | 스택 맨 위를 꺼낸다. |
136 | 10001000 | 스택 맨 위를 복사한다. |
137 | 10001001 | 활성 컨텍스트를 밀어넣는다. |
138-143 | 사용되지 않음(unused) | |
144-151 | 10010iii | iii+1 (예: 1부터 8까지) 를 건너뛴다. |
152-159 | 10011iii | 꺼내서 False iii+1 (예: 1부터 8까지) 로 건너뛴다. |
160-167 | 10100iii jjjjjjjj |
(iii-4)*256+jjjjjjjj 만큼 건너뛴다. |
168-171 | 101010ii jjjjjjjj |
꺼내서 True ii*256+jjjjjjjj 로 건너뛴다. |
172-175 | 101011ii jjjjjjjj |
꺼내서 False ii*256+jjjjjjjj 로 건너뛴다. |
176-191 | 1011iiii | 산술 메시지 #iiii 를 전송한다. |
192-207 | 1100iiii | 특수 메시지 #iiii 를 전송한다. |
208-223 | 1101iiii | 인자가 없는 리터럴 선택자 #iiii 를 전송한다. |
224-239 | 1110iiii | 1개의 인자가 있는 리터럴 선택자 #iiii 를 전송한다. |
240-255 | 1111iiii | 2개의 인자가 있는 리터럴 선택자 #iiii 를 전송한다. |
Smalltalk-80 바이트코드 |
Stack 바이트코드(Stack Bytecodes)
Stack 바이트코드는 모두 활성 컨텍스트의 평가 스택에서 단순한 연산을 실행한다.
- 107 바이트코드는 객체 포인터를 스택 맨 위로 밀어넣는다
- 99 는 상수 객체 포인터를 밀어넣는다
- 7 은 상수 객체 포인터를 밀어넣는다
- 1 은 인터프리터의 활성 컨텍스트 레지스터를 밀어넣는다 (activeContext)
- 18 바이트코드는 스택에서 발견된 객체 포인터를 객체 메모리에 저장한다
- 17 은 스택으로부터 그것을 제거한다
- 1 은 스택에 남겨둔다
- 1 바이트코드는 다른 곳에 저장하지 않고 스택으로부터 객체 포인터를 제거한다
스택을 조작하는 데 사용된 루틴은 앞절에서 컨텍스트를 (push:, popStack, pop:) 다루면서 설명하였다. stackBytecode 루틴은 현재 바이트코드에 적절한 루틴으로 전달한다.
stackBytecode
(currentBytecode between: 0 and: 15) ifTrue: [↑self pushReceiverVariableBytecode].
(currentBytecode between: 16 and: 31) ifTrue: [↑self pushTemporaryVariableBytecode].
(currentBytecode between: 32 and: 63) ifTrue: [↑self pushLiteralConstantBytecode].
(currentBytecode between: 64 and: 95) ifTrue: [↑self pushLiteralVariableBytecode].
(currentBytecode between: 96 and: 103) ifTrue: [↑self storeAndPopReceiverVariableBytecode].
(currentBytecode between: 104 and: 111) ifTrue: [↑self storeAndPopTemporaryVariableBytecode].
currentBytecode = 112 ifTrue: [↑self pushReceiverBytecode].
(currentBytecode between: 113 and: 119) ifTrue: [↑self pushConstantBytecode].
currentBytecode = 128 ifTrue: [↑self extendedPushBytecode].
currentBytecode = 129 ifTrue: [↑self extendedStoreBytecode].
currentBytecode = 130 ifTrue: [↑self extendedStoreAndPopBytecode].
currentBytecode = 135 ifTrue: [↑self popStackBytecode].
currentBytecode = 136 ifTrue: [↑self duplicateTopBytecode].
currentBytecode = 137 ifTrue: [↑self pushActiveContextBytecode]
수신자의 첫 16개 인스턴스 변수와 첫 16개 임시 프레임 위치를 밀어넣는 단일 바이트 명령들이 있다. 임시 프레임은 인자와 임시 변수를 포함함을 상기해보자.
pushReceiverVariableBytecode
| fieldIndex |
fieldIndex ← self extractBits: 12 to: 15
of: currentBytecode.
self pushReceiverVariable: fieldIndex
pushReceiverVariable: fieldIndex
self push: (memory fetchPointer: fieldIndex
ofObject: receiver)
pushTemporaryVariableBytecode
| fieldIndex |
fieldIndex ← self extract Bits: 12 to: 15
of: currentBytecode.
self pushTemporaryVariable: fieldIndex
pushTemporaryVariable: temporaryIndex
self push: (self temporary: temporaryIndex)
또 활성 컨텍스트의 메서드의 리터럴 프레임에서 첫 32개 위치를 참조하는 단일 바이트 명령도 있다. 이러한 위치 중 하나의 내용을 pushLiteralConstantBytecode를 이용해 밀어넣을 수 있다. 이러한 위치 중 한 군데에 저장된 Association의 값 필드의 내용은 pushLiteralVariableBytecode를 이용해 밀어넣을 수 있다.
pushLiteralConstantBytecode
| fieldIndex |
fieldIndex ← self extractBits: 11 to: 15
of: currentBytecode.
self pushLiteralConstant: fieldIndex
pushLiteralConstant: literalIndex
self push: (self literal: literalIndex)
pushLiteralVariableBytecode
| fieldIndex |
fieldIndex ← self extractBits: 11 to: 15
of: currentBytecode.
self pushLiteralVariable: fieldIndex
pushLiteralVariable: literalIndex
| association |
association ← self literal: literalIndex.
self push: (memory fetchPointer: ValueIndex
ofObject: association)
Associations는 이름과 값에 대한 필드가 하나씩 있는 객체이다. 이는 공유 변수를 (전역 변수, 클래스 변수, pool 변수) 구현하는 데 사용된다. 다음 루틴은 Associations의 값 필드를 인출하는 데 사용되는 색인을 초기화한다.
initializeAssociationIndex
ValueIndex ← 1
확장된 push 바이트코드는 위에 설명된 네 가지 연산 중 (수신자 변수, 임시 프레임 위치, 리터럴 상수 또는 리터럴 변수) 어느 것이든 실행할 수 있다. 하지만 접근 가능한 변수가 16 또는 32개로 제한되는 대신 64개의 인스턴스 변수, 임시 위치, 리터럴 상수 또는 리터럴 변수로 접근할 수 있다. 확장된 push 바이트코드 다음에는 바이트가 따라오는데, 이 바이트의 상위 2비트는 어떠한 타입의 push를 실행하는지를 결정하고 하위 2비트는 사용할 오프셋을 결정한다.
extendedPushBytecode
| descriptor variableType variableIndex |
descriptor ← self fetchByte.
variableType ← self extractBits: 8 to: 9
of: descriptor.
variableIndex ← self extractBits: 10 to: 15
of: descriptor.
variableType=0 ifTrue: [↑self pushReceiverVariable: variableIndex].
variableType=1 ifTrue: [↑self pushTemporaryVariable: variableIndex].
variableType=2 ifTrue: [↑self pushLiteralConstant: variableIndex].
variableType=3 ifTrue: [↑self pushLiteralVariable: variableIndex]
pushReceiverBytecode 루틴은 포인터를 활성 컨텍스트의 수신자로 밀어넣는다. 이는 Smalltalk 메서드에서 self 또는 super의 사용에 해당한다.
pushReceiverBytecode
self push: receiver
duplicateTopBytecode 루틴은 객체 포인터의 또 다른 복사본을 스택의 맨 위에 밀어넣는다.
duplicateTopBytecode
↑self push: self stackTop
pushConstantBytecode 루틴은 7개의 상수 객체 포인터 중 하나를 (true, false, nil, -1, 0, 1, 또는 2) 밀어넣는다.
pushConstantBytecode
currentBytecode = 113 ifTrue: [↑self push: TruePointer].
currentBytecode = 114 ifTrue: [↑self push: FalsePointer].
currentBytecode = 115 ifTrue: [↑self push: NilPointer].
currentBytecode = 116 ifTrue: [↑self push: MinusOnePointer].
currentBytecode = 117 ifTrue: [↑self push: ZeroPointer].
currentBytecode = 118 ifTrue: [↑self push: OnePointer].
currentBytecode = 119 ifTrue: [↑self push: TwoPointer]
pushActiveContextBytecode 루틴은 활성 컨텍스트 자체로 포인터를 밀어넣는다. 이는 Smalltalk 메서드에서 thisContext의 사용에 해당한다.
pushActiveContextBytecode
self push: activeContext
Store 바이트코드는 push 바이트코드와 반대 방향으로, 즉 스택의 맨 위에서 수신자의 인스턴스 변수, 임시 프레임 또는 리터럴 프리엠 방향으로 참조를 전달한다. 임시 프레임이나 수신자의 첫 8개 변수로 저장한 후 스택을 꺼내는 단일 바이트 버전이 있다.
storeAndPopReceivervariableBytecode
| variableIndex |
variableIndex ← self extractBits: 13 to: 15
of: currentBytecode.
memory storePointer: variableIndex
ofObject: receiver
withValue: self popStack
storeAndPopTemporaryVariableBytecode
| variableIndex |
variableIndex ← self extractBits: 13 to: 15
of: currentBytecode.
memory storePointer: variableIndex + TempFrameStart
ofObject: homeContext
withValue: self popStack
단일 바이트 버전에 의해 접근이 가능한 것 이외의 변수로 저장하는 작업은 두 개의 확장된 store 바이트코드에 의해 이루어진다. 하나는 저장 후 스택을 꺼내지만 나머지 하나는 꺼내지 않는다. 두 가지 확장된 stores 모두 확장된 push와 동일한 형태로 된 아래의 바이트를 취한다. 하지만 10xxxxxx 형태의 바이트로 확장된 store를 따르는 것은 합리적이지 못한데, 이는 리터럴 상수의 값이 변경됨을 의미하기 때문이다.
extendedStoreAndPopBytecode
self extendedStoreBytecode.
self popStackBytecode
extendedStoreBytecode
| descriptor variableType variableIndex association |
descriptor ← self fetchByte.
variableType ← self extractBits: 8 to: 9
of: descriptor.
variableIndex ← self extractBits: 10 to: 15
of: descriptor.
variableType = O ifTrue:
[↑memory storePointer: variableIndex
ofObject: receiver
withValue: self stackTop].
variableType = 1 ifTrue:
[↑memory storePointer: variableIndex + TempFrameStart
ofObject: homeContext
withValue: self stackTop].
variableType = 2 ifTrue:
[↑self error: 'illegal store'].
variableType = 3 ifTrue:
[association ← self literal: variableIndex.
↑memory storePointer: ValueIndex
ofObject: association
withValue: self stackTop]
마지막 스택 바이트코드는 스택으로부터 객체 포인터를 제거하기만 하고 그 외에 어떤 일도 하지 않는다.
popStackBytecode
self popStack
Jump 바이트코드(Jump Bytecodes)
Jump 바이트코드는 활성 컨텍스트의 명령 포인터를 명시된 양만큼 변경한다. 비조건부 jumps는 명령 포인터를 마주칠 때마다 변경한다. 조건부 jumps는 스택 맨 위의 객체 포인터가 명시된 Boolean 객체일 경우 (true 또는 false) 명령 포인터만 변경한다. 비조건부와 조건부 jumps 모두 짧은 (단일 바이트) 형태와 긴 (2 바이트) 형태를 가진다.
jumpBytecode
(currentBytecode between: 144 and: 151)
ifTrue: [↑self shortUnconditionalJump].
(currentBytecode between: 152 and: 159)
ifTrue: [↑self shortConditionalJump].
(currentBytecode between: 160 and: 167)
ifTrue: [↑self longUnconditionalJump].
(currentBytecode between: 168 and: 175)
ifTrue: [↑self longConditionalJump]
Jump 바이트코드는 실제로 바이트코드 색인을 변경하기 위해 jump: 루틴을 사용한다.
jump: offset
instructionPointer ← instructionPointer + offset
8개의 짧은 비조건부 jumps는 명령 포인터를 1부터 8까지 advance시킨다.
shortUnconditionalJump
| offset |
offset ← self extractBits: 13 to: 15
of: currentBytecode.
self jump: offset + 1
8개의 긴 비조건부 jumps 다음에는 또 다른 바이트가 따라온다. Jump 바이트코드의 하위 3 비트는 명령 포인터로 추가될 11-bit 2에 대한 보수 변위의 상위 3 비트를 제공한다. Jump 이후 바이트는 변위의 하위 8 비트를 제공한다. 따라서 긴 비조건부 jumps는 1023만큼 앞으로, 1024만큼 뒤로 jump할 수 있다.
longUnconditionalJump
| offset |
offset ← self extractBits: 13 to: 15
of: currentBytecode.
self jump: offset - 4 * 256 + self fetchByte
조건부 jumps는 jumpIf:by: 루틴을 이용해 스택의 맨 위를 검사하고 jump를 실행할지 결정한다. 스택의 맨 위는 검사 후에 삭제된다.
jumpIf: condition by: offset
| boolean |
boolean ← self popStack.
boolean = condition
ifTrue: [self jump: offset]
ifFalse: [(boolean = TruePointer) | (boolean = FalsePointer)
ifFalse: [self unPop:1.
self sendMustBeBoolean]]
조건부 jumps는 booleans로 전송되는 컴파일된 메시지 형태에서 사용된다 (예: ifTrue:와 whileFalse:). 조건부 jump 시점에서 스택의 맨 위가 true 또는 false가 아닐 경우 오류가 되는데, boolean을 제외한 객체로 booleans만 이해할 수 있는 메시지가 전송되었기 때문이다. 인터프리터는 doesNotUnderstand: 메시지를 전송하는 대신 mustBeBoolean을 전송한다.
sendMustBeBoolean
self sendSelector: MustBeBooleanSelector
argumentCount: 0
sendSelector:argumentCount: 루틴은 send 바이트코드에 대한 다음 절에 설명되어 있다.
8번의 짧은 조건부 jump는 스택의 맨 위가 false일 경우 명령 포인터를 1부터 8까지 advance한다.
shortConditionalJump
| offset |
offset ← self extractBits: 13 to: 15
of:currentBytecode.
self jumpIf: FalsePointer
by: offset + 1
따라서 짧은 조건부 jump에 가능한 결과는 세 가지가 있다:
- 스택의 맨 위가 false일 경우 jump를 취한다.
- 스택의 맨 위가 true일 경우 jump를 취하지 않는다.
- 스택의 맨 위가 false도 아니고 true도 아닐 경우 mustBeBoolean가 스택으로 전송된다.
스택의 맨 위가 false일 경우 긴 조건부 jumps 중 절반은 jump를 실행하고 나머지는 true일 때 jump를 실행한다. 바이트의 하위 2 비트는 부호가 없는 10-bit 변위에서 상위 2 비트가 된다. Jump 이후의 바이트는 변위의 하위 8 비트를 제공한다. 따라서 긴 조건부 jumps는 1023으로 앞으로 건너뛸 수 있다.
longConditionalJump
| offset |
offset ← self extractBits: 14 to: 15
of: currentBytecode.
offset ← offset * 256 + self fetchByte.
(currentBytecode between: 168 and: 171)
ifTrue: [↑self jumpIf: TruePointer
by: offset].
(currentBytecode between: 172 and: 175)
ifTrue: [↑self jumpIf: FalsePointer
by: offset]
Send 바이트코드(Send Bytecodes)
Send 바이트코드는 메시지의 전송을 야기한다. 메시지의 인자와 수신자에 대한 객체 포인터는 활성 컨텍스트의 평가 스택에서 찾을 수 있다. Send 바이트코드는 메시지의 선택자와 스택으로부터 몇 개의 인자를 취하는지 결정한다. 인자의 개수는 메시지가 호출한 CompiledMethod에서도 나타난다. 컴파일러는 이러한 정보가 충분하게 있도록 보장하는데, 단 perform: 메시지가 CompiledMethod를 도달하여 CompiledMethod가 올바른 인자 개수를 취하도록 검사되는 경우는 제외된다. perform: 메시지는 다음 장의 제어 프리미티브와 관련된 절에서 논하도록 하겠다.
대부분 메시지의 선택자는 CompiledMethod의 리터럴 프레임에서 발견된다. "Literal-selector"(리터럴-선택자) 바이트코드와 "extended-send" 바이트코드는 리터럴 프레임 내에 선택자의 색인과 메시지의 인자 계수를 명시한다. 32개의 "special-selector"(특수-선택자) 바이트코드는 객체 메모리의 Array에 인자 계수와 선택자의 오프셋을 명시한다. 이러한 Array는 시스템 내 모든 CompiledMethods가 공유한다.
sendBytecode
(currentBytecode between: 131 and: 134)
ifTrue: [↑self extendedSendBytecode].
(currentBytecode between: 176 and: 207)
ifTrue: [↑self sendSpecialSelectorBytecode].
(currentBytecode between: 208 and: 255)
ifTrue: [↑self sendLiteralSelectorBytecode]
리터럴-선택자 바이트코드는 0, 1 또는 2를 명시할 수 있는 단일 바이트와 리터럴 프레임의 첫 16개 위치 중 하나에 있는 선택자이다. 선택자 색인과 인자 계수 모두 바이트코드의 비트에서 인코딩된다.
sendLiteralSelectorBytecode
| selector |
selector ← self literal: (self extractBits: 12 to: 15
of: currentBytecode).
self sendSelector: selector
argumentCount: (self extractBits: 10 to: 11
of: currentBytecode) - 1
대부분의 send 바이트코드는 적절한 선택자와 인자 계수를 결정한 후에 sendSelector:argumentCount: 루틴을 호출한다. 이러한 루틴은 messageSelector와 argumentCount 변수를 설정하는데, 이 변수들은 인터프리터 내에서 메시지를 검색하고 잘하면 메서드를 활성화하게 될 다른 루틴에서 이용 가능하다.
sendSelector: selector argumentCount: count
| newReceiver |
messageSelector ← selector.
argumentCount ← count.
newReceiver ← self stackValue: argumentCount.
self sendSelectorToClass: (memory fetchClassOf: newReceiver)
sendSelectorToClass: classPointer
self findNewMethodInClass: classPointer.
self executeNewMethod
인터프리터는 선택자와 연관된 CompiledMethods를 찾는 데 필요한 사전 검색의 개수를 감소시키기 위해 메서드 캐시를 사용한다. 메서드 캐시는 위의 sendSelectorToClass: 에서 findNewMethodInClass: 에 대한 호출 대신 lookupMethodInClass: 호출로 대체하면 누락될 수도 있다. lookupMethodInClass: 루틴은 앞장에서 클래스에 관한 절에 설명한 바 있다. 캐시는 다양한 방식으로 구현될 수 있다. 다음 루틴은 Array 내에 각 엔트리마다 네 개의 순차적 위치를 사용한다. 네 개의 위치는 선택자, 클래스, CompiledMethod, 엔트리에 대한 프리미티브 색인을 저장한다. 이러한 루틴은 재조사를 허용하지 않는다.
findNewMethodInClass: class
| hash |
hash ← (((messageSelector bitAnd: class) bitAnd: 16rFF) bitShift: 2) + 1.
((methodCache at: hash)=messageSelector
and: [(methodCache at: hash + 1) = class])
ifTrue: [newMethod ← methodCache at: hash + 2.
primitiveIndex ← methodCache at: hash + 3]
ifFalse: [self lookupMethodInClass: class.
methodCache at: hash put: messageSelector.
methodCache at: hash + 1 put: class.
methodCache at: hash + 2 put: newMethod.
methodCache at: hash + 3 put: primitiveIndex]
메서드 캐시는 다음 루틴으로 초기화된다.
InitializeMethodCache
methodCacheSize ← 1024.
methodCache ← Array new: methodCacheSize
executeNewMethod 루틴은 CompiledMethod와 연관된 경우 프리미티브 루틴을 호출한다. 어떤 프리미티브도 나타나지 않거나 프리미티브 루틴이 결과를 생성할 수 없는 경우 primitiveResponse 루틴은 false를 리턴한다. 이런 경우 CompiledMethod가 활성화된다. 프리미티브 루틴과 primitiveResponse 루틴은 다음 장에서 설명할 것이다.
executeNewMethod
self primitiveResponse
ifFalse: [self activateNewMethod]
메서드를 활성화하는 루틴은 MethodContext를 생성하고 현재 활성 컨텍스트로부터 새로운 컨텍스트의 스택으로 수신자와 인자를 전달한다. 이는 새로운 컨텍스트를 인터프리터의 활성 컨텍스트로 만든다.
activateNewMethod
| contextSize newContext newReceiver |
(self largeContextFlagOf: newMethod) = 1
ifTrue: [contextSize ← 32 + TempFrameStart]
ifFalse: [contextSize ← 12 + TempFrameStart].
newContext ← memory instantiateClass: ClassMethodContextPointer
withPointers: contextSize.
memory storePointer: SenderIndex
ofObject: newContext
withValue: activeContext.
self storeInstructionPointerValue:
(self initialInstructionPointerOfMethod: newMethod)
inContext: newContext.
self storeStackPointerValue: (self temporaryCountOf: newMethod)
inContext: newContext.
memory storePointer: MethodIndex
ofObject: newContext
withValue: newMethod.
self transfer: argumentCount + 1
fromIndex: stackPointer - argumentCount
ofObject: activeContext
toIndex: ReceiverIndex
ofObject: newContext.
self pop: argumentCount + 1.
self newActiveContext: newContext
Extended-send 바이트코드에는 네 가지가 있다. 처음 두 개는 선택자 색인과 인자 계수가 바이트코드 자체가 아니라 다음으로 오는 하나 또는 두 개의 바이트에서 발견된다는 점만 제외하면 리터럴-선택자 바이트코드와 동일한 효과를 갖는다. 나머지 두 개의 extended-send 바이트코드는 슈퍼클래스 메시지에 사용된다.
extendedSendBytecode
currentBytecode = 131 ifTrue: [↑self singleExtendedSendBytecode].
currentBytecode = 132 ifTrue: [↑self doubleExtendedSendBytecode].
currentBytecode = 133 ifTrue: [↑self singleExtendedSuperBytecode].
currentBytecode = 134 ifTrue: [↑self doubleExtendedSuperBytecode]
확장 전송의 첫 번째 형태 다음에는 단일 바이트가 따라오는데, 그 중에 상위 3 비트에 인자의 개수를 명시하고 하위 5 비트에 선택자 색인을 명시한다.
singleExtendedSendBytecode
| descriptor selectorIndex |
descriptor ← self fetchByte.
selectorIndex ← self extractBits: 11 to: 15
of: descriptor.
self sendSelector: (self literal: selectorIndex)
argumentCount: (self extractBits: 8 to: 10
of: descriptor
두 번째 확장 전송 바이트코드의 형태 뒤에는 2바이트가 따라오고, 첫 번째는 인자의 개수, 두 번째 바이트는 리터럴 프레임 내 선택자의 색인을 나타낸다.
doubleExtendedSendBytecode
| count selector |
count ← self fetchByte.
selector ← self literal: self fetchByte.
self sendSelector: selector
argumentCount: count
컴파일러가 심볼릭 메서드에서 super로 전송되는 메시지에 마주치면 수신자에 대한 self를 밀어넣는 바이트코드를 사용하지만, 선택자를 나타내기 위해서는 정규 send 바이트코드 대신 extended-super 바이트코드를 사용한다. 2개의 extended-super 바이트코드는 extended-send 바이트코드 2개와 유사하다. 첫 번째 뒤에는 단일 바이트가 따라오고 두 번째 뒤에는 extended-send 바이트코드와 정확히 동일하게 해석되는 2 바이트가 따라온다. 이러한 바이트코드가 하는 일에서 유일한 차이는 현재 CompiledMethod가 발견된 클래스의 슈퍼클래스에서 메시지 검색을 시작한다는 점이다. 꼭 self의 직접적인 슈퍼클래스일 필요는 없음을 주목한다. 특히 extended-super 바이트코드를 포함하는 CompiledMethod가 본래 self의 슈퍼클래스에서 발견되었다면 self의 직접적인 슈퍼클래스에 해당하지 않을 것이다. Extended-super 바이트코드를 포함하는 모든 CompiledMethods는 마지막 리터럴 변수로서 발견된 클래스를 갖는다.
singleExtendedSuperBytecode
| descriptor selectorIndex methodClass |
descriptor ← self fetchByte.
argumentCount ← self extractBits: 8 to: 10
of: descriptor.
selectorIndex ← self extractBits: 11 to: 15
of: descriptor.
messageSelector ← self literal: selectorIndex.
methodClass ← self methodClassOf: method.
self sendSelectorToClass: (self superclassOf: methodClass)
doubleExtendedSuperBytecode
| methodClass |
argumentCount ← self fetchByte.
messageSelector ← self literal: self fetchByte.
methodClass ← self methodClassOf: method.
self sendSelectorToClass: (self superclassOf: methodClass)
특수 선택자 집합은 리터럴 프레임에 포함되지 않고 메시지에서 사용될 수 있다. 객체 메모리 내 Array는 alternating 위치에서 선택자의 객체 포인터를 포함한다. 각 선택자에 대한 인자 계수는 선택자의 객체 포인터 다음 위치에 저장된다. specialSelectorPrimitiveResponse 루틴은 다음 장에서 설명할 것이다.
sendSpecialSelectorBytecode
| selectorIndex selector count |
self specialSelectorPrimitiveResponse
ifFalse: [selectorIndex ← (currentBytecode - 176) * 2.
selector ← memory fetchPointer: selectorIndex
ofObject: SpecialSelectorsPointer.
count ← self fetchInteger: selectorIndex + 1
ofObject: SpecialSelectorsPointer.
self sendSelector: selector
argumentCount: count]
Return 바이트코드(Return Bytecodes)
컨텍스트로부터 값과 제어를 리턴하는 바이트코드가 6개가 있는데 5개는 메시지의 값을 리턴하고 (명시적으로 "↑"를 호출하거나 메서드의 끝에서 암시적으로 호출하여) 나머지 하나는 블록의 값을 리턴한다 (블록의 끝에서 암시적으로 호출하여). 두 가지 리턴 유형의 차이는 전자의 경우 home 컨텍스트의 전송자로 리턴하는 반면 후자는 활성 컨텍스트의 호출자에게 리턴한다는 데에 있다. 5개의 return 바이트코드로부터 리턴된 값은 수신자(self), true, false, nil, 또는 스택의 맨 위가 되겠다. 마지막 return 바이트코드는 스택의 맨 위를 블록의 값으로 리턴한다.
returnBytecode
currentBytecode = 120
ifTrue: [↑self returnValue: receiver
to: self sender].
currentBytecode = 121
ifTrue: [↑self returnValue: TruePointer
to: self sender].
currentBytecode = 122
ifTrue: [↑self returnValue: FalsePointer
to: self sender].
currentBytecode = 123
ifTrue: [↑self returnValue: NilPointer
to: self sender].
currentBytecode = 124
ifTrue: [↑self returnValue: self popStack
to: self sender].
currentBytecode = 125
ifTrue: [↑self returnValue: self popStack
to: self caller]
컨텍스트로 값을 리턴하는 간단한 방법은 그것을 활성 컨텍스트로 만들어 값을 스택에 밀어넣는 방법일 것이다.
simpleReturnValue: resultPointer to: contextPointer
self newActiveContext: contextPointer.
self push: resultPointer
하지만 이러한 루틴이 너무 간단해서 올바로 작동하지 못하는 상황이 세 가지 있다. 활성 컨텍스트의 전송자가 nil인 경우 이 루틴은 인터프리터의 활성 컨텍스트 포인터에 nil을 저장하여 시스템을 바람직하지 못한 halt로 가져갈 것이다. 이를 방지하기 위해서는 실제 returnValue:to: 루틴이 먼저 전송자가 nil인지 확인해야 한다. 인터프리터는 이미 리턴시킨 컨텍스트로는 리턴을 금지한다. 리턴 시 활성 컨텍스트의 명령 포인터에 nil을 저장하고 리턴되는 컨텍스트의 nil 명령 포인터를 검사하여 이루어진다. 두 가지 상황 모두 컨텍스트가 객체인데다가 인터프리터뿐만 아니라 사용자 프로그램에 의해 조작될 수 있기 때문에 발생한다. 두 상황 중 하나라도 발생하면 인터프리터는 활성 컨텍스트로 메시지를 전송하여 문제를 알린다. 세 번째 상황은 참조 계수를 바탕으로 객체를 자동으로 할당해제하는 시스템에서 발생할 것이다. 활성 컨텍스트는 리턴하면서 할당해제될 수 있다. 이로 인해 다시 리턴되는 결과에 대한 참조만 포함하게 될지도 모른다. 이런 경우 결과는 새로운 컨텍스트의 스택으로 밀어넣기 전에 할당해제될 것이다. 이러한 사항들로 인해 returnValue: 루틴은 더 복잡할 수 밖에 없다.
returnValue: resultPointer to: contextPointer
| sendersIP |
contextPointer = NilPointer
ifTrue: [self push: activeContext.
self push: resultPointer.
↑self sendSelector: CannotReturnSelector
argumentCount: 1].
sendersIP ← memory fetchPointer: InstructionPointerIndex
ofObject: contextPointer.
sendersIP = NilPointer
ifTrue: [self push: activeContext.
self push: resultPointer.
↑self sendSelector: CannotReturnSelector
argumentCount: 1].
memory increaseReferencesTo: resultPointer.
self returnToActiveContext: contextPointer.
self push: resultPointer.
memory decreaseReferencesTo: resultPointer
이 루틴은 새로운 스택으로 밀어넣을 때까지 참조 계수를 증가시킴으로써 리턴되는 결과의 할당해제를 예방한다. 활성 컨텍스트를 전환시키기 전에 결과를 밀어넣을 수도 있다. returnToActiveContext: 루틴은 기본적으로 newActiveContext: 루틴과 같은데, 리턴시키는 컨텍스트의 cache된 필드를 저장하는 대신 전송자와 명령 포인터 필드에 nil을 저장한다는 점이 다르다.
returnToActiveContext: aContext
memory increaseReferencesTo: aContext.
self nilContextFields.
memory decreaseReferencesTo: activeContext.
activeContext ← aContext.
self fetchContextRegisters
nilContextFields
memory storePointer: SenderIndex
ofObject: activeContext
withValue: NilPointer.
memory storePointer: InstructionPointerIndex
ofObject: activeContext
withValue: NilPointer