🌴 카일루아

카일루아루아 프로그래밍 언어를 위한 실험적 타입 검사기 및 통합 개발 환경(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 이상 지원이 들어갈 경우 기본 타입으로도 쓰일 예정입니다.)

  • truefalse, 정수, 그리고 문자열 리터럴은 각각 boolean, integerstring의 서브타입입니다.

  • 테이블 타입은 네 종류의 유용한 경우로 나뉩니다.

    중요한 사항으로, 앞의 두 경우는 자동으로 추론되지 않기 때문에 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 검사를 하지 않습니다. 즉, integernil을 대입할 수 있는데 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.pathpackage.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"],
    },
}