프라그마
프라그마
프라그마(pragma)는 수많은 새 키워드를 도입하지 않고도 컴파일러에 추가 정보/명령을 전달하는 Nim의 방법입니다. 프라그마는 시맨틱 검사(semantic checking) 중에 그 자리에서 처리됩니다. 프라그마는 특별한 중괄호 {.와 .}로 둘러쌉니다. 프라그마는 또한 해당 기능에 더 나은 문법이 제공되기 전에 언어 기능을 먼저 시험해 보기 위한 첫 구현으로 자주 사용됩니다.
deprecated 프라그마 (deprecated pragma)
deprecated 프라그마는 심볼을 더 이상 사용하지 않음(deprecated)으로 표시하는 데 사용합니다:
proc p() {.deprecated.}
var x {.deprecated.}: char
이 프라그마는 개발자에게 전달할 선택적인 경고 문자열(warning string)을 받을 수도 있습니다.
proc thing(x: bool) {.deprecated: "use thong instead".}
compileTime 프라그마 (compileTime pragma)
compileTime 프라그마는 프로시저나 변수를 컴파일 시점(compile-time) 실행에서만 사용하도록 표시하는 데 사용합니다. 이에 대한 코드는 생성되지 않습니다. 컴파일 시점 프로시저는 매크로의 헬퍼로 유용합니다. 언어 버전 0.12.0부터, 매개변수 타입에 system.NimNode를 사용하는 프로시저는 암시적으로 compileTime으로 선언됩니다:
proc astHelper(n: NimNode): NimNode =
result = n
다음과 같은 뜻입니다:
proc astHelper(n: NimNode): NimNode {.compileTime.} =
result = n
compileTime 변수는 런타임에서도 사용할 수 있습니다. 이는 변수가 컴파일 시점에 채워지지만(예: 룩업 테이블) 런타임에 접근되는 특정 관용구를 단순화합니다:
import std/macros
var nameToProc {.compileTime.}: seq[(string, proc (): string {.nimcall.})]
macro registerProc(p: untyped): untyped =
result = newTree(nnkStmtList, p)
let procName = p[0]
let procNameAsStr = $p[0]
result.add quote do:
nameToProc.add((`procNameAsStr`, `procName`))
proc foo: string {.registerProc.} = "foo"
proc bar: string {.registerProc.} = "bar"
proc baz: string {.registerProc.} = "baz"
doAssert nameToProc[2][1]() == "baz"
noreturn 프라그마 (noreturn pragma)
noreturn 프라그마는 결코 반환하지 않는(return) 프로시저를 표시하는 데 사용합니다.
acyclic 프라그마 (acyclic pragma)
acyclic 프라그마는 순환(cyclic)처럼 보임에도 불구하고 비순환(acyclic)으로 표시하기 위해 객체 타입에 사용할 수 있습니다. 이는 가비지 컬렉터(GC)가 이 타입의 객체를 순환의 일부로 간주하지 않도록 하는 최적화입니다:
type
Node = ref NodeObj
NodeObj {.acyclic.} = object
left, right: Node
data: string
또는 ref 객체를 직접 사용하는 경우:
type
Node {.acyclic.} = ref object
left, right: Node
data: string
예제에서 Node 타입으로 트리 구조를 선언합니다. 타입 정의가 재귀적이므로 GC는 이 타입의 객체가 순환 그래프를 형성할 수 있다고 가정해야 합니다. acyclic 프라그마는 이것이 일어날 수 없다는 정보를 GC에 전달합니다. 프로그래머가 실제로는 순환적인 데이터 타입에 acyclic 프라그마를 사용하면 메모리 누수가 발생할 수 있지만, 메모리 안전성(memory safety)은 유지됩니다.
final 프라그마 (final pragma)
final 프라그마는 객체 타입에 사용하여 그 타입으로부터 상속(inherit)될 수 없도록 지정할 수 있습니다. 상속은 기존 객체로부터 상속하는 객체(object of SuperType 문법)나 inheritable로 표시된 객체에만 가능하다는 점에 유의하세요.
shallow 프라그마 (shallow pragma)
shallow 프라그마는 타입의 의미(semantics)에 영향을 줍니다: 컴파일러가 얕은 복사(shallow copy)를 수행하도록 허용됩니다. 이는 심각한 의미론적 문제를 일으키고 메모리 안전성을 깨뜨릴 수 있습니다! 하지만 대입(assignment)을 상당히 빨라지게 할 수 있는데, Nim의 의미론은 시퀀스와 문자열을 깊은 복사(deep copy)하도록 요구하기 때문입니다. 특히 시퀀스로 트리 구조를 만들 때 이 비용이 클 수 있습니다:
type
NodeKind = enum nkLeaf, nkInner
Node {.shallow.} = object
case kind: NodeKind
of nkLeaf:
strVal: string
of nkInner:
children: seq[Node]
pure 프라그마 (pure pragma)
객체 타입에 pure 프라그마를 표시하면 런타임 타입 식별(runtime type identification)에 사용되는 타입 필드(type field)가 생략됩니다. 이전에는 다른 컴파일 언어와의 바이너리 호환성을 위해 필요했습니다.
열거형(enumerated) 타입을 pure로 표시할 수도 있습니다. 그러면 그 필드에 접근할 때 항상 완전한 정규화(full qualification)가 필요합니다.
asmNoStackFrame 프라그마 (asmNoStackFrame pragma)
asmNoStackFrame 프라그마로 프로시저를 표시하면 컴파일러에게 해당 프로시저에 대해 스택 프레임(stack frame)을 생성하지 말라고 지시할 수 있습니다. 또한 return result; 같은 종료 문장도 생성되지 않고, 생성된 C 함수는 (사용하는 C 컴파일러에 따라) __declspec(naked) 또는 __attribute__((naked))로 선언됩니다.
참고: 이 프라그마는 오직 어셈블러 문장으로만 구성된 프로시저에만 사용해야 합니다.
error 프라그마 (error pragma)
error 프라그마는 컴파일러가 주어진 내용으로 오류 메시지를 출력하도록 하는 데 사용합니다. 다만 오류 이후에 컴파일이 반드시 중단되지는 않습니다.
error 프라그마는 또한 (이터레이터나 프로시저 같은) 심볼을 주석 처리하는 데 사용할 수 있습니다. 그 심볼을 사용하면 정적 오류(static error)가 발생합니다. 이는 오버로딩과 타입 변환 때문에 어떤 연산이 유효하게 취급되는 것을 배제하는 데 특히 유용합니다:
## check that underlying int values are compared and not the pointers:
proc `==`(x, y: ptr int): bool {.error.}
fatal 프라그마 (fatal pragma)
fatal 프라그마는 컴파일러가 주어진 내용으로 오류 메시지를 출력하도록 하는 데 사용합니다. error 프라그마와 달리, 이 프라그마는 컴파일이 반드시 중단됨이 보장됩니다. 예:
when not defined(objc):
{.fatal: "Compile this program with the objc command!".}
warning 프라그마 (warning pragma)
warning 프라그마는 컴파일러가 주어진 내용으로 경고 메시지를 출력하도록 하는 데 사용합니다. 경고 이후에도 컴파일은 계속됩니다.
hint 프라그마 (hint pragma)
hint 프라그마는 컴파일러가 주어진 내용으로 힌트 메시지를 출력하도록 하는 데 사용합니다. 힌트 이후에도 컴파일은 계속됩니다.
line 프라그마 (line pragma)
line 프라그마는 스택 역추적(stack backtrace)에서 보이는 주석 처리된 문장의 줄 정보에 영향을 주는 데 사용할 수 있습니다:
template myassert*(cond: untyped, msg = "") =
if not cond:
# change run-time line information of the 'raise' statement:
{.line: instantiationInfo().}:
raise newException(AssertionDefect, msg)
line 프라그마를 매개변수와 함께 사용하면 그 매개변수는 tuple[filename: string, line: int]이어야 합니다. 매개변수 없이 사용하면 system.instantiationInfo()가 사용됩니다.
linearScanEnd 프라그마 (linearScanEnd pragma)
linearScanEnd 프라그마는 Nim case 문을 어떻게 컴파일할지 컴파일러에 알려주는 데 사용할 수 있습니다. 문법상 문장(statement)으로 사용해야 합니다:
case myInt
of 0:
echo "most common case"
of 1:
{.linearScanEnd.}
echo "second most common case"
of 2: echo "unlikely: use branch table"
else: echo "unlikely too: use branch table for ", myInt
예제에서 case 분기 0과 1은 나머지 case보다 훨씬 흔합니다. 따라서 생성된 어셈블러 코드는 이 값들을 먼저 검사해서 CPU의 분기 예측기(branch predictor)가 성공할 좋은 기회를 갖도록 해야 합니다(값비싼 CPU 파이프라인 지연을 피함). 나머지 case들은 O(1) 오버헤드를 위해 점프 테이블(jump table)에 넣을 수 있지만 (매우 가능성 높은) 파이프라인 지연을 대가로 치릅니다.
linearScanEnd 프라그마는 선형 검색(linear scanning)으로 검사해야 할 마지막 분기에 넣어야 합니다. 전체 case 문의 마지막 분기에 넣으면 전체 case 문이 선형 검색을 사용합니다.
computedGoto 프라그마 (computedGoto pragma)
computedGoto 프라그마는 while true 문 안의 Nim case를 어떻게 컴파일할지 컴파일러에 알려주는 데 사용할 수 있습니다. 문법상 루프 안의 문장(statement)으로 사용해야 합니다:
type
MyEnum = enum
enumA, enumB, enumC, enumD, enumE
proc vm() =
var instructions: array[0..100, MyEnum]
instructions[2] = enumC
instructions[3] = enumD
instructions[4] = enumA
instructions[5] = enumD
instructions[6] = enumC
instructions[7] = enumA
instructions[8] = enumB
instructions[12] = enumE
var pc = 0
while true:
{.computedGoto.}
let instr = instructions[pc]
case instr
of enumA:
echo "yeah A"
of enumC, enumD:
echo "yeah CD"
of enumB:
echo "yeah B"
of enumE:
break
inc(pc)
vm()
예제에서 보듯 computedGoto는 주로 인터프리터에 유용합니다. 기반이 되는 백엔드(C 컴파일러)가 computed goto 확장을 지원하지 않으면 이 프라그마는 그냥 무시됩니다.
immediate 프라그마 (immediate pragma)
immediate 프라그마는 더 이상 사용되지 않습니다(obsolete). "Typed vs untyped parameters"를 참고하세요.
redefine 프라그마 (redefine pragma)
같은 시그니처를 가진 템플릿 심볼의 재정의가 허용됩니다. 이는 redefine 프라그마로 명시적으로 나타낼 수 있습니다:
template foo: int = 1
echo foo() # 1
template foo: int {.redefine.} = 2
echo foo() # 2
# warning: implicit redefinition of template
template foo: int = 3
이는 주로 매크로가 생성한 코드를 위한 것입니다.
컴파일 옵션 프라그마 (compilation option pragmas)
여기에 나열된 프라그마들은 proc/method/converter에 대한 코드 생성 옵션을 재정의하는 데 사용할 수 있습니다.
현재 구현은 다음 옵션들을 제공합니다(다른 것들은 나중에 추가될 수 있습니다).
| 프라그마 | 허용 값 | 설명 |
|---|---|---|
checks |
on|off | 모든 런타임 검사에 대한 코드 생성을 켜거나 끕니다. |
boundChecks |
on|off | 배열 경계 검사에 대한 코드 생성을 켜거나 끕니다. |
overflowChecks |
on|off | 오버플로/언더플로 검사에 대한 코드 생성을 켜거나 끕니다. |
nilChecks |
on|off | nil 포인터 검사에 대한 코드 생성을 켜거나 끕니다. |
assertions |
on|off | assertions에 대한 코드 생성을 켜거나 끕니다. |
warnings |
on|off | 컴파일러의 경고 메시지를 켜거나 끕니다. |
hints |
on|off | 컴파일러의 힌트 메시지를 켜거나 끕니다. |
optimization |
none|speed|size | 속도나 크기를 위해 코드를 최적화하거나, 최적화를 끕니다. |
patterns |
on|off | 항 재작성(term rewriting) 템플릿/매크로를 켜거나 끕니다. |
callconv |
cdecl|... | 그 뒤에 오는 모든 프로시저(및 프로시저 타입)의 기본 호출 규약(calling convention)을 지정합니다. |
예:
{.checks: off, optimization: speed.}
# compile without runtime checks and optimize for speed
push와 pop 프라그마 (push and pop pragmas)
push/pop 프라그마는 option 지시어와 매우 비슷하지만, 설정을 일시적으로 재정의하는 데 사용됩니다. 예:
{.push checks: off.}
# compile this section without runtime checks as it is
# speed critical
# ... some code ...
{.pop.} # restore old settings
push/pop은 일부 표준 라이브러리 프라그마를 켜고 끌 수 있습니다. 예:
{.push inline.}
proc thisIsInlined(): int = 42
func willBeInlined(): float = 42.0
{.pop.}
proc notInlined(): int = 9
{.push discardable, boundChecks: off, compileTime, noSideEffect, experimental.}
template example(): string = "https://nim-lang.org"
{.pop.}
{.push deprecated, used, stackTrace: off.}
proc sample(): bool = true
{.pop.}
서드파티 프라그마의 경우, 구현에 따라 다르지만 같은 문법을 사용합니다.
register 프라그마 (register pragma)
register 프라그마는 변수에만 사용합니다. 변수를 register로 선언하여 하드웨어 레지스터에 배치해 더 빠르게 접근해야 한다는 힌트를 컴파일러에 줍니다. 하지만 C 컴파일러는 보통 이것을 무시하는데, 그럴 만한 이유가 있습니다: 대개는 그것 없이도 더 나은 결과를 내기 때문입니다.
그러나 매우 특수한 경우(예: 바이트코드 인터프리터의 디스패치 루프)에는 이점이 있을 수 있습니다.
global 프라그마 (global pragma)
global 프라그마는 프로시저 안의 변수에 적용하여, 컴파일러가 그 변수를 전역 위치에 저장하고 프로그램 시작 시 한 번 초기화하도록 지시할 수 있습니다.
proc isHexNumber(s: string): bool =
var pattern {.global.} = re"[0-9a-fA-F]+"
result = s.match(pattern)
제네릭 프로시저 안에서 사용하면, 프로시저의 각 인스턴스화마다 별도의 고유한 전역 변수가 생성됩니다. 모듈 내에서 생성된 전역 변수들의 초기화 순서는 정의되어 있지 않지만, 모두 그 원래 모듈(originating module)의 모든 최상위 변수 이후, 그리고 그 모듈을 import하는 모듈의 어떤 변수보다 이전에 초기화됩니다.
특정 메시지 비활성화 (Disabling certain messages)
Nim은 사용자를 귀찮게 할 수 있는 일부 경고와 힌트를 생성합니다. 특정 메시지를 비활성화하는 메커니즘이 제공됩니다: 각 힌트와 경고 메시지는 심볼과 연결됩니다. 이것이 메시지의 식별자이며, 프라그마 뒤의 대괄호 안에 넣어 메시지를 켜거나 끄는 데 사용할 수 있습니다:
{.hint[XDeclaredButNotUsed]: off.} # Turn off the hint about declared but not used symbols.
이것은 모든 경고를 한 번에 비활성화하는 것보다 종종 더 낫습니다.
used 프라그마 (used pragma)
Nim은 내보내지지(export) 않고 사용되지도 않는 심볼에 대해 경고를 생성합니다. used 프라그마를 심볼에 붙이면 이 경고를 억제할 수 있습니다. 이는 특히 해당 심볼이 매크로로 생성된 경우에 유용합니다:
template implementArithOps(T) =
proc echoAdd(a, b: T) {.used.} =
echo a + b
proc echoSub(a, b: T) {.used.} =
echo a - b
# no warning produced for the unused 'echoSub'
implementArithOps(int)
echoAdd 3, 5
used는 모듈을 "사용됨(used)"으로 표시하는 최상위 문장으로도 사용할 수 있습니다. 이는 "Unused import" 경고를 방지합니다:
# module: debughelper.nim
when defined(nimHasUsed):
# 'import debughelper' is so useful for debugging
# that Nim shouldn't produce a warning for that import,
# even if currently unused:
{.used.}
experimental 프라그마 (experimental pragma)
experimental 프라그마는 실험적인 언어 기능을 활성화합니다. 구체적인 기능에 따라, 이는 그 기능이 안정적인 릴리스에는 너무 불안정하다고 간주되거나, 그 기능의 미래가 불확실하다는 뜻입니다(언제든 제거될 수 있습니다). 자세한 내용은 experimental 매뉴얼을 참고하세요.
예:
import std/threadpool
{.experimental: "parallel".}
proc threadedEcho(s: string, i: int) =
echo(s, " ", $i)
proc useParallel() =
parallel:
for i in 0..4:
spawn threadedEcho("echo in parallel", i)
useParallel()
최상위 문장으로서 experimental 프라그마는 활성화된 모듈의 나머지 부분에 대해 기능을 활성화합니다. 이는 모듈 범위를 넘어서는 매크로 및 제네릭 인스턴스화에 문제가 됩니다. 현재 이러한 사용은 .push/.pop 환경 안에 넣어야 합니다:
# client.nim
proc useParallel*[T](unused: T) =
# use a generic T here to show the problem.
{.push experimental: "parallel".}
parallel:
for i in 0..4:
echo "echo in parallel"
{.pop.}
import client
useParallel(1)