grammar: v0 EBNF 확정과 열린 문법 결정 8건

열려 있던 문법 항목을 언어 철학에 따라 전부 닫고 EBNF로 고정한다.

- 제네릭 인자를 대괄호로 통일(List[a], map[Int, String](...)). <>는 식
  위치에서 비교 연산자와 갈리지 않아 turbofish라는 제2 표기를 부르는데,
  effect 규칙이 명시적 인스턴스화 문법을 요구하므로 회피할 수 없다.
  대괄호는 전위=리터럴 / 후위=인스턴스화로 위치가 결정해 LL(1)이고 표기가
  하나다. 대가로 인덱싱 연산자를 두지 않는다.
- 파라미터 규칙을 callable 한정에서 전 파라미터로 일반화. 기본은 빌림,
  own만 소유 이전. capability를 예외로 두지 않는 이유는 capability의 존재가
  이미 타입과 effects 절에 드러나기 때문이다 — 표기가 실을 정보는 소유 이동뿐.
- 문 구분은 줄바꿈(Go식 자동 삽입), 블록은 식, return은 조기 탈출 전용.
  if/match를 식으로 두면 재대입이 줄어 move 검사가 단순해진다.
- match 가드 없음. 가드는 exhaustiveness를 흐리고 SMT 없이는 _ 분기를
  강요한다. 철학 1의 대표 항목을 문법 편의와 바꾸지 않는다.
- ?는 Result 전용 하드코딩. 조기 탈출도 블록 종료이므로 scope의 join/cancel이
  ? 경로에서도 실행된다. v1 linear 도입 시 이 경로가 암묵 drop 문제와 만난다.
- 수식어 순서는 바인딩(own, mut) 먼저, 타입(affine) 나중.

