Smalltalk80LanguageImplementationKor:Chapter 28

From 흡혈양파의 번역工房
Jump to navigation Jump to search
제 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