정의
정의
Nim으로 코드를 쓰기 전에, 매뉴얼 곳곳에서 반복해서 만나게 될 기본 용어들을 먼저 정리해 둘게요. 이 정의들이 단순한 사전식 나열처럼 보여도, 뒤에서 설명할 "타입 검사", "컴파일 타임 실행", "panic" 같은 주제를 정확히 이해하는 데 전부 바탕이 되거든요. 이름만 기억할 게 아니라, 각 용어가 실제로 가리키는 대상을 함께 짚어 보면 좋아요.
출처: Nim Manual
본문
위치와 변수
Nim 코드는 메모리를 대상으로 하는 계산을 명시해요. 이때 메모리는 location(위치)이라는 구성 요소로 이루어져 있는데, **변수(variable)**는 기본적으로 어떤 location에 붙은 이름이라고 보면 돼요. 즉 변수와 location은 둘 다 특정한 type(타입)을 가져요. 여기서 변수의 타입을 static type(정적 타입), location의 타입을 dynamic type(동적 타입)이라고 불러요. static type과 dynamic type이 다를 때는, 한쪽이 다른 쪽의 super-type이거나 subtype이 되는 경우에요.
변수의 타입이라고 하면 보통 static type을 떠올리기 쉬운데요, 실제 저장 공간이 가진 타입은 dynamic type이라는 점을 구분해 두면 나중에 타입 시스템을 다룰 때 훨씬 수월해져요.
식별자와 유효 범위
identifier(식별자)는 변수, 타입, 프로시저 같은 대상을 가리키기 위해 선언된 이름이에요. 이 선언이 적용되는 프로그램의 영역을 그 선언의 scope(유효 범위)라고 해요. scope는 중첩될 수 있어요.
그럼 이름이 겹치면 어떻게 정해질까요? 식별자의 의미는 그 식별자가 선언된 가장 작은(가장 안쪽의) enclosing scope에 의해 결정돼요. 다만 오버로딩(overloading) 해석 규칙이 달리 제안하는 경우는 예외예요. 중첩된 scope에서 같은 이름을 다시 선언하면 안쪽 것이 이긴다고 이해하면 돼요.
표현식과 l-value
expression(표현식)은 값을 만들거나 location을 만들어 내는 계산이에요. 이때 location을 만들어 내는 표현식을 l-value라고 불러요. l-value는 문맥에 따라 location 자체를 가리킬 수도, 그 location이 담고 있는 값을 가리킬 수도 있어요.
프로그램, 소스 파일, 컴파일러, 실행 파일
Nim program은 Nim 코드를 담고 있는 하나 이상의 텍스트 source files(소스 파일)로 구성돼요. 이 프로그램은 Nim compiler에 의해 처리되어 executable(실행 파일)이 돼요. 이 executable의 실체는 컴파일러 구현에 따라 달라져요. 예를 들어 네이티브 바이너리일 수도 있고, JavaScript 소스 코드일 수도 있죠.
컴파일 타임과 런타임
전형적인 Nim 프로그램에서는 대부분의 코드가 executable 안으로 컴파일돼요. 하지만 일부 코드는 compile-time(컴파일 타임)에 실행되기도 해요. 여기에는 상수 표현식, 매크로 정의, 그리고 매크로 정의가 사용하는 Nim 프로시저가 포함될 수 있어요.
Nim 언어의 대부분은 compile-time에서 지원되지만, 몇 가지 제한이 있어요. 자세한 내용은 컴파일 타임 실행의 제한 절에서 다룰게요. 참고로 이 매뉴얼에서 runtime(런타임)이라는 말은 compile-time 실행과 executable 안에서의 코드 실행을 모두 아우르는 표현이에요.
AST와 의미 분석
컴파일러는 Nim 소스 코드를 abstract syntax tree(구문 트리, 줄여서 AST)라는 내부 데이터 구조로 파싱해요. 그리고 코드를 실행하거나 executable로 컴파일하기 전에, 이 AST를 semantic analysis(의미 분석) 과정을 통해 변형해요. 이 과정에서 표현식의 타입, 식별자의 의미, 경우에 따라서는 표현식의 값 같은 의미 정보가 추가돼요.
의미 분석 중에 잡히는 오류를 static error(정적 오류)라고 해요. 이 매뉴얼에서 달리 명시하지 않는 한, 설명하는 오류는 모두 static error라고 보면 돼요.
panic
panic은 구현이 runtime에 감지해서 보고하는 오류예요. 이 오류를 보고하는 방식은 예외를 던지거나(raising exceptions) 치명적 오류로 죽는(dying with a fatal error) 것 두 가지예요. 다만 구현은 이 runtime checks를 끌 수 있는 수단을 제공해요. 자세한 내용은 Pragmas 절에서 다룰게요.
panic이 예외가 될지 치명적 오류가 될지는 구현에 따라 달라요. 그래서 다음 프로그램은 사실상 올바르지 않아요. 코드가 배열 범위를 벗어난 접근으로부터 IndexDefect를 잡겠다는 의도인데도, 컴파일러가 대신 프로그램을 치명적 오류로 끝내버릴 수 있거든요.
var a: array[0..1, char]
let i = 5
try:
a[i] = 'N'
except IndexDefect:
echo "invalid index"
현재 구현에서는 --panics:on|off 옵션으로 이 두 동작을 전환할 수 있어요. panics를 켜면 프로그램이 panic으로 죽고, 끄면 runtime 오류가 예외로 바뀌어요. --panics:on의 장점은 더 작은 바이너리 코드를 만들고, 컴파일러가 코드를 최적화할 자유가 더 커진다는 점이에요.
검사되지 않는 런타임 오류와 안전성
unchecked runtime error(검사되지 않는 런타임 오류)는 감지가 보장되지 않는 오류로, 그 이후 계산 동작이 임의적(arbitrary)이 될 수 있어요. 이 오류는 safe(안전한) 언어 기능만 사용하고, 어떤 runtime check도 끄지 않으면 발생하지 않아요.
상수 표현식
constant expression(상수 표현식)은 그 값이 나타난 코드의 의미 분석 중에 계산될 수 있는 표현식이에요. 이 표현식은 절대 l-value가 아니고, 절대 부작용(side effect)을 갖지 않아요. 그리고 constant expression은 constant folding 같은 의미 분석 기능에 국한되지 않아요. compile-time 실행을 위해 지원되는 모든 Nim 언어 기능을 사용할 수 있죠.
constant expression이 배열 경계를 정의할 때처럼 의미 분석의 입력으로 쓰일 수 있기 때문에, 이 유연성은 컴파일러가 의미 분석과 compile-time 코드 실행을 서로 섞어서 수행하도록 요구해요.
대략적으로는, 의미 분석이 소스 코드를 위에서 아래로, 왼쪽에서 오른쪽으로 진행되면서, 이후의 의미 분석에 필요한 값을 계산하기 위해 compile-time 코드 실행이 필요할 때 그 실행이 끼워지는 방식이라고 생각하면 거의 정확해요. 그리고 이 문서의 훨씬 뒤에서 보게 되겠지만, 매크로 호출은 이렇게 끼워지는 실행을 필요로 할 뿐만 아니라, 의미 분석이 완전히 위에서 아래로, 왼쪽에서 오른쪽으로 진행되지 않는 상황까지 만들어 내요.
더 알아보기 (Learn more)
- Nim Manual — 공식 매뉴얼 전문
- 컴파일 타임 실행의 제한 — compile-time에서 지원되지 않는 기능 정리
- Pragmas — runtime check를 끄는 방법