🌴 카일루아

카일루아루아 프로그래밍 언어를 위한 실험적 타입 검사기 및 통합 개발 환경(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"],
    },
}

클래스 시스템

루아는 일반적으로 클래스를 포함한 객체지향 기능을 직접 지원하지는 않습니다. 대신 루아는 클래스 시스템을 설계하는 어려운 일을 사용자(또는 라이브러리 저자)에게 맡깁니다. 덕분에 다양한 클래스 시스템이 널리 쓰이고 있으며, 카일루아가 뭘 지원하든 이 모든 클래스 시스템의 기능을 제공해야만 합니다.

이는 불가능에 가깝기 때문에, 카일루아는 사용자 지정 가능한 클래스 시스템을 지원합니다. 현재 이 기능은 초기 개발 단계이며 모든 가능한 기능이 구현되어 있지 않은데, 보통 어떤 기능이 사용되는지 분석을 하지 못 하기 때문에 그렇습니다. 다른 클래스 시스템의 분석 및 제안은 언제나 환영합니다.

클래스 시스템의 선언

현재 세션에서 사용 가능한 클래스 시스템은 고유한 전역 이름으로 구분되며, 명시적으로 선언되어야 합니다.

--# class system gideros

현재 각 클래스 시스템의 이름은 고정이며 다음 목록에서 골라야 합니다.

이름 설명 상속 지원 [make_class]
gideros 기데로스 클래스 시스템 단일 지원

클래스의 선언

클래스는 두 가지 방법으로 선언할 수 있습니다.

  1. --# assume class 명령은 이미 존재하는 클래스를 선언 및 가정(assume)할 수 있습니다.
  2. 클래스 시스템이 지원할 경우, [make_class(<class system>)] 속성을 가진 함수가 호출될 때마다 새 클래스가 생성됩니다.

--# assume class 명령

--# assume class 명령은 클래스 시스템에 소속되지 않은 클래스를 만들 수 있습니다. 이는 상속을 쓰지 않고 특별한 의미론이 붙지 않은 간단한 클래스를 선언하는 데 유용합니다.

-- 전역 변수 `Hello`를 새로 선언된 전역 클래스 `Hello`의
-- 프로토타입으로 선언함

--# assume global class Hello

이는 단순한 명령이기 때문에(즉 루아는 무시할 것이기 때문에), global이 없이 선언된 지역 클래스는 해당 이름이 이미 지역 변수여야만 합니다.

local Hello = {}
--# assume class Hello

클래스 시스템이 있다면 괄호로 묶여야 합니다. 클래스 시스템이 상속을 지원할 경우 부모 클래스도 지정할 수 있습니다.

--# assume global class(gideros) Object
--# assume global class(gideros) Sprite: Object

일반적으로 서로 다른 클래스 시스템, 그리고 클래스 시스템에 소속된 클래스와 그렇지 않은 클래스는 서로 상호작용할 수 없습니다.

[make_class] 속성

[make_class] 속성이 붙은 함수는 일반적인 루아 코드가 새 클래스를 만드는 데 쓰는 방법과 비슷합니다. 이 속성은 클래스 시스템에 소속된 클래스에만 쓸 수 있으며, 다음과 같이 --# assume 되거나...

--# assume global `class`: [make_class(gideros)] function() --> table

또는 함수 명세와 함께 명시적으로 선언될 수 있습니다:

--v [NO_CHECK] -- 클래스의 내부 구현은 체크하기 어려우므로
--v [make_class(gideros)]
--v function() --> table
function make_class()
    -- ...
end

많은 경우 이 함수를 인자 없이 호출하면 명시적인 부모 클래스가 없는 새 클래스를 선언하게 되고, 부모 클래스 프로토타입을 인자로 넘겨 주면 상속으로 처리됩니다. 정확한 동작과 인자들의 해석은 물론 클래스 시스템마다 다를 수 있습니다.

[make_class] 함수가 반환한 새 클래스 프로토타입은 최대한 빨리 변수에 대입되어야 합니다. 이 때가 바로 클래스에 이름이 붙는 때입니다:

local Hello = class() -- 지역 타입 `Hello`를 함께 정의

Sprite = class() -- 전역 타입 `Sprite`를 함께 정의

몇 가지 꼼수로 이름이 없는 클래스를 사용할 수는 있지만 권장하지는 않습니다. 이러한 클래스 또한 오류 메시지 등에서는 유일하게 지칭됩니다.

필드와 메소드의 선언

일단 클래스가 선언되면, 클래스 시스템의 자체적인 제한에 걸리지 않는 한 필드와 메소드를 자유롭게 추가할 수 있습니다:

--# assume global class Person

--v function(name: string) --> Person
function Person.new(name)
    local person = {}
    set_metaclass_for_person(person) -- 이 부분은 여러분에게 맡깁니다
    --# assume person: Person

    person.name = name -- 새 필드를 정의
    return person
end

-- 새 클래스 필드를 선언
Person.HELLO = 'Hello'

-- 새 메소드를 선언 (메소드는 사실 `self`를 받는 함수가 들어간 클래스 필드이므로)
--v method()
function Person:greet()
    print(self.HELLO .. ', ' .. self.name)