docs/grammar.ebnf는 LL(1)을 설계 제약으로 명시하고, effect 합집합을
결과 위치 전용 프로덕션으로 새겨 파라미터 위치의 역산을 파서가 거부하게 한다.
samples/는 확정 표기로 전면 갱신.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019ZVDeU6KLuUVL3gs18Hm3E
This commit is contained in:
2026-08-30 02:09:58 +09:00
co-authored by Claude Opus 5
parent e3b19b1300
commit 630d12104a
9 changed files with 329 additions and 108 deletions
+181
View File
@@ -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 ) *)
+29 -4
View File
@@ -66,15 +66,18 @@ Capability 전달 (Alias/Move 모델의 제3의 규칙):
죽이므로 기각. 별도의 nonescaping 개념을 만들지 않고 use의 전염으로 정의해 죽이므로 기각. 별도의 nonescaping 개념을 만들지 않고 use의 전염으로 정의해
"한 개념 한 방식"을 지킨다 (Swift @noescape, second-class values의 검증된 경로) "한 개념 한 방식"을 지킨다 (Swift @noescape, second-class values의 검증된 경로)
- 소유권 이전이 필요할 때만 명시적 move - 소유권 이전이 필요할 때만 명시적 move
- 파라미터 위치의 callable은 기본이 use(비탈출)이고, 저장하는 쪽이 own fn으로 - 파라미터 위치는 전부 기본이 use(빌림)이고, 소유 이전만 own으로 유표기한다.
유표기한다. 파라미터 밖(반환 타입, struct 필드, channel 원소)의 callable은 capability든 callable이든 일반 affine 값이든 규칙은 하나다
use가 애초에 불가능한 위치이므로 무표기로 항상 owned 의미다
※ 무표기-흔함 / 유표기-위험은 이미 언어의 다른 곳에 있는 규칙이다 ※ 무표기-흔함 / 유표기-위험은 이미 언어의 다른 곳에 있는 규칙이다
(effect 없는 함수가 무표기, effect 있는 쪽이 effects 유표기). (effect 없는 함수가 무표기, effect 있는 쪽이 effects 유표기).
새 조항이 아니라 같은 원칙의 적용이다 새 조항이 아니라 같은 원칙의 적용이다
※ 반대로 두면 클로저를 저장하지 않는 고차 함수 — 즉 표준 라이브러리의 거의 전부 — ※ 반대로 두면 클로저를 저장하지 않는 고차 함수 — 즉 표준 라이브러리의 거의 전부 —
use 표기로 도배된다. 위험한 쪽(클로저를 오래 보관하는 sink)에 표기가 가야 가 표기로 도배된다. 위험한 쪽(소유를 가져가는 쪽)에 표기가 가야
리뷰어의 눈이 그리로 간다 리뷰어의 눈이 그리로 간다
※ capability를 별도 규칙으로 두지 않는 이유: capability의 존재는 이미 타입과
effects 절에 드러난다. 표기가 실어야 할 정보는 "소유가 옮겨가는가"뿐이다
- 파라미터 밖(반환 타입, struct 필드, channel 원소)은 use가 애초에 불가능한
위치이므로 무표기로 항상 owned 의미다
- capability가 셋 이상 필요하면 capability struct로 접는다 (표준 관용구): - capability가 셋 이상 필요하면 capability struct로 접는다 (표준 관용구):
struct Deps { pay: PaymentGateway, log: Logger, db: Database } struct Deps { pay: PaymentGateway, log: Logger, db: Database }
use deps: Deps로 받으면 affinity 전이와 use 규칙이 그대로 적용되므로 use deps: Deps로 받으면 affinity 전이와 use 규칙이 그대로 적용되므로
@@ -223,6 +226,28 @@ Generics (철학 2에서 파생):
- coollang 단독 표기 (스탠퍼드 Cool과 구별, golang 방식 검색성 확보) - coollang 단독 표기 (스탠퍼드 Cool과 구별, golang 방식 검색성 확보)
- .cool (충돌 제로, 확장자 생략은 toolchain이 허용) - .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 — "기능의 서브셋, 아키텍처의 풀셋" ■ v0 — "기능의 서브셋, 아키텍처의 풀셋"
목표: 시스템 속성(빠른 검증 루프)은 측정으로만 증명된다. 목표: 시스템 속성(빠른 검증 루프)은 측정으로만 증명된다.
성공 기준(숫자): 10만 줄 규모에서 함수 수정 시 check 50ms 이내. 성공 기준(숫자): 10만 줄 규모에서 함수 수정 시 check 50ms 이내.
+15 -14
View File
@@ -1,30 +1,30 @@
// 01. use 파라미터 + effects 절이 한 시그니처에 동시에 붙는 모양 // 01. capability 파라미터 effects 절
// //
// 어순: fn 이름(파라미터) effects {...} -> 반환타입 // 어순: fn 이름(파라미터) effects {...} -> 반환타입
// 검증 대상: capability를 받는 평범한 함수가 사람이 읽을 만한가. // 파라미터는 기본이 빌림(use)이다. 소유를 가져갈 때만 own을 붙인다.
import "cool.dev/std/list" as List import "cool.dev/std/list" as List
pub fn refund_order( pub fn refund_order(
use pay: PaymentGateway, pay: PaymentGateway,
id: OrderId, id: OrderId,
) effects {PaymentGateway.refund} -> Result<Receipt, PayError> { ) effects {PaymentGateway.refund} -> Result[Receipt, PayError] {
pay.refund(id) pay.refund(id)
} }
// capability 둘, effect 둘. // capability 둘, effect 둘.
pub fn refund_and_log( pub fn refund_and_log(
use pay: PaymentGateway, pay: PaymentGateway,
use log: Logger, log: Logger,
id: OrderId, id: OrderId,
) effects {PaymentGateway.refund, Logger.write} -> Result<Receipt, PayError> { ) effects {PaymentGateway.refund, Logger.write} -> Result[Receipt, PayError] {
let receipt = pay.refund(id)? let receipt = pay.refund(id)?
log.write("refunded: ", id) log.write("refunded: ", id)
Ok(receipt) Ok(receipt)
} }
// capability 셋 이상은 struct로 접는다 (표준 관용구). // capability 셋 이상은 struct로 접는다 (표준 관용구).
// Deps는 affine 필드를 가지므로 전이적으로 affine이고, use로 빌려 쓴다. // Deps는 affine 필드를 가지므로 전이적으로 affine이고, 무표기이므로 빌려 쓴다.
pub struct Deps { pub struct Deps {
pay: PaymentGateway, pay: PaymentGateway,
log: Logger, log: Logger,
@@ -32,20 +32,21 @@ pub struct Deps {
} }
pub fn checkout( pub fn checkout(
use deps: Deps, deps: Deps,
order: Order, order: Order,
) effects {PaymentGateway.charge, Database.write, Logger.write} -> Result<Receipt, Error> { ) effects {PaymentGateway.charge, Database.write, Logger.write}
-> Result[Receipt, Error] {
let receipt = deps.pay.charge(order.id, order.amount)? let receipt = deps.pay.charge(order.id, order.amount)?
deps.db.write(receipt)? deps.db.write(receipt)?
deps.log.write("checkout: ", order.id) deps.log.write("checkout: ", order.id)
Ok(receipt) Ok(receipt)
} }
// use 값을 다른 use 파라미터로 넘기는 것은 허용된다 (복제가 아니다). // 빌린 값을 다른 빌림 자리로 넘기는 것은 허용된다 (복제가 아니다).
pub fn refund_all( pub fn refund_all(
use pay: PaymentGateway, pay: PaymentGateway,
ids: List<OrderId>, ids: List[OrderId],
) effects {PaymentGateway.refund} -> Result<Unit, PayError> { ) effects {PaymentGateway.refund} -> Result[Unit, PayError] {
List.each(ids, fn(id) { List.each(ids, fn(id) {
refund_order(pay, id)? refund_order(pay, id)?
Ok(unit) Ok(unit)
+20 -15
View File
@@ -1,46 +1,51 @@
// 02. effect 변수와 구문 수준 제한 // 02. effect 변수와 구문 수준 제한
// //
// 파라미터 위치의 callable은 기본이 use(비탈출)이므로 표기가 없다. // 파라미터 위치의 effect 절은 `effects <변수>` 또는 `effects {리터럴}`만 가능하다.
// 파라미터 위치의 effect 절은 `effects <변수>` 또는 `effects {리터럴}`만 가능하고,
// 합집합은 결과 위치에서만 쓸 수 있다 — 문법이 그렇게 생겼다. // 합집합은 결과 위치에서만 쓸 수 있다 — 문법이 그렇게 생겼다.
pub fn map<a, b, e: effects>( pub fn map[a, b, e: effects](
xs: List<a>, xs: List[a],
f: fn(a) effects e -> b, f: fn(a) effects e -> b,
) effects e -> List<b> ) effects e -> List[b]
pub fn each<a, e: effects>( pub fn each[a, e: effects](
xs: List<a>, xs: List[a],
f: fn(a) effects e, f: fn(a) effects e,
) effects e ) effects e
pub fn fold<a, acc, e: effects>( pub fn fold[a, acc, e: effects](
xs: List<a>, xs: List[a],
init: acc, init: acc,
f: fn(acc, a) effects e -> acc, f: fn(acc, a) effects e -> acc,
) effects e -> acc ) effects e -> acc
// 결과 위치의 합집합. 분해되지 않으므로 결정적이다. // 결과 위치의 합집합. 분해되지 않으므로 결정적이다.
pub fn map_then_each<a, b, e1: effects, e2: effects>( pub fn map_then_each[a, b, e1: effects, e2: effects](
xs: List<a>, xs: List[a],
f: fn(a) effects e1 -> b, f: fn(a) effects e1 -> b,
g: fn(b) effects e2, g: fn(b) effects e2,
) effects e1 | e2 { ) effects e1 | e2 {
each(map(xs, f), g) each(map(xs, f), g)
} }
// 클로저를 저장하는 sink는 own으로 유표기한다. 여기엔 use 값을 넘길 수 없다. // 클로저를 저장하는 sink는 own으로 유표기한다. 빌린 값을 넘길 수 없다.
pub fn register(handler: own fn()) effects {Registry.add} pub fn register(handler: own fn()) effects {Registry.add}
// 호출 지점: e는 인자의 시그니처에서 읽어온다. 추론이 아니라 읽기. // 호출 지점: e는 인자의 시그니처에서 읽어온다. 추론이 아니라 읽기.
pub fn total_size( pub fn total_size(
use fs: FileSystem, fs: FileSystem,
paths: List<Path>, paths: List[Path],
) effects {FileSystem.stat} -> Int { ) effects {FileSystem.stat} -> Int {
fold(paths, 0, fn(acc, p) { acc + fs.stat(p).size }) fold(paths, 0, fn(acc, p) { acc + fs.stat(p).size })
} }
// effect-free 클로저는 effects 절을 생략한다 (기본값 {}, 추론 아님). // effect-free 클로저는 effects 절을 생략한다 (기본값 {}, 추론 아님).
pub fn lengths(xs: List<String>) -> List<Int> { pub fn lengths(xs: List[String]) -> List[Int] {
map(xs, fn(s) { String.len(s) }) 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 })
}
+9 -9
View File
@@ -3,9 +3,9 @@
// scope는 이름을 갖는다. 중첩 시 어느 스코프에 붙는 태스크인지가 코드에 보인다. // scope는 이름을 갖는다. 중첩 시 어느 스코프에 붙는 태스크인지가 코드에 보인다.
pub fn main( pub fn main(
use sc: TaskScope, sc: TaskScope,
use fs: FileSystem, fs: FileSystem,
use log: Logger, log: Logger,
) effects {TaskScope.spawn, FileSystem.read, Logger.write} { ) effects {TaskScope.spawn, FileSystem.read, Logger.write} {
let paths = [Path("a.txt"), Path("b.txt"), Path("c.txt")] let paths = [Path("a.txt"), Path("b.txt"), Path("c.txt")]
@@ -25,10 +25,10 @@ pub fn main(
// 중첩. 이름이 있으므로 outer.spawn과 inner.spawn이 그냥 갈린다. // 중첩. 이름이 있으므로 outer.spawn과 inner.spawn이 그냥 갈린다.
// 안쪽 블록에서 바깥 스코프에 붙이는 것도 표현 가능하다 — 수명이 코드에 보인다. // 안쪽 블록에서 바깥 스코프에 붙이는 것도 표현 가능하다 — 수명이 코드에 보인다.
pub fn fan_out( pub fn fan_out(
use outer: TaskScope, outer: TaskScope,
use fs: FileSystem, fs: FileSystem,
use log: Logger, log: Logger,
groups: List<List<Path>>, groups: List[List[Path]],
) effects {TaskScope.spawn, FileSystem.read, Logger.write} { ) effects {TaskScope.spawn, FileSystem.read, Logger.write} {
List.each(groups, fn(g) { List.each(groups, fn(g) {
scope inner { scope inner {
@@ -43,8 +43,8 @@ pub fn fan_out(
// spawn 클로저는 by-move 또는 immutable capture만 가능하다. // spawn 클로저는 by-move 또는 immutable capture만 가능하다.
pub fn broadcast( pub fn broadcast(
use sc: TaskScope, sc: TaskScope,
use log: Logger, log: Logger,
msg: String, msg: String,
) effects {TaskScope.spawn, Logger.write} { ) effects {TaskScope.spawn, Logger.write} {
scope sc { scope sc {
+37 -26
View File
@@ -1,51 +1,54 @@
// 05. 컴파일 에러가 나야 하는 코드 // 05. 컴파일 에러가 나야 하는 코드
// //
// 각 함수는 주석에 적힌 진단 하나를 정확히 내야 한다. // 각 함수는 주석에 적힌 진단 하나를 정확히 내야 한다.
// 파서·체커가 생기면 그대로 테스트 케이스가 된다. // 체커가 생기면 그대로 테스트 케이스가 된다.
// close는 파일을 소비한다: own 유표기
pub fn close(own f: File) effects {File.close}
// [E-move-after-move] affine 값의 이중 소비 // [E-move-after-move] affine 값의 이중 소비
pub fn double_close(f: File) effects {File.close} { pub fn double_close(own f: File) effects {File.close} {
File.close(f) close(f)
File.close(f) // ERROR: f는 이미 move됨 (앞줄에서 소비) close(f) // ERROR: f는 이미 move됨 (앞줄에서 소비)
} }
// [E-move-join] 분기 병합은 보수적 합집합 // [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 { 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 { if c {
File.close(f) close(f)
} else { } else {
File.close(f) close(f)
} }
} }
// [E-use-escape] use 값의 반환 // [E-use-escape] 빌린 값의 반환
pub fn leak_capability(use pay: PaymentGateway) -> PaymentGateway { pub fn leak_capability(pay: PaymentGateway) -> PaymentGateway {
pay // ERROR: use 값은 반환할 수 없음 pay // ERROR: 빌린 값은 반환할 수 없음 (own이 아니다)
} }
// [E-use-escape] use 값의 저장 // [E-use-escape] 빌린 값의 저장
pub struct Holder { pub struct Holder {
pay: PaymentGateway, pay: PaymentGateway,
} }
pub fn store_capability(use pay: PaymentGateway) -> Holder { pub fn store_capability(pay: PaymentGateway) -> Holder {
Holder { pay: pay } // ERROR: use 값은 struct에 저장할 수 없음 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 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)) }) register(fn() { pay.refund(OrderId(1)) })
// ERROR: pay를 capture한 클로저는 use 값이며 own fn 위치에 전달할 수 없음 // ERROR: pay를 capture한 클로저는 빌린 값이며 own 자리에 전달할 수 없음
} }
// [E-affinity-transitive] affine 필드를 가진 타입을 copyable로 선언 // [E-affinity-transitive] affine 필드를 가진 타입을 copyable로 선언
@@ -54,14 +57,13 @@ pub copyable struct Box {
} }
// [E-callable-affinity] affine 값을 capture한 클로저를 fn 위치에 대입 // [E-callable-affinity] affine 값을 capture한 클로저를 fn 위치에 대입
// 반환 타입 위치의 callable은 무표기 owned이지만, affinity는 별개로 표기된다. pub fn misuse_affine_closure(own f: File) -> fn() effects {File.close} {
pub fn misuse_affine_closure(f: File) -> fn() effects {File.close} { fn() { close(f) }
fn() { File.close(f) }
// ERROR: f를 capture했으므로 타입은 affine fn()이며 fn() 위치에 대입할 수 없음 // ERROR: f를 capture했으므로 타입은 affine fn()이며 fn() 위치에 대입할 수 없음
} }
// [E-spawn-capture] spawn 클로저의 mutable capture // [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 { scope sc {
sc.spawn(fn() { counter = counter + 1 }) sc.spawn(fn() { counter = counter + 1 })
// ERROR: spawn 클로저는 mutable 참조를 capture할 수 없음 // 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 // [E-effect-undeclared] 선언되지 않은 effect
pub fn silent_write(use log: Logger) { pub fn silent_write(log: Logger) {
log.write("hi") // ERROR: effect Logger.write가 시그니처에 선언되지 않음 log.write("hi") // ERROR: effect Logger.write가 시그니처에 선언되지 않음
} }
// [E-syntax-effect-union] 파라미터 위치의 합집합은 문법에 존재하지 않는다. // [E-syntax-effect-union] 파라미터 위치의 합집합은 문법에 존재하지 않는다.
// 검사기가 거부하는 것이 아니라 파서가 거부한다. // 검사기가 아니라 파서가 거부한다 (eff_param 프로덕션에 "|"가 없다).
pub fn peel<a, e: effects>( pub fn peel[a, e: effects](
f: fn(a) effects e | {Logger.write}, f: fn(a) effects e | {Logger.write},
) effects e -> fn(a) ) effects e -> fn(a)
// ERROR (parse): 파라미터 위치의 effects 절은 변수 단독 또는 리터럴 집합만 허용 // 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",
}
}
+14 -13
View File
@@ -1,37 +1,38 @@
// 06. callable affinity — fn과 affine fn의 구분 // 06. callable affinity — fn과 affine fn의 구분
// //
// affinity(타입 속성)와 use/own(파라미터 위치 속성)은 직교한다. // affinity타입 속성, own/무표기는 파라미터 위치 속성. 둘은 직교한다.
// copyable 환경만 capture → fn. 복사해서 여러 번 써도 된다. // copyable 환경만 capture → fn. 복사해서 여러 번 써도 된다.
pub fn make_formatter(prefix: String) -> fn(String) -> String { pub fn make_formatter(prefix: String) -> fn(String) -> String {
fn(s) { String.concat(prefix, s) } fn(s) { String.concat(prefix, s) }
} }
// affine 값을 소유 → affine fn. 자신도 move 규칙을 따른다. // affine 값을 소유해 가므로 own. 결과 클로저는 자동으로 affine fn이 된다.
pub fn deferred_close(f: File) -> affine fn() effects {File.close} { pub fn deferred_close(own f: File) -> affine fn() effects {File.close} {
fn() { File.close(f) } fn() { File.close(f) }
} }
// 파라미터 위치이므로 use가 기본이지만, affine fn은 소유해야 호출 후 소비다. // affine fn 호출 후 소비하려면 소유해야 한다.
pub fn run_once(action: own affine fn() effects {File.close}) effects {File.close} { pub fn run_once(own action: affine fn() effects {File.close})
effects {File.close} {
action() action()
} }
pub fn example(f: File) effects {File.close} { pub fn example(own f: File) effects {File.close} {
let close = deferred_close(f) let close = deferred_close(f)
run_once(close) run_once(close)
// run_once(close) // ERROR가 되어야 함: close는 affine, 이미 move됨 // run_once(close) // ERROR: close는 affine이고 앞줄에서 move됨
} }
// use 클로저 — 표기가 없는 쪽이 흔한 쪽이다. // 빌리는 쪽은 무표기다 — 흔한 쪽에 표기가 없다.
pub fn with_retry<a, e: effects>( pub fn with_retry[a, e: effects](
times: Int, times: Int,
body: fn() effects e -> Result<a, Error>, body: fn() effects e -> Result[a, Error],
) effects e -> Result<a, Error> ) effects e -> Result[a, Error]
pub fn refund_with_retry( pub fn refund_with_retry(
use pay: PaymentGateway, pay: PaymentGateway,
id: OrderId, id: OrderId,
) effects {PaymentGateway.refund} -> Result<Receipt, Error> { ) effects {PaymentGateway.refund} -> Result[Receipt, Error] {
with_retry(3, fn() { pay.refund(id) }) with_retry(3, fn() { pay.refund(id) })
} }
+4 -4
View File
@@ -4,9 +4,9 @@
// downstream이 검사에 쓰는 정보 전부다. 본문은 하나도 필요 없다. // downstream이 검사에 쓰는 정보 전부다. 본문은 하나도 필요 없다.
pub capability PaymentGateway { pub capability PaymentGateway {
fn refund(id: OrderId) effects {PaymentGateway.refund} -> Result<Receipt, PayError> fn refund(id: OrderId) effects {PaymentGateway.refund} -> Result[Receipt, PayError]
fn charge(id: OrderId, amount: Money) fn charge(id: OrderId, amount: Money)
effects {PaymentGateway.charge} -> Result<Receipt, PayError> effects {PaymentGateway.charge} -> Result[Receipt, PayError]
} }
// 타입 정의 본문 (exhaustiveness에 직결하므로 variant까지 전부) // 타입 정의 본문 (exhaustiveness에 직결하므로 variant까지 전부)
@@ -33,8 +33,8 @@ pub const MAX_RETRIES: Int = 3
// 함수는 시그니처만. 본문은 hash에 들어가지 않는다. // 함수는 시그니처만. 본문은 hash에 들어가지 않는다.
pub fn refund_order( pub fn refund_order(
use pay: PaymentGateway, pay: PaymentGateway,
id: OrderId, id: OrderId,
) effects {PaymentGateway.refund} -> Result<Receipt, PayError> ) effects {PaymentGateway.refund} -> Result[Receipt, PayError]
pub fn retry_budget() -> Int pub fn retry_budget() -> Int
+20 -23
View File
@@ -1,44 +1,41 @@
# samples — 문법 탐침 # samples — 문법 탐침
여기 있는 `.cool` 파일은 **명세가 아니라 탐침**이다. 목적은 하나: 여기 있는 `.cool` 파일은 **명세가 아니라 탐침**이다. 목적은
`docs/thesis.md`의 v0 성공 기준 중 "체감" 항목 — 확정된 규칙(use/own, affine, `docs/thesis.md`의 v0 성공 기준 중 "체감" 항목 — 확정된 규칙으로 짠 코드가
effect 변수, TaskScope)으로 짠 코드가 사람이 읽을 만한지 — 을 EBNF를 쓰기 전에 사람이 읽을 만한지 — 을 눈으로 확인하는 것. 문법 정의는 `docs/grammar.ebnf`.
눈으로 확인하는 것.
파서는 아직 없으므로 이 파일들은 컴파일되지 않는다. 파서는 아직 없으므로 이 파일들은 컴파일되지 않는다.
| 파일 | 검증 대상 | | 파일 | 검증 대상 |
|---|---| |---|---|
| 01_capability_signature | use 파라미터 + effects 절, capability struct 관용구 | | 01_capability_signature | capability 파라미터, effects 절 어순, capability struct 관용구 |
| 02_higher_order_effects | effect 변수, 구문 수준 제한, use 기본 / own 유표기 | | 02_higher_order_effects | effect 변수, 구문 수준 제한, 명시적 인스턴스화 |
| 03_scope_concurrency | TaskScope, 이름 있는 scope, 중첩 시 수명 표현 | | 03_scope_concurrency | TaskScope, 이름 있는 scope, 중첩 시 수명 표현 |
| 04_enum_match_interface | enum 정의 본문이 interface surface에 들어가는 경로 | | 04_enum_match_interface | enum 정의 본문이 interface surface에 들어가는 경로 |
| 05_move_errors | **에러가 나야 하는** 코드 — 진단 하나씩 | | 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가 담아야 할 것 전부 | | 07_module_interface | interface artifact가 담아야 할 것 전부 |
`05_move_errors.cool`은 통과하면 안 되는 파일이다. 각 함수 주석의 `[E-...]` `05_move_errors.cool`은 통과하면 안 되는 파일이다. 각 함수 주석의 `[E-...]`
태그가 기대 진단이며, 체커가 생기면 그대로 테스트 케이스가 된다. 태그가 기대 진단이며, 체커가 생기면 그대로 테스트 케이스가 된다.
`E-syntax-*` 태그는 파서가, 나머지는 체커가 거부해야 한다.
## 확정된 표기 ## 확정된 표기
- 어순: `fn 이름(파라미터) effects {...} -> 반환타입`. - 어순: `fn 이름(파라미터) effects {...} -> 반환타입`.
함수 타입도 동일: `fn(a) effects e -> b` 함수 타입도 동일: `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` - 모듈: 파일 = 모듈, `pub`이 exported, `import "domain/path" as Name`
- 재수출: `reexport Name` (전용 키워드 — hash 전파를 동반하므로 구분한다) - 재수출: `reexport Name` (전용 키워드 — hash 전파를 동반한다)
- capability 파라미터: `fn f(use pay: Gateway)`
- callable 파라미터: 기본이 use(비탈출, 무표기). 저장하는 쪽만 `own fn`
- 파라미터 밖(반환 타입, struct 필드, channel 원소)의 callable: 무표기 owned
- affine 클로저 타입: `affine fn(a) effects e -> b`
- effect 집합: `effects {Type.method, ...}`, 빈 집합은 생략 - effect 집합: `effects {Type.method, ...}`, 빈 집합은 생략
- effect 변수: `<e: effects>` 선언, 합집합 `e1 | e2`는 결과 위치 전용 - effect 변수: `[e: effects]` 선언, 합집합 `e1 | e2`는 결과 위치 전용
- 제네릭: `List<a>` - 문 구분은 줄바꿈 (Go식 자동 삽입)
- scope: `scope sc { ... }` — 이름 필수, 블록 종료 시 join/cancel - 블록은 식. 마지막 식이 값이고 `return`은 조기 탈출 전용
- 오류 전파: `?` - `if``match`는 식. `match` 가드 없음
- 리스트 리터럴 `[a, b, c]`, 빈 리터럴은 타입 주석 필요
## 아직 열려 있는 것 - 오류 전파 `?``Result` 전용
- 표현식 `a < b`와 제네릭 `List<a>``<` 모호성 해소 방법
- `?`의 정확한 의미론 (Result 전용인지, 어디까지 전파하는지)
- `match` 가드, 중첩 패턴의 범위
- 리터럴 배열 `[...]`과 컬렉션 표기