diff --git a/docs/grammar.ebnf b/docs/grammar.ebnf new file mode 100644 index 0000000..f3c3591 --- /dev/null +++ b/docs/grammar.ebnf @@ -0,0 +1,181 @@ +(* coollang v0 문법 — EBNF + * + * 표기 규약: + * = 정의 + * | 선택 + * [ ] 선택적 (0 또는 1) + * { } 반복 (0 이상) + * ( ) 묶음 + * " " 단말 + * (* *) 주석 + * + * 설계 제약: LL(1). backtracking 없음, 렉서 피드백 없음. + * 어떤 프로덕션도 무한 선읽기를 요구해서는 안 된다 (철학 2). + *) + +(* ------------------------------------------------------------------ *) +(* 어휘 *) +(* ------------------------------------------------------------------ *) + +(* 주석은 // 부터 줄 끝까지. 블록 주석 없음 (중첩 규칙이라는 변종을 만들지 않는다) *) + +(* 문 구분자 NEWLINE은 렉서가 삽입한다: + * 줄의 마지막 토큰이 ident, 리터럴, ")", "]", "}", "?", "return" 중 하나이면 + * 그 줄 끝에 NEWLINE 토큰을 넣는다. 그 외에는 넣지 않는다. + * 따라서 연산자나 여는 괄호로 끝나는 줄은 다음 줄로 이어진다. + * 빈 줄과 주석만 있는 줄은 NEWLINE을 만들지 않는다. + *) + +ident = letter , { letter | digit | "_" } ; +int_lit = digit , { digit | "_" } ; +string_lit = '"' , { char - '"' } , '"' ; +bool_lit = "true" | "false" ; +literal = int_lit | string_lit | bool_lit ; + +(* ------------------------------------------------------------------ *) +(* 모듈 *) +(* ------------------------------------------------------------------ *) + +module = { NEWLINE } , { item } ; +item = ( import | reexport | decl ) , NEWLINE ; + +import = "import" , string_lit , "as" , ident ; +reexport = "reexport" , ident ; + +decl = [ "pub" ] , ( fn_decl | struct_decl | enum_decl + | capability_decl | const_decl ) ; + +(* ------------------------------------------------------------------ *) +(* 선언 *) +(* ------------------------------------------------------------------ *) + +fn_decl = "fn" , ident , [ gen_params ] , "(" , [ params ] , ")" , + [ eff_result ] , [ "->" , type ] , [ block ] ; +(* block이 없으면 시그니처 선언. interface 파일과 capability 본문에서 쓴다 *) + +struct_decl = [ "copyable" ] , "struct" , ident , [ gen_params ] , + "{" , { field } , "}" ; +field = ident , ":" , type , "," , { NEWLINE } ; + +enum_decl = "enum" , ident , [ gen_params ] , "{" , { variant } , "}" ; +variant = ident , [ "(" , type_list , ")" ] , "," , { NEWLINE } ; + +capability_decl = "capability" , ident , "{" , { cap_method } , "}" ; +cap_method = "fn" , ident , "(" , [ params ] , ")" , + [ eff_result ] , [ "->" , type ] , NEWLINE ; + +const_decl = "const" , ident , ":" , type , "=" , expr ; + +gen_params = "[" , gen_param , { "," , gen_param } , [ "," ] , "]" ; +gen_param = ident , [ ":" , "effects" ] ; +(* ident 단독 = 타입 파라미터, ": effects" = effect 파라미터 *) + +params = param , { "," , param } , [ "," ] ; +param = [ "own" ] , [ "mut" ] , ident , ":" , type ; +(* 무표기 = use(빌림). own만이 소유 이전을 뜻한다. + * 수식어 순서 고정: 바인딩 수식어(own, mut)가 먼저, 타입 수식어(affine)는 type 안 *) + +(* ------------------------------------------------------------------ *) +(* effect 절 *) +(* ------------------------------------------------------------------ *) + +(* 결과 위치 — 함수 선언 자신의 effect. 합집합 허용 *) +eff_result = "effects" , eff_union ; +eff_union = eff_atom , { "|" , eff_atom } ; + +(* 파라미터 위치 — 함수 타입 안의 effect. 합집합이 문법에 없다. + * "검사기가 거부"가 아니라 "그런 문장이 존재하지 않음"이다 *) +eff_param = "effects" , eff_atom ; + +eff_atom = ident | eff_set ; +eff_set = "{" , [ eff_name , { "," , eff_name } , [ "," ] ] , "}" ; +eff_name = ident , "." , ident ; +(* 타입 수준 이름만. capability 값의 identity는 정적 층에 등장하지 않는다 *) + +(* ------------------------------------------------------------------ *) +(* 타입 *) +(* ------------------------------------------------------------------ *) + +type = fn_type | named_type ; + +fn_type = [ "affine" ] , "fn" , "(" , [ type_list ] , ")" , + [ eff_param ] , [ "->" , type ] ; + +named_type = ident , [ type_args ] ; +type_args = "[" , type , { "," , type } , [ "," ] , "]" ; +type_list = type , { "," , type } , [ "," ] ; + +(* ------------------------------------------------------------------ *) +(* 문과 블록 *) +(* ------------------------------------------------------------------ *) + +block = "{" , { NEWLINE } , { stmt } , "}" ; +stmt = ( let_stmt | return_stmt | assign_stmt | expr ) , NEWLINE ; + +let_stmt = "let" , [ "mut" ] , pattern , [ ":" , type ] , "=" , expr ; +return_stmt = "return" , [ expr ] ; +assign_stmt = place , "=" , expr ; +place = ident , { "." , ident } ; + +(* 블록의 값 = 마지막 stmt가 expr이면 그 값, 아니면 Unit. + * return은 조기 탈출 전용이며, 꼬리 위치의 return은 formatter가 지적한다 *) + +(* ------------------------------------------------------------------ *) +(* 식 *) +(* ------------------------------------------------------------------ *) + +expr = or_expr ; +or_expr = and_expr , { "||" , and_expr } ; +and_expr = cmp_expr , { "&&" , cmp_expr } ; +cmp_expr = add_expr , [ cmp_op , add_expr ] ; +cmp_op = "==" | "!=" | "<" | "<=" | ">" | ">=" ; +add_expr = mul_expr , { ( "+" | "-" ) , mul_expr } ; +mul_expr = unary , { ( "*" | "/" | "%" ) , unary } ; +unary = [ "!" | "-" ] , postfix ; + +postfix = primary , { call_sfx | field_sfx | inst_sfx | "?" } ; +call_sfx = "(" , [ args ] , ")" ; +field_sfx = "." , ident ; +inst_sfx = type_args ; +(* 후위 "[" = 명시적 인스턴스화, 전위 "[" = 리스트 리터럴. 위치가 결정한다 *) +args = expr , { "," , expr } , [ "," ] ; + +primary = literal + | ident + | list_lit + | struct_lit + | closure + | if_expr + | match_expr + | scope_expr + | "(" , expr , ")" ; + +list_lit = "[" , [ expr , { "," , expr } , [ "," ] ] , "]" ; +(* 빈 리스트는 타입 주석이 필요하다: let xs: List[Int] = [] *) + +struct_lit = ident , "{" , { field_init } , "}" ; +field_init = ident , ":" , expr , "," , { NEWLINE } ; + +closure = "fn" , "(" , [ cl_params ] , ")" , + [ eff_param ] , [ "->" , type ] , block ; +cl_params = cl_param , { "," , cl_param } , [ "," ] ; +cl_param = ident , [ ":" , type ] ; +(* 파라미터 타입 생략 가능. 호출 지점의 기대 타입에서 읽어온다 — 함수 로컬이다. + * 기대 타입이 없는 자리에서 생략하면 error *) + +if_expr = "if" , expr_ns , block , [ "else" , ( block | if_expr ) ] ; +match_expr = "match" , expr_ns , "{" , { arm } , "}" ; +arm = pattern , "=>" , ( expr | block ) , "," , { NEWLINE } ; +scope_expr = "scope" , ident , block ; + +(* expr_ns = struct_lit로 시작하지 않는 expr. + * if/match/scope의 머리 자리에서 "{"가 블록의 시작인지 struct 리터럴인지 + * 갈리지 않으므로, 그 자리의 struct 리터럴은 괄호로 감싼다 *) + +(* ------------------------------------------------------------------ *) +(* 패턴 *) +(* ------------------------------------------------------------------ *) + +pattern = "_" | literal | ctor_pattern | ident ; +ctor_pattern = ident , "(" , pattern , { "," , pattern } , [ "," ] , ")" ; +(* 가드 없음. 중첩은 제한 없음 (exhaustiveness 알고리즘이 처리한다) *) diff --git a/docs/thesis.md b/docs/thesis.md index 39f6aa7..0083f24 100644 --- a/docs/thesis.md +++ b/docs/thesis.md @@ -66,15 +66,18 @@ Capability 전달 (Alias/Move 모델의 제3의 규칙): 죽이므로 기각. 별도의 nonescaping 개념을 만들지 않고 use의 전염으로 정의해 "한 개념 한 방식"을 지킨다 (Swift @noescape, second-class values의 검증된 경로) - 소유권 이전이 필요할 때만 명시적 move -- 파라미터 위치의 callable은 기본이 use(비탈출)이고, 저장하는 쪽이 own fn으로 - 유표기한다. 파라미터 밖(반환 타입, struct 필드, channel 원소)의 callable은 - use가 애초에 불가능한 위치이므로 무표기로 항상 owned 의미다 +- 파라미터 위치는 전부 기본이 use(빌림)이고, 소유 이전만 own으로 유표기한다. + capability든 callable이든 일반 affine 값이든 규칙은 하나다 ※ 무표기-흔함 / 유표기-위험은 이미 언어의 다른 곳에 있는 규칙이다 (effect 없는 함수가 무표기, effect 있는 쪽이 effects 유표기). 새 조항이 아니라 같은 원칙의 적용이다 ※ 반대로 두면 클로저를 저장하지 않는 고차 함수 — 즉 표준 라이브러리의 거의 전부 — - 가 use 표기로 도배된다. 위험한 쪽(클로저를 오래 보관하는 sink)에 표기가 가야 + 가 표기로 도배된다. 위험한 쪽(소유를 가져가는 쪽)에 표기가 가야 리뷰어의 눈이 그리로 간다 + ※ capability를 별도 규칙으로 두지 않는 이유: capability의 존재는 이미 타입과 + effects 절에 드러난다. 표기가 실어야 할 정보는 "소유가 옮겨가는가"뿐이다 +- 파라미터 밖(반환 타입, struct 필드, channel 원소)은 use가 애초에 불가능한 + 위치이므로 무표기로 항상 owned 의미다 - capability가 셋 이상 필요하면 capability struct로 접는다 (표준 관용구): struct Deps { pay: PaymentGateway, log: Logger, db: Database } use deps: Deps로 받으면 affinity 전이와 use 규칙이 그대로 적용되므로 @@ -223,6 +226,28 @@ Generics (철학 2에서 파생): - coollang 단독 표기 (스탠퍼드 Cool과 구별, golang 방식 검색성 확보) - .cool (충돌 제로, 확장자 생략은 toolchain이 허용) +표기 (철학 2,5에서 파생 — 문법 상세는 docs/grammar.ebnf): +- 제네릭 인자는 대괄호 하나로 통일: List[a], map[Int, String](xs, f) + ※ <>는 식 위치에서 비교 연산자와 갈리지 않아 turbofish 같은 제2 표기를 부른다. + 대괄호는 전위=리스트 리터럴 / 후위=인스턴스화로 위치가 결정하므로 LL(1)이고 + 표기가 하나로 끝난다. 대가로 인덱싱 연산자를 두지 않는다 (List.at을 쓴다) + ※ effect 규칙이 "합집합 위치에서는 명시적 인스턴스화 요구"이므로 식 위치의 + 인스턴스화 문법은 반드시 존재해야 한다. 추론으로 회피할 수 없다 +- 수식어 순서: 바인딩 수식어(own, mut)가 먼저, 타입 수식어(affine)가 뒤: + own affine fn(...) effects e -> b +- 문 구분은 줄바꿈이다 (Go식 자동 삽입). 공식 formatter가 하나뿐이라 함정이 봉쇄된다 +- 블록은 식이다. 마지막 식이 블록의 값이고 return은 조기 탈출 전용 + ※ if와 match가 식이면 let x = if c { a } else { b }를 재대입 없이 쓸 수 있다. + mutable 변수와 덮어쓰기가 줄면 move 검사가 그만큼 단순해진다 — 기준은 검증기다 +- match 가드는 v0에 없다 + ※ 가드가 붙는 순간 그 분기가 패턴 전체를 덮는다고 말할 수 없어 exhaustiveness가 + 흐려지고, SMT 없이는 보수적으로 _ 분기를 강요하게 된다. 철학 1의 대표 항목을 + 문법 편의와 바꾸지 않는다. 필요하면 분기 본문에서 if를 쓴다 +- ?는 Result 전용으로 하드코딩한다 (trait solver가 없으므로 일반화 경로가 없다) + ※ 조기 탈출도 블록 종료이므로 scope의 join/cancel은 ? 경로에서도 실행된다 + ※ v1에서 linear를 넣으면 ?의 조기 탈출 경로마다 해제가 필요해진다. + 이것이 암묵 drop 문제와 만나는 지점이다 — linear 설계는 이 제약을 안고 시작한다 + ■ v0 — "기능의 서브셋, 아키텍처의 풀셋" 목표: 시스템 속성(빠른 검증 루프)은 측정으로만 증명된다. 성공 기준(숫자): 10만 줄 규모에서 함수 수정 시 check 50ms 이내. diff --git a/samples/01_capability_signature.cool b/samples/01_capability_signature.cool index ef1c0c2..ed204c9 100644 --- a/samples/01_capability_signature.cool +++ b/samples/01_capability_signature.cool @@ -1,30 +1,30 @@ -// 01. use 파라미터 + effects 절이 한 시그니처에 동시에 붙는 모양 +// 01. capability 파라미터와 effects 절 // // 어순: fn 이름(파라미터) effects {...} -> 반환타입 -// 검증 대상: capability를 받는 평범한 함수가 사람이 읽을 만한가. +// 파라미터는 기본이 빌림(use)이다. 소유를 가져갈 때만 own을 붙인다. import "cool.dev/std/list" as List pub fn refund_order( - use pay: PaymentGateway, + pay: PaymentGateway, id: OrderId, -) effects {PaymentGateway.refund} -> Result { +) effects {PaymentGateway.refund} -> Result[Receipt, PayError] { pay.refund(id) } // capability 둘, effect 둘. pub fn refund_and_log( - use pay: PaymentGateway, - use log: Logger, + pay: PaymentGateway, + log: Logger, id: OrderId, -) effects {PaymentGateway.refund, Logger.write} -> Result { +) effects {PaymentGateway.refund, Logger.write} -> Result[Receipt, PayError] { let receipt = pay.refund(id)? log.write("refunded: ", id) Ok(receipt) } // capability 셋 이상은 struct로 접는다 (표준 관용구). -// Deps는 affine 필드를 가지므로 전이적으로 affine이고, use로 빌려 쓴다. +// Deps는 affine 필드를 가지므로 전이적으로 affine이고, 무표기이므로 빌려 쓴다. pub struct Deps { pay: PaymentGateway, log: Logger, @@ -32,20 +32,21 @@ pub struct Deps { } pub fn checkout( - use deps: Deps, + deps: Deps, order: Order, -) effects {PaymentGateway.charge, Database.write, Logger.write} -> Result { +) effects {PaymentGateway.charge, Database.write, Logger.write} + -> Result[Receipt, Error] { let receipt = deps.pay.charge(order.id, order.amount)? deps.db.write(receipt)? deps.log.write("checkout: ", order.id) Ok(receipt) } -// use 값을 다른 use 파라미터로 넘기는 것은 허용된다 (복제가 아니다). +// 빌린 값을 다른 빌림 자리로 넘기는 것은 허용된다 (복제가 아니다). pub fn refund_all( - use pay: PaymentGateway, - ids: List, -) effects {PaymentGateway.refund} -> Result { + pay: PaymentGateway, + ids: List[OrderId], +) effects {PaymentGateway.refund} -> Result[Unit, PayError] { List.each(ids, fn(id) { refund_order(pay, id)? Ok(unit) diff --git a/samples/02_higher_order_effects.cool b/samples/02_higher_order_effects.cool index 0c5376a..0c1665d 100644 --- a/samples/02_higher_order_effects.cool +++ b/samples/02_higher_order_effects.cool @@ -1,46 +1,51 @@ // 02. effect 변수와 구문 수준 제한 // -// 파라미터 위치의 callable은 기본이 use(비탈출)이므로 표기가 없다. -// 파라미터 위치의 effect 절은 `effects <변수>` 또는 `effects {리터럴}`만 가능하고, +// 파라미터 위치의 effect 절은 `effects <변수>` 또는 `effects {리터럴}`만 가능하다. // 합집합은 결과 위치에서만 쓸 수 있다 — 문법이 그렇게 생겼다. -pub fn map( - xs: List, +pub fn map[a, b, e: effects]( + xs: List[a], f: fn(a) effects e -> b, -) effects e -> List +) effects e -> List[b] -pub fn each( - xs: List, +pub fn each[a, e: effects]( + xs: List[a], f: fn(a) effects e, ) effects e -pub fn fold( - xs: List, +pub fn fold[a, acc, e: effects]( + xs: List[a], init: acc, f: fn(acc, a) effects e -> acc, ) effects e -> acc // 결과 위치의 합집합. 분해되지 않으므로 결정적이다. -pub fn map_then_each( - xs: List, +pub fn map_then_each[a, b, e1: effects, e2: effects]( + xs: List[a], f: fn(a) effects e1 -> b, g: fn(b) effects e2, ) effects e1 | e2 { each(map(xs, f), g) } -// 클로저를 저장하는 sink는 own으로 유표기한다. 여기엔 use 값을 넘길 수 없다. +// 클로저를 저장하는 sink는 own으로 유표기한다. 빌린 값을 넘길 수 없다. pub fn register(handler: own fn()) effects {Registry.add} // 호출 지점: e는 인자의 시그니처에서 읽어온다. 추론이 아니라 읽기. pub fn total_size( - use fs: FileSystem, - paths: List, + fs: FileSystem, + paths: List[Path], ) effects {FileSystem.stat} -> Int { fold(paths, 0, fn(acc, p) { acc + fs.stat(p).size }) } // effect-free 클로저는 effects 절을 생략한다 (기본값 {}, 추론 아님). -pub fn lengths(xs: List) -> List { +pub fn lengths(xs: List[String]) -> List[Int] { map(xs, fn(s) { String.len(s) }) } + +// 합집합 위치는 명시적 인스턴스화를 요구한다. 후위 대괄호가 그 문법이다. +pub fn explicit_call(fs: FileSystem, paths: List[Path]) + effects {FileSystem.stat} -> List[Int] { + map[Path, Int, {FileSystem.stat}](paths, fn(p) { fs.stat(p).size }) +} diff --git a/samples/03_scope_concurrency.cool b/samples/03_scope_concurrency.cool index 0573b11..ac7c3ae 100644 --- a/samples/03_scope_concurrency.cool +++ b/samples/03_scope_concurrency.cool @@ -3,9 +3,9 @@ // scope는 이름을 갖는다. 중첩 시 어느 스코프에 붙는 태스크인지가 코드에 보인다. pub fn main( - use sc: TaskScope, - use fs: FileSystem, - use log: Logger, + sc: TaskScope, + fs: FileSystem, + log: Logger, ) effects {TaskScope.spawn, FileSystem.read, Logger.write} { let paths = [Path("a.txt"), Path("b.txt"), Path("c.txt")] @@ -25,10 +25,10 @@ pub fn main( // 중첩. 이름이 있으므로 outer.spawn과 inner.spawn이 그냥 갈린다. // 안쪽 블록에서 바깥 스코프에 붙이는 것도 표현 가능하다 — 수명이 코드에 보인다. pub fn fan_out( - use outer: TaskScope, - use fs: FileSystem, - use log: Logger, - groups: List>, + outer: TaskScope, + fs: FileSystem, + log: Logger, + groups: List[List[Path]], ) effects {TaskScope.spawn, FileSystem.read, Logger.write} { List.each(groups, fn(g) { scope inner { @@ -43,8 +43,8 @@ pub fn fan_out( // spawn 클로저는 by-move 또는 immutable capture만 가능하다. pub fn broadcast( - use sc: TaskScope, - use log: Logger, + sc: TaskScope, + log: Logger, msg: String, ) effects {TaskScope.spawn, Logger.write} { scope sc { diff --git a/samples/05_move_errors.cool b/samples/05_move_errors.cool index 9156c77..d4a64a7 100644 --- a/samples/05_move_errors.cool +++ b/samples/05_move_errors.cool @@ -1,51 +1,54 @@ // 05. 컴파일 에러가 나야 하는 코드 // // 각 함수는 주석에 적힌 진단 하나를 정확히 내야 한다. -// 파서·체커가 생기면 그대로 테스트 케이스가 된다. +// 체커가 생기면 그대로 테스트 케이스가 된다. + +// close는 파일을 소비한다: own 유표기 +pub fn close(own f: File) effects {File.close} // [E-move-after-move] affine 값의 이중 소비 -pub fn double_close(f: File) effects {File.close} { - File.close(f) - File.close(f) // ERROR: f는 이미 move됨 (앞줄에서 소비) +pub fn double_close(own f: File) effects {File.close} { + close(f) + close(f) // ERROR: f는 이미 move됨 (앞줄에서 소비) } // [E-move-join] 분기 병합은 보수적 합집합 -pub fn conditional_close(f: File, c: Bool) effects {File.close} { +pub fn conditional_close(own f: File, c: Bool) effects {File.close} { if c { - File.close(f) + close(f) } - File.close(f) // ERROR: f는 이 분기에서 move됨 (조건부 소비) + close(f) // ERROR: f는 이 분기에서 move됨 (조건부 소비) } // 정당한 형태 — 양쪽 분기에서 소비하면 통과해야 한다. -pub fn both_branches_close(f: File, c: Bool) effects {File.close} { +pub fn both_branches_close(own f: File, c: Bool) effects {File.close} { if c { - File.close(f) + close(f) } else { - File.close(f) + close(f) } } -// [E-use-escape] use 값의 반환 -pub fn leak_capability(use pay: PaymentGateway) -> PaymentGateway { - pay // ERROR: use 값은 반환할 수 없음 +// [E-use-escape] 빌린 값의 반환 +pub fn leak_capability(pay: PaymentGateway) -> PaymentGateway { + pay // ERROR: 빌린 값은 반환할 수 없음 (own이 아니다) } -// [E-use-escape] use 값의 저장 +// [E-use-escape] 빌린 값의 저장 pub struct Holder { pay: PaymentGateway, } -pub fn store_capability(use pay: PaymentGateway) -> Holder { - Holder { pay: pay } // ERROR: use 값은 struct에 저장할 수 없음 +pub fn store_capability(pay: PaymentGateway) -> Holder { + Holder { pay: pay } // ERROR: 빌린 값은 struct에 저장할 수 없음 } -// [E-use-escape] use-closure를 own fn 위치에 전달 +// [E-use-escape] 빌린 값을 capture한 클로저를 own 자리에 전달 pub fn register(handler: own fn()) effects {Registry.add} -pub fn escape_via_closure(use pay: PaymentGateway) effects {Registry.add} { +pub fn escape_via_closure(pay: PaymentGateway) effects {Registry.add} { register(fn() { pay.refund(OrderId(1)) }) - // ERROR: pay를 capture한 클로저는 use 값이며 own fn 위치에 전달할 수 없음 + // ERROR: pay를 capture한 클로저는 빌린 값이며 own 자리에 전달할 수 없음 } // [E-affinity-transitive] affine 필드를 가진 타입을 copyable로 선언 @@ -54,14 +57,13 @@ pub copyable struct Box { } // [E-callable-affinity] affine 값을 capture한 클로저를 fn 위치에 대입 -// 반환 타입 위치의 callable은 무표기 owned이지만, affinity는 별개로 표기된다. -pub fn misuse_affine_closure(f: File) -> fn() effects {File.close} { - fn() { File.close(f) } +pub fn misuse_affine_closure(own f: File) -> fn() effects {File.close} { + fn() { close(f) } // ERROR: f를 capture했으므로 타입은 affine fn()이며 fn() 위치에 대입할 수 없음 } // [E-spawn-capture] spawn 클로저의 mutable capture -pub fn spawn_mutable(use sc: TaskScope, mut counter: Int) effects {TaskScope.spawn} { +pub fn spawn_mutable(sc: TaskScope, mut counter: Int) effects {TaskScope.spawn} { scope sc { sc.spawn(fn() { counter = counter + 1 }) // ERROR: spawn 클로저는 mutable 참조를 capture할 수 없음 @@ -69,13 +71,22 @@ pub fn spawn_mutable(use sc: TaskScope, mut counter: Int) effects {TaskScope.spa } // [E-effect-undeclared] 선언되지 않은 effect -pub fn silent_write(use log: Logger) { +pub fn silent_write(log: Logger) { log.write("hi") // ERROR: effect Logger.write가 시그니처에 선언되지 않음 } // [E-syntax-effect-union] 파라미터 위치의 합집합은 문법에 존재하지 않는다. -// 검사기가 거부하는 것이 아니라 파서가 거부한다. -pub fn peel( +// 검사기가 아니라 파서가 거부한다 (eff_param 프로덕션에 "|"가 없다). +pub fn peel[a, e: effects]( f: fn(a) effects e | {Logger.write}, ) effects e -> fn(a) // ERROR (parse): 파라미터 위치의 effects 절은 변수 단독 또는 리터럴 집합만 허용 + +// [E-syntax-match-guard] match 가드는 v0 문법에 없다 +pub fn classify(e: PayError) -> String { + match e { + Network(r) if r.retryable => "retry", + // ERROR (parse): match 가드는 지원되지 않음. 분기 본문에서 if를 쓸 것 + _ => "other", + } +} diff --git a/samples/06_affine_closure.cool b/samples/06_affine_closure.cool index 56330fa..671d8d4 100644 --- a/samples/06_affine_closure.cool +++ b/samples/06_affine_closure.cool @@ -1,37 +1,38 @@ // 06. callable affinity — fn과 affine fn의 구분 // -// affinity(타입 속성)와 use/own(파라미터 위치 속성)은 직교한다. +// affinity는 타입 속성, own/무표기는 파라미터 위치 속성. 둘은 직교한다. // copyable 환경만 capture → fn. 복사해서 여러 번 써도 된다. pub fn make_formatter(prefix: String) -> fn(String) -> String { fn(s) { String.concat(prefix, s) } } -// affine 값을 소유 → affine fn. 자신도 move 규칙을 따른다. -pub fn deferred_close(f: File) -> affine fn() effects {File.close} { +// affine 값을 소유해 가므로 own. 결과 클로저는 자동으로 affine fn이 된다. +pub fn deferred_close(own f: File) -> affine fn() effects {File.close} { fn() { File.close(f) } } -// 파라미터 위치이므로 use가 기본이지만, affine fn은 소유해야 호출 후 소비된다. -pub fn run_once(action: own affine fn() effects {File.close}) effects {File.close} { +// affine fn을 호출 후 소비하려면 소유해야 한다. +pub fn run_once(own action: affine fn() effects {File.close}) + effects {File.close} { action() } -pub fn example(f: File) effects {File.close} { +pub fn example(own f: File) effects {File.close} { let close = deferred_close(f) run_once(close) - // run_once(close) // ERROR가 되어야 함: close는 affine, 이미 move됨 + // run_once(close) // ERROR: close는 affine이고 앞줄에서 move됨 } -// use 클로저 — 표기가 없는 쪽이 흔한 쪽이다. -pub fn with_retry( +// 빌리는 쪽은 무표기다 — 흔한 쪽에 표기가 없다. +pub fn with_retry[a, e: effects]( times: Int, - body: fn() effects e -> Result, -) effects e -> Result + body: fn() effects e -> Result[a, Error], +) effects e -> Result[a, Error] pub fn refund_with_retry( - use pay: PaymentGateway, + pay: PaymentGateway, id: OrderId, -) effects {PaymentGateway.refund} -> Result { +) effects {PaymentGateway.refund} -> Result[Receipt, Error] { with_retry(3, fn() { pay.refund(id) }) } diff --git a/samples/07_module_interface.cool b/samples/07_module_interface.cool index f859544..7de9612 100644 --- a/samples/07_module_interface.cool +++ b/samples/07_module_interface.cool @@ -4,9 +4,9 @@ // downstream이 검사에 쓰는 정보 전부다. 본문은 하나도 필요 없다. pub capability PaymentGateway { - fn refund(id: OrderId) effects {PaymentGateway.refund} -> Result + fn refund(id: OrderId) effects {PaymentGateway.refund} -> Result[Receipt, PayError] fn charge(id: OrderId, amount: Money) - effects {PaymentGateway.charge} -> Result + effects {PaymentGateway.charge} -> Result[Receipt, PayError] } // 타입 정의 본문 (exhaustiveness에 직결하므로 variant까지 전부) @@ -33,8 +33,8 @@ pub const MAX_RETRIES: Int = 3 // 함수는 시그니처만. 본문은 hash에 들어가지 않는다. pub fn refund_order( - use pay: PaymentGateway, + pay: PaymentGateway, id: OrderId, -) effects {PaymentGateway.refund} -> Result +) effects {PaymentGateway.refund} -> Result[Receipt, PayError] pub fn retry_budget() -> Int diff --git a/samples/README.md b/samples/README.md index 239fa27..20f9583 100644 --- a/samples/README.md +++ b/samples/README.md @@ -1,44 +1,41 @@ # samples — 문법 탐침 -여기 있는 `.cool` 파일은 **명세가 아니라 탐침**이다. 목적은 하나: -`docs/thesis.md`의 v0 성공 기준 중 "체감" 항목 — 확정된 규칙(use/own, affine, -effect 변수, TaskScope)으로 짠 코드가 사람이 읽을 만한지 — 을 EBNF를 쓰기 전에 -눈으로 확인하는 것. +여기 있는 `.cool` 파일은 **명세가 아니라 탐침**이다. 목적은 +`docs/thesis.md`의 v0 성공 기준 중 "체감" 항목 — 확정된 규칙으로 짠 코드가 +사람이 읽을 만한지 — 을 눈으로 확인하는 것. 문법 정의는 `docs/grammar.ebnf`. 파서는 아직 없으므로 이 파일들은 컴파일되지 않는다. | 파일 | 검증 대상 | |---|---| -| 01_capability_signature | use 파라미터 + effects 절, capability struct 관용구 | -| 02_higher_order_effects | effect 변수, 구문 수준 제한, use 기본 / own 유표기 | +| 01_capability_signature | capability 파라미터, effects 절 어순, capability struct 관용구 | +| 02_higher_order_effects | effect 변수, 구문 수준 제한, 명시적 인스턴스화 | | 03_scope_concurrency | TaskScope, 이름 있는 scope, 중첩 시 수명 표현 | | 04_enum_match_interface | enum 정의 본문이 interface surface에 들어가는 경로 | | 05_move_errors | **에러가 나야 하는** 코드 — 진단 하나씩 | -| 06_affine_closure | callable affinity (fn vs affine fn), use/own과의 직교성 | +| 06_affine_closure | callable affinity (fn vs affine fn), own과의 직교성 | | 07_module_interface | interface artifact가 담아야 할 것 전부 | `05_move_errors.cool`은 통과하면 안 되는 파일이다. 각 함수 주석의 `[E-...]` 태그가 기대 진단이며, 체커가 생기면 그대로 테스트 케이스가 된다. +`E-syntax-*` 태그는 파서가, 나머지는 체커가 거부해야 한다. ## 확정된 표기 - 어순: `fn 이름(파라미터) effects {...} -> 반환타입`. 함수 타입도 동일: `fn(a) effects e -> b` +- 파라미터는 기본이 빌림(use, 무표기). 소유 이전만 `own` 유표기. + capability든 클로저든 일반 affine 값이든 규칙은 하나다 +- 파라미터 밖(반환 타입, struct 필드, channel 원소)은 항상 owned, 무표기 +- 제네릭은 대괄호: 타입 `List[a]`, 식 `map[Int, String, {fs.read}](xs, f)`. + 인덱싱 연산자는 두지 않는다 (`List.at`) +- 수식어 순서: `own` / `mut`가 먼저, 타입 수식어 `affine`은 타입 안 - 모듈: 파일 = 모듈, `pub`이 exported, `import "domain/path" as Name` -- 재수출: `reexport Name` (전용 키워드 — hash 전파를 동반하므로 구분한다) -- capability 파라미터: `fn f(use pay: Gateway)` -- callable 파라미터: 기본이 use(비탈출, 무표기). 저장하는 쪽만 `own fn` -- 파라미터 밖(반환 타입, struct 필드, channel 원소)의 callable: 무표기 owned -- affine 클로저 타입: `affine fn(a) effects e -> b` +- 재수출: `reexport Name` (전용 키워드 — hash 전파를 동반한다) - effect 집합: `effects {Type.method, ...}`, 빈 집합은 생략 -- effect 변수: `` 선언, 합집합 `e1 | e2`는 결과 위치 전용 -- 제네릭: `List` -- scope: `scope sc { ... }` — 이름 필수, 블록 종료 시 join/cancel -- 오류 전파: `?` - -## 아직 열려 있는 것 - -- 표현식 `a < b`와 제네릭 `List`의 `<` 모호성 해소 방법 -- `?`의 정확한 의미론 (Result 전용인지, 어디까지 전파하는지) -- `match` 가드, 중첩 패턴의 범위 -- 리터럴 배열 `[...]`과 컬렉션 표기 +- effect 변수: `[e: effects]` 선언, 합집합 `e1 | e2`는 결과 위치 전용 +- 문 구분은 줄바꿈 (Go식 자동 삽입) +- 블록은 식. 마지막 식이 값이고 `return`은 조기 탈출 전용 +- `if`와 `match`는 식. `match` 가드 없음 +- 리스트 리터럴 `[a, b, c]`, 빈 리터럴은 타입 주석 필요 +- 오류 전파 `?`는 `Result` 전용