end

local person = Person.new('J. Random Hacker')
person:greet()

여기서 볼 수 있듯,

  • 클래스 시스템이 없을 경우 새 인스턴스 타입을 만들려면 무조건 --# assume이 필요합니다. 한편 클래스 시스템은 보통 생성자로부터 new 메소드 같은 걸 자동으로 만드는 기능이 있을 겁니다.

  • 인스턴스 필드는 대입문으로 만들 수 있습니다. 하지만 없는 필드를 읽는 건 여전히 오류이므로, 만약 위에서 name 필드를 설정한 뒤에 person 변수의 타입을 바꿔 치웠다면 타입 체커는 name 필드의 존재를 알 수 없을 것입니다.

  • 메소드는 함수와 같은 문법으로 선언할 수 있지만 function 대신 method 예약어를 쓰고 self 인자는 생략(되며 자동으로 추론)됩니다.

--# assume은 클래스에도 쓸 수 있습니다. 만약 이미 존재하는 클래스에 타입만 붙이는 거라면 위의 코드는 다음과 같이 다시 쓸 수 있습니다:

--# assume global class Person
--# assume static Person.new: function(name: string) --> Person
--# assume static Person.HELLO: string
--# assume Person.greet: method()

local person = Person.new('J. Random Hacker')
person:greet()

이는 보통의 --# assume과 같으나 몇 가지 차이점에 주의하세요:

  • Person.greet는 기술적으로는 Person을 첫 인자로 가지는 static 함수여야 할 겁니다. method 예약어는 편의를 위해서 제공됩니다. (method로 시작하는 타입은 없습니다.)

  • 전역에 선언된 클래스의 필드는 최상위 블록에서만 --# assume 할 수 있습니다. 이는 --# assume은 일반적으로 현재의 지역 스코프에 해당 이름의 복사본을 생성하는데, 새 필드를 만드는 건 클래스에 전역적으로 영향을 주기 때문입니다.

  • 클래스 시스템에 따라서는 어떤 필드는 아예 정의를 할 수 없을 수도 있습니다.

기데로스 지원

gideros 클래스 시스템은 기데로스 클래스의 동작을 모방합니다. 이 클래스 시스템은, 물론 이미 필요했다는 사실은 차치하고, 너무 단순하게 설계되어 타입 체킹에서 문제가 될 수 있는 흔한 예제라 선택되었습니다.

다음은 gideros 클래스 시스템에서 흔히 쓰이게 될 초기 선언입니다.

--# class system gideros
--# assume global class(gideros) Object
--# assume global Core: {}
--# assume Core.class: [make_class(gideros)] function(parent: table?) --> table

이 명령들은 Object 최상위 클래스와, 클래스를 생성해 내는 Core.class 함수를 선언합니다.

생성자

init 메소드가 선언되면 자동으로 new 메소드도 함께 생성됩니다.

--# assume global class(gideros) Foo: Object

--v method(text: string)
function Foo:init(text)
    self.text = text
end

local x = Foo.new('string')

참고로, 현재 실제로 new 메소드가 생성되는 시점은 new가 처음으로 불렸을 때입니다. 따라서 오류가 init의 선언보다 늦어져서 사용하는 위치에서 날 수 있습니다.

상속

기데로스 클래스 시스템은 단일 상속을 지원하며, 이전 장에서 설명한 일반적인 문법과 동작을 따릅니다.

자식 클래스에서 선언된 필드는 부모 클래스에서 이미 선언된 필드를 단순히 감추게 되는데, 이를 제약하지 않으면 서브타이핑(부모 클래스를 예상하는 곳에 자식 클래스를 쓸 수 있는 기능)이 깨지게 됩니다. 따라서 카일루아에서 필드들은 일반적으로 오버라이딩할 수 없습니다. 다만 생성자(init)에 한해서 오버라이딩이 가능한데, 대신 생성자는 인스턴스를 통해서는 접근할 수 없습니다.

기데로스에서 모든 클래스는 Object 최상위 클래스의 자식으로 가정됩니다. 카일루아는 부모 클래스 없이 선언된 첫번째 (그리고 마지막) 클래스를 인식하며 그러한 클래스가 여럿 생기는 걸 금지합니다. Core.class 함수는 부모 클래스가 없을 경우 Object를 대신 쓸 것입니다. 하지만 이런 경우가 아니라면 암묵적인 동작이 혼란스럽기 때문에, --# assume class의 경우 부모 클래스가 Object더라도 무조건 명시적으로 제시해야 합니다.

카일루아의 내부 동작

카일루아는 Rust 애플리케이션입니다. 당분간은 docs.rs에 있는 생성된 문서를 사용해 주세요:

라이선스

Copyright © 2015–2017 Nexon Corporation. All rights reserved.

카일루아는 MIT 라이선스아파치 라이선스 2.0으로 라이선스되어 있으며 둘 중 하나를 선택할 수 있습니다. 카일루아에 기여하면 기여된 내용이 이 두 라이선스로 라이선스된다는 데 동의하게 됩니다.

카일루아의 원 개발자는 데브캣 스튜디오강 성훈이며 프로젝트 메인테이너로 남아 있습니다.