지난 시간까지는 double 타입의 숫자를 다룰 수 있는 기본적인 형태를 만들었습니다. 이번에는 true, false, nil 과 같은 숫자 외에 프로그래밍에 필요한 데이터 타입을 정의합니다.
이번 시간의 목표는 앞서 말한 데이터 타입들의 정의와 이를 이용한 비교 연산(<, >, ==)과 논리 연산(!)을 수행할 수 있도록 확장하는 것입니다.
Value 구조체의 변경: union가장 근본적인 변화는 데이터를 담는 그릇인 Value 타입의 정의입니다.
// value.h - 이전
typedef double Value;
기존에는 모든 값을 숫자로만 취급했기에 double 그 자체였습니다.
// value.h - 현재
typedef enum {
VAL_BOOL,
VAL_NIL,
VAL_NUMBER,
} ValueType;
typedef struct {
ValueType type; // 값의 종류를 나타내는 태그
union {
bool boolean;
double number;
} as; // 실제 데이터
} Value;
이제는 값이 숫자인지, 불리언인지, 혹은 nil인지 구분해야 합니다. C 언어는 정적 타입 언어이므로, 동적 타입인 Lox 의 값을 담기 위해 union을 도입했습니다.
Value는 타입을 외부에 전달하기 위한 type과 실제 값을 가지고 있는 as를 가지고 있습니다.
이를 안전하게 다루기 위해 값을 확인(IS_BOOL)하고, C 언어의 기본 타입으로 변환(AS_BOOL)하고, 다시 Lox 값으로 포장(BOOL_VAL)하는 매크로들도 대거 추가되었습니다. 아래와 같이 사용할 수 있습니다.
// vm.c
static bool isFalsey(Value value) {
return IS_NIL(value) || (IS_BOOL(value) && !AS_BOOL(value));
}
메모리에 적힌 두 값이 다른지 가장 빠르게 비교하는 방법은 무엇일까요? 아마도 답은 메모리를 직접 비교(memcmp)하는 것일 겁니다. 비교 함수를 호출할 필요도 없이 말입니다. 하지만 clox에서는 불가능했습니다. 아래는 clox의 값이 같은지 검사하는 코드입니다.
// value.c
bool valuesEqual ( Value a , Value b ) {
if ( a . type != b . type ) return false ;
switch ( a . type ) {
case VAL_BOOL : return AS_BOOL ( a ) == AS_BOOL ( b );
case VAL_NIL : return true ;
case VAL_NUMBER : return AS_NUMBER ( a ) == AS_NUMBER ( b );
default : return false ; // 도달할 수 없음.
}
}
이렇게 했던 이유는 아래 그림을 보며 설명하겠습니다.
이미지 출처: Crafting Interpreters
C 언어에서는 사용하지 않는 비트에 대해서 동일한 값이 들어있음을 보장하지 않습니다. 그리고 Value의 특성상 패딩으로 인해 빈 메모리 공간은 존재합니다. 이 패딩 안의 값은 동일한 타입이라도 다른 값이 들어있을 수 있어 memcmp를 사용할 수 없었습니다. 저자는 수 많은 디버깅을 통해 이 결과를 알아냈지만 우리는 그 결과를 쉽게 받아들일 수 있음에 큰 감사를 보냅니다.
OpCode)와 컴파일러의 확장새로운 타입이 생겼으니, 이를 처리할 새로운 명령어도 필요합니다. 산술 연산만 있던 Chunk에 논리 및 비교 연산이 추가되었습니다.
// chunk.h
typedef enum
{
OP_CONSTANT,
OP_NIL,
OP_TRUE,
OP_FALSE,
OP_EQUAL,
OP_GREATER,
OP_LESS,
...
OP_NOT,
OP_NEGATE,
OP_RETURN,
} OpCode;
OP_NIL, OP_TRUE, OP_FALSEOP_EQUAL (==), OP_GREATER (>), OP_LESS (<)OP_NOT (!)// compiler.c
static void literal()
{
switch (parser.previous.type) {
case TOKEN_FALSE: emitByte(OP_FALSE); break;
case TOKEN_NIL: emitByte(OP_NIL); break;
case TOKEN_TRUE: emitByte(OP_TRUE); break;
default: return; // Unreachable.
}
}
number()가 숫자를 상수 테이블에 넣었다면, literal()은 고정된 값(true, false, nil)에 대응하는 특정 명령어를 바로 내보냅니다. 이 함수를 통해 상수 테이블을 쓰지 않고 직접 스택에 푸시합니다.
비교 연산(>, <)은 결과로 VAL_BOOL 타입의 값을 스택에 푸시합니다.
rules) 업데이트프랫 파서의 rules 테이블에 새로운 토큰들을 등록했습니다. 이제 !는 unary로, 비교 연산자들은 binary로 연결되어 우선순위에 따라 파싱됩니다.
// compiler.c
ParseRule rules[] =
{
[TOKEN_BANG] = {unary, NULL, PREC_NONE}, // !
[TOKEN_BANG_EQUAL] = {NULL, binary, PREC_EQUALITY}, // !=
[TOKEN_EQUAL] = {NULL, NULL, PREC_NONE},
[TOKEN_EQUAL_EQUAL] = {NULL, binary, PREC_EQUALITY},
[TOKEN_GREATER] = {NULL, binary, PREC_COMPARISON},
[TOKEN_GREATER_EQUAL] = {NULL, binary, PREC_COMPARISON},
[TOKEN_LESS] = {NULL, binary, PREC_COMPARISON},
[TOKEN_LESS_EQUAL] = {NULL, binary,
...
[TOKEN_EOF] = {NULL, NULL, PREC_NONE},
};
가장 큰 변화는 가상 머신(vm.c)에 있습니다. 이전에는 무조건 pop()해서 더하거나 빼면 됐지만, 이제는 "숫자가 아닌 것에 뺄셈을 시도하는지" 감시해야 합니다.
BINARY_OP 매크로가 단순히 연산만 하는 것에서, 피연산자의 타입을 검사하는 로직으로 변경되었습니다.
// vm.c
#define BINARY_OP(valueType, op) \
do { \
if (!IS_NUMBER(peek(0)) || !IS_NUMBER(peek(1))) { \
runtimeError("Operands must be numbers."); \
return INTERPRET_RUNTIME_ERROR; \
} \
// ... 연산 수행
} while (false) \
이제 false + 5 같은 코드를 실행하면 VM이 Operands must be numbers. 라는 런타임 에러를 뱉고 안전하게 종료됩니다.
Falsey의 개념 (nil 과 false)Lox 언어에서 "거짓"으로 취급되는 것은 nil과 false 두 가지뿐입니다. 이를 처리하기 위한 앞서 Value 구조체 설명에서 나왔던 isFalsey 함수가 추가되었고, OP_NOT (!) 연산에서 이를 활용합니다.
이제 clox는 다음과 같은 복합적인 식을 이해하고 실행할 수 있습니다.
> !(5 - 4 > 1)
true
5 - 4를 계산하여 1을 얻습니다.
1 > 1을 비교하여 false를 얻습니다.
!false를 수행하여 최종적으로 true가 출력됩니다.
이번 시간은 지난 포스팅보다 쉬웠습니다. 그리고 좀 더 '굴러가는' 듯한 느낌을 주는 과정이었습니다. 하지만 모든 과정이 지난 포스팅의 '재귀 함수를 통해 인터프리터가 작동하는 원리' 를 이해하는 것을 기조로 하고 있습니다.
재미있던 점은, C 언어의 특성으로 인해 memcmp로 빠르게 가능할 것이라 여겼던 동일 비교 연산이 그렇지 않았다는 것입니다. 패딩의 여백 부분의 동일함을 보장되지 않아 어쩔 수 없이 AS_BOOL, AS_NUMBER 와 같은 매크로 연산이 불가피했던 것은 몰랐던 내용이었습니다.
현재는 숫자와 연산자로 이루어진 식을 이해하고 결과를 도출하는 인터프리터를 만들었습니다. 다음 시간에는 문자열을 상수 컨테이너에 넣고, 정상적으로 해제하는 구조를 만들어보겠습니다.
참고 자료: Cratring Interpreters - types of values
Github: Will-big/clox