루아 레퍼런스 매뉴얼 — 언어: 문
루아 레퍼런스 매뉴얼 — 언어: 문 (Statements)
루아가 다루는 **문(statement)**은 다른 일반적인 언어들과 크게 다르지 않아요. 블록(block), 할당(assignment), 제어 구조(control structure), 함수 호출(function call), 변수 선언(variable declaration)이 바로 그것인데요. 이 절에서는 이 다섯 종류가 문법상 어떻게 생겼고, 실제로 어떤 식으로 동작하는지를 하나씩 풀어볼게요. 이 절을 쭉 따라가다 보면 "루아에서는 문이 어떻게 쓰이는구나" 하는 전체 그림이 잡힐 거예요.
본문
루아는 거의 전통적인 문 집합을 지원해요. 다른 일반적인 언어들과 비슷하다고 보면 돼요. 이 집합에는 블록, 할당, 제어 구조, 함수 호출, 그리고 변수 선언이 포함돼요.
3.3.1 블록 (Blocks)
블록은 문들의 목록인데, 이 문들이 순서대로 실행돼요.
block ::= {stat}
루아에는 **빈 문(empty statement)**이라는 게 있어요. 이걸 쓰면 문과 문을 세미콜론으로 구분하거나, 블록을 세미콜론으로 시작하거나, 세미콜론 두 개를 연달아 쓸 수도 있어요.
stat ::= ';'
여기서 잠깐, 함수 호출과 할당은 둘 다 여는 괄호로 시작할 수 있는데요. 이 점이 루아 문법에 모호함(ambiguity)을 만들어 내요. 다음 코드 조각을 볼게요.
a = b + c
(print or io.write)('done')
이 조각은 문법상 두 가지로 해석될 수 있어요.
a = b + c(print or io.write)('done')
a = b + c; (print or io.write)('done')
그런데 현재의 파서(parser)는 항상 첫 번째 방식으로 해석해요. 즉 여는 괄호를 호출의 인자(arguments) 시작으로 보는 거죠. 이런 모호함을 피하려면, 괄호로 시작하는 문 앞에는 항상 세미콜론을 붙이는 게 좋은 습관이에요.
;(print or io.write)('done')
블록은 명시적으로 경계를 그어서 하나의 문으로 만들 수도 있어요.
stat ::= do block end
명시적 블록은 변수 선언의 유효 범위(scope)를 제어할 때 유용해요. 또 다른 블록의 중간에 return 문을 넣고 싶을 때도 쓰이곤 해요(§3.3.4 참고).
3.3.2 청크 (Chunks)
루아의 컴파일 단위를 **청크(chunk)**라고 불러요. 문법적으로 보면 청크는 그냥 블록 하나예요.
chunk ::= block
루아는 청크를 "가변 개수의 인자를 받는 익명 함수의 본문"으로 취급해요(§3.4.11 참고). 그래서 청크는 지역 변수를 정의하고, 인자를 받고, 값을 반환할 수 있어요. 게다가 이 익명 함수는 _ENV라는 외부 지역 변수의 유효 범위 안에서 컴파일되는데요(§2.2 참고). 결과적으로 만들어지는 함수는 그 변수를 실제로 쓰지 않더라도 항상 _ENV를 유일한 외부 변수로 가지게 돼요.
청크는 파일에 저장될 수도 있고, 호스트 프로그램 안의 문자열에 저장될 수도 있어요. 청크를 실행하려면 루아가 먼저 청크를 **로드(load)**해요. 이때 청크의 코드를 가상 머신(virtual machine)용 명령어로 미리 컴파일(precompile)하고, 그다음 가상 머신의 인터프리터로 컴파일된 코드를 실행하는 거예요.
청크는 바이너리(binary) 형태로도 미리 컴파일될 수 있어요. 자세한 내용은 luac 프로그램과 string.dump 함수를 참고하세요. 소스 형태와 컴파일된 형태의 프로그램은 서로 바꿔 쓸 수 있어요. 루아가 파일 형식을 자동으로 감지해서 그에 맞게 동작하기 때문이죠(load 참고).
3.3.3 할당 (Assignment)
루아는 **다중 할당(multiple assignment)**을 지원해요. 그래서 할당 문법은 왼쪽에 변수 목록, 오른쪽에 식 목록을 정의해요. 두 목록 안의 요소들은 쉼표로 구분돼요.
stat ::= varlist '=' explist
varlist ::= var {',' var}
explist ::= exp {',' exp}
식(expression)에 대한 설명은 §3.4에서 다뤄요.
할당이 일어나기 전에, 값의 목록은 변수 목록의 길이에 맞춰 **조정(adjust)**돼요(§3.4.12 참고).
만약 어떤 변수가 다중 할당 안에서 할당되면서 동시에 읽힌다면, 루아는 모든 읽기가 할당 이전의 변수 값을 얻게 해 줘요. 그러니까 다음 코드는
i = 3
i, a[i] = i+1, 20
a[3]을 20으로 설정해요. a[4]에는 영향을 주지 않는데요, 그 이유는 a[i] 안의 i가 할당이 일어나기 전에 3으로 평가되기 때문이에요. 비슷하게
x, y = y, x
는 x와 y의 값을 서로 바꿔 주고,
x, y, z = y, z, x
는 x, y, z의 값을 순환(cyclic)하게 회전시켜 줘요.
여기서 주의할 점이 하나 있어요. 이 보장은 할당 문 안에서 문법적으로 접근하는 경우에만 적용돼요. 할당 중에 호출된 함수나 메타메서드(metamethod)가 어떤 변수의 값을 바꾼다면, 루아는 그 접근의 순서에 대해 어떤 보장도 하지 않아요.
전역 이름에 대한 할당 x = val은 _ENV.x = val과 동등해요(§2.2 참고).
테이블 필드와 전역 변수에 대한 할당의 의미(사실 전역 변수도 테이블 필드예요)는 메타테이블(metatable)을 통해 바꿀 수 있어요(§2.4 참고).
3.3.4 제어 구조 (Control Structures)
if, while, repeat 같은 제어 구조는 우리가 평소 알고 있는 의미와 친숙한 문법을 그대로 가져요.
stat ::= while exp do block end
stat ::= repeat block until exp
stat ::= if exp then block {elseif exp then block} [else block] end
루아에는 for 문도 있는데, 두 가지 형태가 있어요(§3.3.5 참고).
제어 구조의 조건식은 어떤 값이든 반환할 수 있어요. false와 nil 둘 다 거짓으로 검사돼요. nil과 false가 아닌 모든 값은 참으로 검사되죠. 특히 숫자 0과 빈 문자열도 참으로 검사된다는 점을 기억해 두세요.
repeat–until 루프에서는 내부 블록이 until 키워드에서 끝나지 않고, 조건 다음에서야 끝나요. 그래서 조건은 루프 블록 안에서 선언된 지역 변수를 참조할 수 있어요.
goto 문은 프로그램의 제어를 라벨(label)로 옮겨 줘요. 문법적인 이유 때문에, 라벨도 루아에서는 문으로 취급돼요.
stat ::= goto Name
stat ::= label
label ::= '::' Name '::'
라벨은 정의된 블록 전체에서 보이는데, 다만 중첩된 함수 안쪽에서는 보이지 않아요. goto는 어떤 보이는 라벨이든 점프할 수 있어요. 단, 지역 변수의 유효 범위 안으로 들어가지는 못해요. 또 어떤 라벨이 보이는 자리에는 같은 이름의 라벨을 선언하면 안 돼요. 그 다른 라벨이 바깥 블록에 선언돼 있더라도 마찬가지예요.
break 문은 while, repeat, for 루프의 실행을 끝내고, 루프 다음의 문으로 건너뛰어요.
stat ::= break
break는 가장 안쪽에 있는 둘러싸는 루프를 끝내요.
return 문은 함수나 청크(익명 함수로 취급되죠)에서 값을 반환할 때 쓰여요.
함수는 값을 두 개 이상 반환할 수 있으니, return 문의 문법은 이렇게 생겼어요.
stat ::= return [explist] [';']
return 문은 블록의 마지막 문으로만 쓸 수 있어요. 블록 중간에 반환해야 한다면 명시적인 내부 블록을 쓰면 되는데요, do return end 같은 관용구(idiom)가 그 예예요. 이렇게 하면 return이 (내부) 블록의 마지막 문이 되니까 가능해지는 거죠.
3.3.5 for 문 (For Statement)
for 문에는 숫자형(numerical)과 일반형(generic) 두 가지 형태가 있어요.
숫자형 for 루프
숫자형 for 루프는 제어 변수(control variable)가 산술 수열을 따라갈 동안 코드 블록을 반복해요. 문법은 다음과 같아요.
stat ::= for Name '=' exp ',' exp [',' exp] do block end
주어진 식별자(Name)는 제어 변수를 정의해요. 이 변수는 루프 본문(block)에 지역인 새로운 변수예요.
루프는 처음에 세 개의 제어식을 한 번 평가하는 것으로 시작해요. 이 값들은 각각 초기값(initial value), 한계값(limit), **증분(step)**이라고 불러요. 증분이 없으면 기본값 1로 처리돼요.
초기값과 증분이 둘 다 정수라면, 루프는 정수로 진행돼요. 이때 한계값은 정수가 아닐 수도 있다는 점을 기억해 두세요. 그 외의 경우에는 세 값이 모두 실수(float)로 변환되고 루프도 실수로 진행돼요. 이 경우에는 부동소수점(floating-point) 정확도 문제를 조심해야 해요.
이 초기화 이후에는 루프 본문이 반복되는데, 제어 변수의 값이 초기값에서 시작해 증분을 공차로 하는 산술 수열을 따라가요. 증분이 음수면 감소하는 수열이 되고, 증분이 0이면 오류(error)를 일으켜요. 값이 한계값보다 작거나 같을 동안(음수 증분이면 크거나 같을 동안) 루프가 계속돼요. 초기값이 이미 한계값보다 크거나(음수 증분이면 작거나) 같다면, 본문은 실행되지 않아요.
정수 루프에서는 제어 변수가 절대 휘감기(wrap around)지 않아요. 대신 오버플로(overflow)가 나면 루프가 끝나요.
루프 도중에는 제어 변수의 값을 바꾸면 안 돼요. 루프 이후에도 그 값이 필요하다면, 루프를 나가기 전에 다른 변수에 할당해 두는 게 좋아요.
일반형 for 루프
일반형 for 문은 **이터레이터(iterator)**라고 불리는 함수 위에서 동작해요. 반복마다 이터레이터 함수가 호출되어 새 값을 만들어 내고, 이 새 값이 nil이 되면 멈춰요. 일반형 for 루프의 문법은 다음과 같아요.
stat ::= for namelist in explist do block end
namelist ::= Name {',' Name}
다음과 같은 for 문이
for var_1, ···, var_n in explist do body end
어떻게 동작하는지 살펴볼게요.
이름들 var_i는 루프 본문에 지역인 루프 변수들을 선언해요. 이 변수들 중 첫 번째가 제어 변수예요.
루프는 explist를 평가해 네 개의 값을 만들어 내는 것으로 시작해요. 바로 이터레이터 함수(iterator function), 상태(state), 제어 변수의 초기값, 그리고 **닫힘 값(closing value)**이에요.
그다음 반복마다 루아는 이터레이터 함수를 상태와 제어 변수 두 개의 인자로 호출해요. 이 호출의 결과는 다중 할당의 규칙을 따라(§3.3.3 참고) 루프 변수들에 할당돼요. 제어 변수가 nil이 되면 루프가 끝나요. 그렇지 않으면 본문이 실행되고 루프는 다음 반복으로 넘어가요.
닫힘 값은 닫힐 예정 변수(to-be-closed variable)처럼 동작해요(§3.3.8 참고). 루프가 끝날 때 자원을 해제하는 데 쓸 수 있는 거죠. 그 외에는 루프에 간섭하지 않아요.
루프 도중에는 제어 변수의 값을 바꾸면 안 돼요.
3.3.6 문으로서의 함수 호출 (Function Calls as Statements)
가능한 부수 효과(side-effects)를 허용하기 위해, 함수 호출을 문으로 실행할 수도 있어요.
stat ::= functioncall
이 경우 반환되는 모든 값은 버려져요. 함수 호출에 대한 자세한 설명은 §3.4.10에서 다뤄요.
3.3.7 지역 변수 선언 (Local Declarations)
지역 변수는 블록 안 어디에서든 선언할 수 있어요. 선언에는 초기화가 포함될 수도 있는데요.
stat ::= local attnamelist ['=' explist]
attnamelist ::= Name attrib {',' Name attrib}
초기화가 있으면, 그 초기 할당은 다중 할당과 같은 의미를 가져요(§3.3.3 참고). 초기화가 없으면 모든 변수는 nil로 초기화돼요.
각 변수 이름 뒤에는 속성(attribute)을 붙일 수 있어요. 속성은 꺾쇠 괄호 안의 이름이에요.
attrib ::= ['<' Name '>']
가능한 속성은 두 가지예요. const는 상수 변수를 선언하는데, 초기화 이후에는 값을 할당할 수 없는 변수를 말해요. close는 닫힐 예정 변수를 선언해요(§3.3.8 참고). 변수 목록에는 닫힐 예정 변수를 최대 한 개까지만 넣을 수 있어요.
청크도 블록이기 때문에(§3.3.2 참고), 명시적인 블록 밖, 즉 청크에서도 지역 변수를 선언할 수 있어요.
지역 변수의 가시성(visibility) 규칙은 §3.5에서 설명해요.
3.3.8 닫힐 예정 변수 (To-be-closed Variables)
닫힐 예정 변수(to-be-closed variable)는 상수 지역 변수처럼 동작해요. 다만 그 값이 변수의 유효 범위를 벗어날 때마다 **닫히(close)**게 된다는 점이 달라요. 여기에는 정상적인 블록 종료, break/goto/return으로 블록을 빠져나가는 경우, 그리고 오류로 빠져나가는 경우가 모두 포함돼요.
여기서 값을 닫는다는 것은 그 값의 __close 메타메서드를 호출한다는 뜻이에요. 메타메서드를 호출할 때 값 자체가 첫 번째 인자로 전달되고, 빠져나가게 만든 오류 객체(있으면)가 두 번째 인자로 전달돼요. 오류가 없었다면 두 번째 인자는 nil이에요.
닫힐 예정 변수에 할당되는 값은 __close 메타메서드를 가지거나, 거짓(false) 값이어야 해요. (nil과 false는 닫힐 예정 값으로 무시돼요.)
여러 닫힐 예정 변수가 같은 사건에서 유효 범위를 벗어나면, 선언된 순서의 역순으로 닫혀요.
닫는 메서드를 실행하는 도중 어떤 오류가 생기면, 그 오류는 변수가 정의된 지점의 일반 코드에서 오류가 났을 때처럼 처리돼요. 오류가 난 뒤에도, 대기 중인 나머지 닫는 메서드들은 여전히 호출돼요.
어떤 코루틴(coroutine)이 양보(yield)한 뒤 다시는 재개(resume)되지 않으면, 일부 변수는 유효 범위를 벗어나지 못할 수 있어요. 그래서 결코 닫히지 않게 되는 거죠. (이 변수들은 코루틴 안에서 만들어지고 코루틴이 양보한 지점에서 유효 범위 안에 있던 변수들이에요.) 마찬가지로 코루틴이 오류로 끝나면 그 스택을 풀지(unwind) 않아서, 어떤 변수도 닫지 않아요. 두 경우 모두 파이널라이저(finalizer)를 쓰거나 coroutine.close를 호출해서 변수를 닫을 수 있어요. 다만 코루틴이 coroutine.wrap을 통해 만들어졌다면, 그에 대응하는 함수가 오류의 경우 코루틴을 대신 닫아 줘요.
더 알아보기
- §3.4 식 (Expressions) — 할당과 반환, for 루프가 다루는 식의 의미를 이어서 알아보세요.
- §3.5 가시성 규칙 (Visibility Rules) — 지역 변수와 제어 변수의 유효 범위가 정확히 어떻게 정해지는지 다뤄요.
- §2.4 메타테이블과 메타메서드 — 할당 의미를 바꾸는
__close메타메서드의 배경이 되는 개념이에요. - §2.6 코루틴 — 닫힐 예정 변수와 얽히는 코루틴 동작을 이해하는 데 도움이 돼요.