키워드 | 용도 |
---|---|
fn |
(parenless) 함수/클로저 선언 |
if |
분기문 |
for |
반복문 |
else |
분기문의 별도 조건 혹은 for 의 정상 종료 구문 |
break |
반복문 탈출 |
return |
조기 반환 |
-
-
Save cr0sh/495cd642f123a60884afce9894c135c5 to your computer and use it in GitHub Desktop.
- 단순함: 최소한의 키워드, 최소한의 문법적 기능
- 유일한 복합 자료형은 테이블
- 대부분의 요소를 런타임 처리(
struct
정의 등) - 문법의 부족으로 boilerplate가 어느 정도 발생하는 것을 허용
- 가능한 한 필요한 추가 기능은 유연한 확장으로 대응 (
bind
, parenless fn)
- 명료함: TOOWTDI (There's Only One Way To Do It)
- 일급 시민 Rust:
extern "Rust"
와 ABI 호환성 보장- 문법의 느낌도 Rust와 최대한 비슷하게
- 안전성: sandboxing 지원, 'safe-only' implementation 제공
- 데이터와 코드의 통합: table로 모든 것을 선언
- 컴파일러를 믿어라: 최적화 힌트를 통해 단순한 언어로부터 최대한의 성능 제공
struct
힌트 등
- 모든 것이 테이블: 모듈, block(
{ /* ... */ }
), ... - Lua에서 메타메서드 차용, 그러나 별도의 메타테이블이 없으며,
"__tostring"
같은 "magic" string key 대신 atom과 유사한 전용 키 (meta.to_string
)를 사용 - 커스텀 키워드를 정의할 수 있는 parenless
fn
(my_keyword { /* ... */ }
) - 동적 언어이지만 ADT 스키마를 정의할 수 있는
struct
와enum
parenless fn - Lua의 coroutine 기반 비동기 프로그래밍이 아닌 async-await 기반 비동기 프로그래밍
- 그러나 function coloring 없이 모든 함수가 (암시적으로) async
- trait 기반 다형성 지원
- Exception 없음: 대신 에러를 직접 반환하며, Rust의
?
나 Zig의try
에 대응되는try
parenless fn을 사용return_twice
로 "최대 2회" unwind하는 것이 주된 컨셉트
keyword let
let foo = "42";
자료형은 7가지이며 type
함수로 구분 가능
type.boolean
: 진리값type.integer
: 64비트 부호 있는 정수type.float
: 64비트 배정밀도 IEEE754 포맷type.function
: 호출 가능 타입type.string
: UTF-8 문자열. Rust&str
과 ABI-compatibletype.table
: 유일한 composite type(array / hashmap에 모두 대응)type.metatype
: 유일한 metatype
type.*
은 prelude에 포함되어 있기 때문에integer
와 같이 바로 사용할 수 있음. 키워드가 아니므로 재대입되거나 쉐도잉될 수 있음. 이는error
등 다른 값들에도 적용됨 (예외로fn
은 키워드임)
let
은 쉐도잉을 함.
let x; x = 1;
과 같이 선언과 정의를 별도로 할 수 있음. 이는 재귀함수를 정의하거나 할 때 유용함.
keyword if
if x { something() } else { another_thing() }
if ... else
는 expression임.
keyword for
// iteration
for let x = { 1, 2, 3 } {
}
// infinite loop
for {
say();
}
// conditional loop: condition에 해당되는 표현식은 매 블록의 시작 전마다 재평가됨.
for should_loop() {
}
for
루프는 statement이지만 for ... else
루프는 expression임. ( for { /* ... */ }
에는 else
를 붙이지 않아도 expression임). else
에 붙은 expression은 항상 마지막에 필요할 때만(루프가 break
없이 탈출할 때만) 평가됨.
let first_even = for let x = { 1, 2, 3 } {
if x % 2 == 0 {
break Some(x);
}
} else None
기본 제공되는 이항 연산자들은 서로 다른 타입 간의 연산을 지원하지 않음. 모두 명시적으로 변환하여야 함.
integer
,float
,string
은<type>.from()
메서드를 가짐. 전술한 세 타입 간의 변환은 모두 지원하며,string.from()
은 모든 타입으로부터의 변환을 지원함.
string.from()
은 해당 타입이meta.to_string
필드를 가질 경우 이를 우선적으로 사용함.
integer
와string
간의 변환을 제외하고 모든 변환은 backwards compatibility를 보장하지 않음.
integer
혹은float
와string
간의 변환은 10진수 표현만을 지원함.
{}
let varkey = "foo"
let table1 = {
arrvalue1,
arrvalue2, // 마지막 콤마는 선택 사항으로, 한 줄 선언 시 없애고 여러 줄 선언 시 붙이는 것을 권장함.
}
let table2 = {
this: "is"
a: "table",
["complex-key"]: "not-a-complex-value"
[varkey]: varvalue // varkey가 위의 키들과 겹치는 경우 아래가 우선함
}
println(table1.0)
println(table1[1])
println(table2.this)
println(table2.a)
println(table2[varkey]) // table.foo
테이블 선언 문법은 아래의 문법 설탕이라 할 수 있음. ({}
는 데이터가 아닌 코드를 담고, 사실 둘은 구분되지 않는다!)
let table = {
self.0 = arrvalue1;
self.1 = arrvalue2;
self.this = "is";
self.a = "table";
self["complex-key"] = "not-a-complex-value";
self[varkey] = varvalue;
}
{ /* ... */ }
블록은 환경을 캡쳐함.
Lua와 동일하게, table이 우변인 대입문은 (meta.clone
이 별도로 정의되지 않는 한) shallow copy를 수행함. table이 좌/우변인 동치 비교문(==
)은 두 테이블이 shallow copy되지 않았던 한 false
로 평가됨.
존재하지 않는 테이블의 필드에 접근 시 index_error
를 반환함.
테이블의 필드를 제거하고 싶은 경우 tbl.remove(key)
를 사용함.
테이블에는 tbl.len()
메서드가 존재하며 테이블의 크기를 반환함.
테이블의 크기는 테이블이 가지고 있는 필드의 수로 정의됨. Lua의 #
연산자와 다르므로 주의를 요함.
keyword fn
(별도의 함수 선언 문법 없음)
let say_hello = fn() { println("hello"); };
위 정의는 호출에 괄호가 꼭 필요하므로 parened fn이라고 함.
Parened 함수 정의는 variadic arguments를 지원하지 않음. parenless 함수는 지원함.
함수에서 값을 조기 반환하고 싶으면 return
키워드를 사용할 수 있음. 함수에서 { /* ... */ }
블럭은 필수 사항이 아님.
let match = fn x branches { /* ... */ }
match x {
"a": fn { /* ... */ }
"b": fn { /* ... */ }
}
Parened 함수와 parenless 함수는 서로 호환되지 않음. 즉 일반 함수를 정의하고 parenless invocation을 하는 것은 허용되지 않음.
모든 statement는 끝에
;
을 붙여야 하며 마지막에 붙이지 않으면 마지막 expression을 평가한 값이 됨.
함수는 여러 값을 반환하고 호출자도 여러 값을 반환받을 수 있음.
너무 많거나 적은 값을 받으면 abort함.
parenless invocation에도 괄호를 사용할 수 있으나 함수 이름과 괄호 사이에 공백이 꼭 있어야 함. 반대로 일반 함수 호출은 공백이 있으면 안 됨.
모든 <expr> <expr> [<expr> ...];
형식의 표현식은 첫 번째 expression을 parenless 함수로 가정한 parenless 함수 호출 구문으로 해석됨.
Parenless 함수 호출 구문은 그 자체로 일종의 2-stack machine으로 변환되어, 각 parenless fn을 prefix operator로 하는 구문들로 해석됨. 해당 2-stack machine은 다음의 과정을 통해 평가가 진행됨.
- 각각 operator stack과 operand stack으로 구분하여 지칭함.
- operator는 parenless fn을 의미하며, operator의 arity는 parenless fn이 받는 파라미터의 수를 의미함.
- expression을 앞부터 순회하면서 다음을 반복
- operator를 만나는 경우 operator stack에, operand를 만나는 경우 operand stack에 push
- operator stack의 맨 위 operator의 arity보다 같거나 많은 operand stack 구성원이 있다면, operand stack의 최상위
{arity}
개 만큼을 한 번에 pop하여 operator에 제공하여 연산을 수행함. - 반환된 값(들)을 순차적으로 순회해 operator인 경우 operator stack에, operand인 경우 operand stack에 넣음.
- 종료 조건은 다음과 같음.
- operator stack이 비게 된 경우 operand stack에 남은 값이 모두 parenless 함수 호출 구문의 평가 결과가 됨.
- 1a. 이 때, 남은 expression이 있을 경우 abort함.
- operator stack이 남았으나 남은 expression이 있을 경우 abort함.
- operand stack이 비게 되었으나 operator stack이 남아있는 경우 abort함. 이는 4보다 우선함.
- 남은 expression이 없는 경우 operator stack에 남아 있는 모든 operand를 넣고 부족한 경우 abort함.
예시:
impl { sayer } struct {
name: string,
} implementation
operator | operand |
---|---|
impl (arity: 3) |
operator | operand |
---|---|
impl (arity: 3) |
{ sayer } |
operator | operand |
---|---|
struct |
|
impl (arity: 3) |
{ sayer } |
operator | operand |
---|---|
struct (arity: 2) |
{ name: string } |
impl (arity: 3) |
{ sayer } |
operator | operand |
---|---|
implementation |
|
struct (arity: 2) |
{ name: string } |
impl (arity: 3) |
{ sayer } |
operator | operand |
---|---|
implementation |
|
struct ({ name: string }) |
|
impl (arity: 3) |
{ sayer } |
operator | operand |
---|---|
impl ({sayer}) (struct ({name: string}) (implementation) |
그러므로 이 호출구문의 실행 결과는 impl ({sayer}) (struct ({name: string}) (implementation)
이다.
let hi = bind {
hello: "hello!",
} fn() {
println("hi! {}", hello)
}
hi() // "hi! hello!"
bind
는type.function
타입에 사용할 수 있으며, Javascript의 Function.prototype.bind() 와 유사하여 context를 전달하거나 method invocation에서 self를 지정하는 데 사용될 수 있다.
use
(또는 use <path> as
)
use std.prelude.println; // 사실 이미 prelude는 임포트가 되어 있음. use std.prelude._;
println("hello world! 1 + 2 = {}", 1 + 2);
임포트할 path는
use sys.path; insert(path, "<newpath>")
로 추가 가능함.#
로 시작하는 path는 빌트인으로 예약됨.
순환 임포트는 허용되지 않음.
pub let
// one.kh
pub let value = 1;
// main.kh
use one.value;
println(value); // 1
// or use one; println(one.value)
주석은 C family와 동일하게
//
와/* ... */
을 사용할 수 있음.
모든 .kl
파일은 모듈로서 작동함. 모듈울 임포트하는 쪽에서는 (익스포트된 이름과 값이 키-값 쌍으로 있는) 테이블로서 사용함.
- 모듈 파일의 첫 두 바이트는
#!
으로 시작될 수 있으며 이 경우 다음으로 만나는 newline(\n
)까지의 내용이 무시됨. (shebang) - 모듈 파일은 시작 시 '도입부'를 가지며, 도입부는 다음을 만족하는 구문 혹은 요소를 포함하는 가장 넓은 영역임.
- shebang
use
구문- 주석
- 공백
- 도입부 밖에서는
use
구문을 사용할 수 없음.
task.join(func1, func2, ...)
/ task.spawn(func1)
별도의
async fn
에 대한 구분된 정의가 없고, async 함수를 호출하는 순간 암시적으로 await하는 구조. 즉 task.spawn(func1()) 은func1()
의 결과를 성급하게 평가 한 뒤에 spawn하게 됨.
모든 Khala 실행 컨텍스트는 싱글스레드이며, 병렬화 혹은 멀티스레딩의 지원 계획은 없음. 각 스레드마다 별도의 실행 컨텍스트를 사용하는 방식을 권장함.
Lua와 비슷함. 아래의 키들을 테이블에 삽입함으로서 선언 가능
meta.call
meta.parenless_call
meta.eq
/meta.lt
/meta.le
meta.add
/meta.sub
/meta.mul
/meta.div
/meta.rem
/meta.unm
meta.clone
: 암시적 복사 생성자meta.drop
: 소멸자- Rust와 다르게 소멸시점은 정의되지 않음! GC에 의해 스코프 탈출 이후 늦게 소멸될 수도 있음.
meta.to_string
테이블에 메타테이블을 붙이는 작업은 따로 필요치 않으며 실제 테이블에 위의 키로 직접 설정하면 됨.
Khala에서는 타입 자체도 first class value로서 다룰 수 있음. 타입을 생성하는 기본적인 방법은 struct
, enum
parenless fn을 사용하는 것임.
let vector3 = struct {
x: integer,
y: string,
};
let v = vector3 {
x: 3,
y: "foo",
}
let kind = enum {
x: { integer, string },
y: {},
};
let some_x = kind.x()
let some_y = kind.y()
struct / enum parenless fn으로 생성된 타입의 값은 by-value로 복사됨. (meta.clone
이 자동 구현됨)
struct
,enum
은 모두 JIT 컴파일러에게 주어지는 최적화 힌트로서도 작용함.
let ty = struct { /* ... */ };
에 대해, ty.is()
로 어떤 변수가 ty
구조체에 해당하는 지 알 수 있음. let ty = enum { /* ... */ };
에 대해서도 동일함.
struct
,enum
에 제공되는 타입 스키마는 테이블이여야 하며, 필드에는struct
혹은 정수 인덱스만을 가지는 플레인 테이블을 넣을 수 있음.
let kind = enum {
x: { integer, string },
y: {},
}
let sayer = trait {
say: method(type.string),
whoami: static_method(),
}
method
,static_method
:todo!()
와 유사한 trait placeholder
let human = impl { sayer } struct {
name: string,
} {
say: fn(to) {
println("hello {}, my name is {}", to, self.name)
}
whoami: fn() {
// 여기서는 self를 사용할 수 없음
println("a human")
}
}
<trait>.is()
함수로 어떤 값이 trait을 구현하는지 런타임에 검사할 수 있음.
주의: trait은 duck typing을 지원하지 않음. 다시 말해 어떤 테이블에 trait의 정의와 같은 함수 필드를 붙여도
<trait>.is()
함수는true
를 반환하지 않음. 항상impl
parenless fn을 사용해야만 함.
trait <supertrait1> <supertrait2> { /* definition */ }
으로 trait 간 상속을 표현할 수 있음.
try <expression>
try
parenless fn은 전달받은 값의 필드에 error
필드가 있으면("error"
가 아님에 유의) 즉시 해당 값을 반환하고 '탈출'함.
try
가 함수로서 먼저 호출되면(try(err) <expression>
) 해당 값을 호출하여 에러 값을 전달하고 반환된 값에 error
필드가 있으면 즉시 해당 값을 반환함.
위 '탈출'을 구현하는 메인 primitive는 bail
빌트인 함수로 리턴값을 "caller의 caller"에게 전달함.
pub let try = {
[meta.call] = fn(e) {
fn result {
if result.has(error) {
bail {
[error]: e,
[error.source]: result[error],
[error.traceback]: std.debug.traceback(),
}
}
result
}
}
[meta.parenless_call] = fn result {
if result.has(error) {
bail error;
}
result
}
};
일반적인 exception과 다른 점은 언와인딩의 횟수가 최대 2회로 고정되어 있다는 것임. try-catch를 하는 대신에 fn() { /* ... */ }
으로 감싸서 간단하게 opt out할 수 있다는 장점이 있음.
try
가 두 번째 값을 가질 경우 원본 에러는 error.source
에 전달되며 추가적으로 error.traceback
필드가 생김. try
parenless fn이 오류를 전파할 때 적절히 값을 추가해 줌.
type.boolean
은bool
과 호환됨type.integer
은i64
와 호환됨type.float
은f64
와 호환됨type.function
은 Rust에서 적절한 shim을 proc-macro로 붙여서 부를 수 있음.- Rust의
fn(T1, T2, ...) -> R
은T<N>
,R
이 모두 호환된다면 Khala에서 부를 수 있음(parened 함수로서).- 특수화: Rust의
fn(T1, T2, ...) -> impl Future<Output=R>
는 비동기 함수로서 자동적으로 await 됨.
- 특수화: Rust의