🌴 카일루아
카일루아는 루아 프로그래밍 언어를 위한 실험적 타입 검사기 및 통합 개발 환경(IDE)입니다. (현재는 루아 5.1만 지원됩니다.)
이 프로젝트는 매우 실험적이며 어떤 보장이나 지원도 제공하지 않습니다!
설치와 사용
카일루아는 독립 검사기로도 쓸 수 있고 IDE 플러그인으로도 쓸 수 있습니다.
독립 검사기
독립 검사기를 설치하려면 먼저 러스트를 설치한 뒤(1.15 이상이 필요합니다), 다음을 입력합니다.
cargo install -f kailua
(-f
는 이미 설치된 검사기도 함께 업그레이드 해 줍니다.)
kailua check <검사를 시작할 파일 경로>
로 실행할 수 있습니다.
또한 kailua.json
이나 .vscode/kailua.json
이 해당 디렉토리에 있다면 kailua check <검사할 디렉토리 경로>
로 실행할 수도 있습니다. 설정 파일의 포맷은 이 문서의 뒷부분을 참고하세요.
Visual Studio Code
카일루아는 Visual Studio Code에서 IDE로 사용할 수 있습니다. 빠른 실행(Ctrl-P
)에서 ext install kailua
를 입력해서 설치합니다. 윈도 이외의 환경에서는 앞에서 설명된 대로 독립 검사기를 먼저 설치해야 합니다.
루아 코드를 포함하는 폴더를 열면 설정 파일을 찾을 수 없다는 오류가 나옵니다. 이 설정 파일은 실시간으로 검사를 수행하는 데 필요합니다.
.vscode/kailua.json
을 직접 만들어도 되고, 명령 팔레트(Ctrl-Shift-P
)에서 "Kailua"로 찾아 설정 파일을 수정할 수도 있습니다.
수동으로 편집할 경우 .vscode/kailua.json
에 다음 내용이 필요합니다.
{
"start_path": "<검사를 시작할 파일 경로>",
"preload": {
// 아래는 우리가 루아 5.1와 모든 기본 라이브러리를 사용함을 나타냅니다.
"open": ["lua51"],
},
}
설정 파일을 적용하려면 Ctrl-R
로 현재 창을 새로 로드해야 합니다.
첫 카일루아 코드
시작점을 지정했으면 첫 카일루아 코드를 작성해 보죠.
--# open lua51
print('Hello, world!')
설정 파일을 사용하고 있다면 좀 더 간단한 코드도 가능합니다.
print('Hello, world!')
이 코드를 잠시 가지고 건드려 보면서 카일루아가 어떤 오류를 잡아 낼 수 있는지 확인해 보세요.
지원되는 IDE 기능들
-
실시간 문법 체크 (모든 파일)
-
실시간 타입 체크 (주어진 시작 경로로부터만 가능)
-
이름 및 필드의 자동 완성
-
함수 서명 도움말
-
수식의 타입에 대한 정보 (마우스 커서를 위에 올렸을 경우)
-
지역 및 전역 변수의 정의로 이동하기
-
프로젝트 전체에서 지역 및 전역 변수의 이름을 바꾸기
카일루아 언어
특별한 주석
카일루아는 올바른 루아 코드의 부분집합으로 별도의 변환 작업이나 컴파일이 필요 없습니다. 추가된 것들은 특별한 주석에 쓰여 있습니다.
-
--: <타입>
은 앞에 나오는 것의 타입(들)을 지정합니다.이 주석은 새 이름이 정의될 수 있는 모든 곳에서 쓰일 수 있습니다. 여기에는
local
(개별 이름 또는 문장 뒤),function
(인자 뒤),for
(개별 이름 뒤) 및 대입문(개별 이름 또는 문장 뒤)이 포함됩니다.이름 뒤에 쓰일 경우에는, 타이핑의 편의를 위해 다음과 같이 콤마나 닫는 괄호를 주석 앞으로 옮길 수 있습니다.
function f(x, --: integer y, --: integer z) --: integer -- ... end
여러 이름을 선언하는 흔한 경우에는 문장 뒤에 여러 타입을 지정할 수 있으며, 이 경우 각 타입은 쉼표로 구분됩니다.
-
--> <타입>
은 함수의 반환 타입을 지정합니다.이 주석은 함수 인자를 닫는 괄호 뒤에서만 쓰일 수 있으며, 마지막 인자에 대응하는
--:
와-->
는 같은 줄에 쓰일 수 있습니다. -
--v function(<이름>: <타입> ...) [--> <타입>]
은 함수 타입을 지정합니다.이 주석은
function
예약어 앞에 올 수 있습니다(네, 익명 함수에도 쓰일 수 있습니다).--:
와-->
를 쓰는 것과 동일하나 훨씬 읽기 쉽습니다. 모든 이름은 대응되는 선언과 일치해야 합니다. 가변 인자는 인자 목록 맨 뒤에...: <타입>
과 같이 쓸 수 있습니다. 반환값이 여럿이면 괄호를 쳐야 합니다.기본적으로 앞의 맥락에서 분명하지 않은 한 모든 함수는
--v
나--:/-->
를 써서 타입이 지정되어야 합니다. 따라서f(function(a, b) ... end)
와 같은 코드는 허용되지만,f
가 그러한 함수를 받는다고 알려져 있을 때만 가능합니다. -
--v method(<이름>: <타입> ...) [--> <타입>]
은 메소드 타입을 지정합니다.function
과 동일하나function A:b(...)
같은 선언에 씁니다. 카일루아는self
의 타입을 추론하려 하며, 그게 불가능할 경우function A.b(self, ...)
와--v function(...)
으로 명시적인 타입을 지정해야 합니다. -
--# ...
은 타입 검사기에게 내리는 특별한 명령입니다.가장 중요한 명령으로는
--# open <내장 라이브러리 이름>
이 있는데, 이는 대응되는 내장된 이름들을 읽어 들이면서 앞으로 어떤 언어 변종을 쓸지를 결정합니다. 현재 지원되는 유일한 내장 라이브러리는lua51
(무수정 루아 5.1) 뿐입니다. 시작점이 되는 파일의 주석이 아닌 첫 줄에 이 명령을 두는 게 좋습니다.--# type [local | global] <이름> = <타입>
은 타입 별명을 짓는데 쓰입니다. 세 종류의 타입 별명이 있습니다.local
은 (local
문장 같이) 새 지역 이름을 만들고,global
은 (A = ...
같이) 전역 이름을 만들며, 아무 것도 없을 경우 타입이 현재 파일로부터 내보내져서,require
를 할 때 그 위치에서 지역 이름으로 쓸 수 있게 됨을 뜻합니다. 최상위 영역이 아닌 위치에서는 지역 타입만 만들 수 있습니다. 변수 이름과는 달리, 안쪽에 있는 타입 이름이 바깥의 이름을 덮어 씌울 수는 없습니다.--# assume [global] <이름>: <타입>
은 주어진 이름의 타입을 덮어 씌웁니다.global
예약어가 있으면 전역 이름을 가리키고, 아니면local
처럼 새 지역 이름이 생깁니다. 검사기를 통과할 수 없는 경우를 해소하는 데 쓸 수 있지만 매우 위험하므로, 조심해서 쓰십시오.추후에 다른 명령들이 추가될 수 있습니다.
같은 종류의 특별한 주석들은 여러 줄로 나눠 쓸 수 있습니다.
--# type Date = {
--# hour: integer;
--# min: integer;
--# sec: integer;
--# }
타입
다음 기본 타입들이 인식됩니다.
-
nil
,boolean
(또는bool
),number
,string
,function
,userdata
,thread
,table
은 모두 기본 루아 타입을 가리킵니다. -
integer
(또는int
)는number
이면서 검사 시간에 정수라고 판단할 수 있는 부분집합입니다. (나중에 루아 5.3 이상 지원이 들어갈 경우 기본 타입으로도 쓰일 예정입니다.) -
true
나false
, 정수, 그리고 문자열 리터럴은 각각boolean
,integer
및string
의 서브타입입니다. -
테이블 타입은 네 종류의 유용한 경우로 나뉩니다.
중요한 사항으로, 앞의 두 경우는 자동으로 추론되지 않기 때문에
local tab = {} --: vector<integer>
처럼 명시적으로 타입을 지정해야 합니다.-
vector<T>
는 연속된 정수 키를 가지는 테이블 타입입니다. -
map<Key, Value>
는 같은 키 타입과 값 타입을 가지는 테이블 타입입니다. -
{ key1: T1, key2: T2 }
는 모든 키가 문자열이고 검사 시간에 알 수 있는 레코드입니다. 쉼표 대신에 세미콜론을 쓸 수 있습니다.명시적으로 선언된 레코드는 기본적으로 "확장될 수 없으며", 이는 필드 목록이 완전하고 더 이상 수정될 수 없음을 뜻합니다. 필드 목록 뒤에
...
를 써서 레코드를 확장 가능하게 만들면table.field = 'string'
과 같이 레코드를 느긋하게 초기화할 수 있습니다. 반대로 일반적인 루아 테이블은 암묵적으로 확장 가능한 레코드 타입을 가지며, 필요할 때만 확장 불가능하게 바뀝니다. -
{ T1, T2, T3 }
은 모든 키가 연속된 정수인 튜플입니다. 이것만 빼면 레코드와 유사합니다.
-
-
function(Arg, ...)
나function(Arg, ...) --> Ret
는 함수 타입입니다. 반환 타입Ret
은 여러 타입일 수 있으며, 이 경우 괄호로 감싸야 합니다(function(vector<T>, integer) --> (integer, string)
). -
T | T | ...
는 합(union) 타입입니다. 이 타입은 여러 리터럴 중 하나일 수 있는 타입에 유용합니다(예:"read" | "write" | "execute"
). 다른 종류의 합 타입도 가능하나, 카일루아에서 이들 타입의 검사는 거의 지원되지 않습니다. -
any
에는 어떤 타입 정보도 없으며, 유용하게 쓰려면--# assume
명령이 필수적입니다. -
WHATEVER
(대문자 주의)는 타입 검사기가 항상 허용하는 구멍입니다.map<integer, WHATEVER>
와map<WHATEVER, string>
은 호환되지만,map<integer, WHATEVER>
와map<string, string>
은 호환되지 않습니다. 타입 검사의 기본을 뒤흔드는 타입이므로 조심해서 쓰십시오.
카일루아 타입은 기본적으로 nil
검사를 하지 않습니다. 즉, integer
에 nil
을 대입할 수 있는데 integer
두 개를 더하는 것도 가능하며, 따라서 올바른 카일루아 코드도 실행 시간에 오류가 날 수 있습니다. 이 결정은 원래 언어를 고치지 않으면서 실용적인 타입 검사기를 만드는 데 필요했습니다.
만약 명시적으로 쓰길 원한다면 다른 두 개의 nil
검사 모드를 쓸 수 있습니다. 이 타입들은 (바로는 아니지만) 서로 자유롭게 대입이 가능하므로, 기계가 읽을 수 있는 문서라고 생각하시길 바랍니다.
-
T?
는nil
을 대입할 수 있지만 자신이nil
을 가질 수 있다는 걸 알고 있는 타입입니다. 따라서integer?
두 개는 더할 수 없습니다. 또한 생략 가능한 필드나 인자를 지정하려면 무조건 이 타입을 써야 합니다.{a: integer?, b: integer}
타입은{a = 42, b = 54}
나{b = 54}
를 담을 수 있지만,{a = 42}
는 안됩니다. -
T!
는nil
이 들어갈 수 없다는 걸 보장합니다.
당연한 이유로, 테이블의 값은 항상 T
또는 T?
가 됩니다.
마지막으로, 이름이나 테이블 값에 해당하는 타입 앞에는 const
가 붙을 수 있습니다. const
타입의 내부는 변경할 수 없습니다(예: map<integer, const vector<string>>
). 하지만 const
타입에 대입하는 건 가능합니다(아니면 쓸모가 없겠지요).
타입 검사기를 피하기
모든 곳에 타입을 다는 것이 실용적이진 않으므로, 카일루아는 지역적으로 타입 검사를 피하는 두 가지 방법을 제공합니다.
-
--v [NO_CHECK] function(...)
은 뒤따르는 함수의 타입 검사를 비활성화합니다.카일루아가 주어진 함수 타입을 믿어야 하므로, 해당 타입은 생략될 수 없습니다.
-
무슨 파일이 검사되는지를
.kailua
파일로 덮어 씌울 수 있습니다.require()
가 검사 시간에 확인되는 문자열로 호출될 경우 카일루아는package.path
와package.cpath
에 설정된 값을 사용합니다.package.path
의 경우 파일F
를 읽기 전에F.kailua
를 먼저 읽어 봅니다.package.cpath
의 경우 파일F
는 아마 실행 파일일테니F.kailua
만 읽습니다. (검색 경로에?
라고 써 놓은 게 아닌 이상 이런 파일들에는 두 개의 확장자.lua.kailua
가 붙게 됩니다.).kailua
파일에는 원래 대응되는 코드가 주어진 타입을 가지고 있다고 가정하기 위해--# assume
명령을 많이 쓰게 됩니다.
설정 포맷
카일루아의 정확한 동작은 kailua.json
파일에 옵션으로 설정할 수 있습니다. 이 파일은 JSON 파일이지만 편의를 위해 주석(//
)을 지원하고, 배열과 오브젝트 맨 뒤에 쉼표가 따라 붙을 수 있습니다:
{
// 어디서 검사를 시작할 지 나타냅니다. 생략될 수 없습니다.
//
// 하나의 문자열이나 문자열 배열이 될 수 있습니다. 배열일 경우, 여러 시작 경로들에서
// 각각 (하지만 가능할 경우 병렬로) 검사가 진행됩니다. 각 검사 세션은 다른 세션과
// 독립적이지만 오류 등은 병합되어 보고됩니다.
"start_path": ["entrypoint.lua", "lib/my_awesome_lib.lua"],
// `package.path`와 `package.cpath` 변수의 값을 나타냅니다.
// 이 경로는 항상 기준 디렉토리(`.vscode`나 `kailua.json`을 담는 디렉토리)에 상대적입니다.
// 정확한 포맷은 루아 설명서를 참고하세요.
//
// 설정 파일에서는 `{start_dir}` 문자열을 쓰면 *현재* 시작 경로를 담은 디렉토리로
// 치환됩니다. 따라서 만약 시작 경로가 `foo/a.lua`와 `bar/b.lua` 두 개라면,
// `{start_dir}/?.lua`는 각 시작 경로에 대해 `foo/?.lua`와 `bar/?.lua`로 확장됩니다.
// 이 기능은 여러 프로젝트를 각자의 디렉토리에 넣고 일부 공통되는 파일만
// 공유하고 싶을 때 유용합니다.
//
// 만약 여기서 명시적으로 설정되지 않았을 경우, 이들 설정은 `package.path`와
// `package.cpath`에 설정되는 값으로부터 추론됩니다. 이 동작은 스크립트에서는
// 편리할 수 있으나 라이브러리와 같이 다른 경우 꽤 귀찮을 것입니다.
//
// 또한 카일루아는 `package_cpath`에 있는 어떤 경로도 읽지 않음에 유의하세요.
// 해당 경로에 대응되는 `.kailua` 파일만 읽게 됩니다.
"package_path": "?.lua;contrib/?.lua",
"package_cpath": "native/?",
// 검사 전에 검사 환경을 초기화하기 위한 옵션들입니다.
// 각 옵션은 아래 나와 있는 순서대로 실행되고, 배열 안에서는 주어진 순서대로 실행됩니다.
"preload": {
// `--# open` 인자들의 목록.
"open": ["lua51"],
// `require()` 인자들의 목록. `package_*` 옵션의 영향을 받습니다.
"require": ["depA", "depB.core"],
},
}