Smalltalk80LanguageImplementationKor:Chapter 28

From 흡혈양파의 번역工房
Jump to navigation Jump to search
The printable version is no longer supported and may have rendering errors. Please update your browser bookmarks and please use the default browser print function instead.
제 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


Notes