D5를 고친다. 함수 타입에 own을 적을 수 없어 "소유권을 가져가는 클로저"를 표현할 수 없었고, move 검사기가 클로저 파라미터를 무조건 소유로 봐서 고차 경계에서 소유권 검사가 뚫려 있었다. 클로저 파라미터의 소유권은 리터럴이 스스로 적는다. 타입은 기대 타입에서 읽어오지만 소유권은 읽어오지 않는다 — move 검사는 타입 검사와 별도 순회라 타입을 모르고, 소유권은 타입보다 결과가 크기 때문이다. unify는 정확히 일치를 요구한다. 방향을 다루려면 부분 타입이 필요하고 없다. 그리고 구멍이 자기가 숨긴 버그를 덮고 있었다. std/list.cool의 fold가 f: fn(acc, a) -> acc 로 적혀 있었는데 틀렸다 — 누적자는 매 단계 소비되고 새것으로 바뀌므로 own이다. 빌림으로 적혀 있어 affine 값을 fold로 실어나를 수 없었는데, 클로저 파라미터를 소유로 봤으니 아무 오류도 안 났다. 고치니 samples/app이 즉시 깨졌고, own을 붙여 고쳤다. 남은 한계를 기록했다: move 검사는 타입이 없어 제네릭을 통과해 affinity를 보지 못한다. 양쪽 다 표기가 없으면 통과한다. 근본 해법은 두 순회를 합치는 것이고 v0에서는 하지 않는다. 대가도 기록했다: own이 흔해진다. fold가 항상 요구하므로 copyable 누적자에도 붙는다. 표기의 신호가 약해지는지 지켜본다. 문법 먼저 고치고 대조 장치가 파서를 지적하게 했다. 지금은 문장 500개, 파일 29개 모두 갈림 0건. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019ZVDeU6KLuUVL3gs18Hm3E
samples — 문법 탐침
여기 있는 .cool 파일은 명세가 아니라 탐침이다. 목적은
docs/thesis.md의 v0 성공 기준 중 "체감" 항목 — 확정된 규칙으로 짠 코드가
사람이 읽을 만한지 — 을 눈으로 확인하는 것. 문법 정의는 docs/grammar.ebnf.
파서는 아직 없으므로 이 파일들은 컴파일되지 않는다.
| 파일 | 검증 대상 |
|---|---|
| 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 | move/affinity 검사기가 거부해야 하는 코드 (자원을 직접 정의) |
| 06_affine_closure | callable affinity (fn vs affine fn), own과의 직교성 |
| 07_module_interface | interface artifact가 담아야 할 것 전부 |
| 08_syntax_errors | 파서가 거부해야 하는 코드 |
| 09_type_errors | 타입 검사기가 거부해야 하는 코드 (외부 타입 0개) |
| 10_effect_errors | effect 검사기가 거부해야 하는 코드 (capability를 직접 정의) |
| 11_exhaustiveness | exhaustiveness 검사기가 거부해야 하는 코드 |
05, 08, 09, 10은 통과하면 안 되는 파일이다. 각 함수 주석의 [E-...] 태그가
기대 진단이며, 넷의 목적이 다르다 — 08은 파서가, 09는 타입 검사기가,
10은 effect 검사기가, 05는 move/affinity 검사가 거부해야 한다. 단계별로
파일을 나눈 이유는 앞 단계가 첫 오류에서 멈추면 뒤 단계 케이스에 영영
도달하지 못하기 때문이다.
05, 09, 10에는 외부 타입이 하나도 없다. 전부 모듈 안에서 정의되므로 검사기가 빠져나갈 구석이 없다 — 검사기에 이빨이 있는지 보는 파일들이다. 10은 capability를 직접 정의해야 메서드의 effect가 알려지고, 05는 affinity의 뿌리가 capability라 자원 타입을 정의해야 affine임이 유도된다.
13은 두 lint다. 미사용 import는 재검사 범위를 넓히고, effect 과잉 선언은 호출자에게 없는 의무를 지운다 — 둘 다 취향이 아니라 비용이다. 미사용 import는 lint이므로 뒤 단계를 막지 않는다: 같은 파일의 타입 오류가 함께 보고된다.
12는 표준 라이브러리가 생긴 뒤에야 가능해진 파일이다. std가 없을 때는
List.each가 모르는 이름이라 조용히 통과했다 — "모르는 것을 틀렸다고 말하지
않는다"는 맞는 원칙이지만 그 그늘에 검사되지 않는 영역이 있었다.
01~04, 06, 07은 coolc check를 통과한다 (exit 0).
modules/는 모듈 경계다. coolc check modules/area.cool이 import를 따라
shapes를 먼저 검사하고, coolc iface modules/shapes.cool이 downstream이
보는 표면과 그 해시를 보여준다.
run/은 실제로 돈다. coolc run run/hello.cool. 이 파일들은 검사를
통과한다가 아니라 무엇을 출력하는지까지 말한다 — 기대 출력이 주석에 있고
같은 것을 test/가 검사한다. main이 선언한 capability만 런타임이 넘기므로,
파라미터에서 Console을 지우면 출력할 방법이 프로그램 안에 없다.
파서는 첫 오류에서 멈춘다(오류 복구 미구현). 타입 검사기는 오류를 전부 모은다.
확정된 표기
- 어순:
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 전파를 동반한다) - effect 집합:
effects {Type.method, ...}, 빈 집합은 생략 - effect 변수:
[e: effects]선언, 합집합e1 | e2는 결과 위치 전용 - 문 구분은 줄바꿈 (Go식 자동 삽입)
- 블록은 식. 마지막 식이 값이고
return은 조기 탈출 전용 if와match는 식.match가드 없음- scope:
scope inner = outer { ... }— 부모를 구문에 명시 - 타입 이름은 그 타입의 함수 이름공간:
String.len(s),File.close(f) - 리스트 리터럴
[a, b, c], 빈 리터럴은 타입 주석 필요 - 오류 전파
?는Result전용