std: 표준 라이브러리 — effect 다형성이 처음으로 검사된다

std/list.cool, string.cool, int.cool, bool.cool. 본문 없는 선언이고
런타임이 구현한다. 이 파일들은 구현이 아니라 시험대다.

부채 상환이 아니라 검증이다. std가 없을 때 List.each는 모르는 이름이라
조용히 통과했다. "모르는 것을 틀렸다고 말하지 않는다"는 맞는 원칙이지만,
그 그늘에 검사되지 않는 영역이 숨어 있었다.

넣자마자 샘플 01이 깨졌다 — List.each에 Result를 반환하는 클로저를 넘기고
그 안에서 ?를 쓰고 있었다. each는 값을 남기지 않는 클로저만 받고, ?는
클로저 밖으로 나가지 못하며, 결과를 버릴 방법은 언어에 없다. map으로
고쳤다. 이것이 std를 먼저 한 이유 그 자체다.

- IR은 이제 모듈 그래프 전체를 받고 전역 이름은 "<경로>#<이름>"으로
  정규화된다. 별칭은 가져오는 쪽의 선택이므로 실행 의미에 남아서는 안 된다.
- 본문 없는 선언은 런타임 구현으로 낮아지고, 그 이름은 모듈 파일에서 온다
  (std/list.cool의 each = "list.each"). 별칭과 무관하다.
- prelude 없음. std도 명시적으로 가져온다.
- samples/12: effect 변수가 호출 지점에서 실제로 해소된다는 증거.

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 15:47:44 +09:00
co-authored by Claude Opus 5
parent 1c4f46e5e5
commit 5593b54772
15 changed files with 386 additions and 108 deletions
+7 -4
View File
@@ -43,12 +43,15 @@ pub fn checkout(
}
// 빌린 값을 다른 빌림 자리로 넘기는 것은 허용된다 (복제가 아니다).
//
// each가 아니라 map인 이유: 결과를 버릴 방법이 언어에 없다. each는 값을
// 남기지 않는 클로저만 받으므로 Result를 삼킬 수 없고, ?는 클로저 밖으로
// 나가지 못한다. 실패를 못 본 척하려면 명시적으로 match해야 한다.
pub fn refund_all(
pay: PaymentGateway,
ids: List[OrderId],
) effects {PaymentGateway.refund} -> Result[Unit, PayError] {
List.each(ids, fn(id) {
refund_order(pay, id)?
Ok(unit)
) effects {PaymentGateway.refund} -> List[Result[Receipt, PayError]] {
List.map(ids, fn(id) {
refund_order(pay, id)
})
}
+33
View File
@@ -0,0 +1,33 @@
// 12. 표준 라이브러리 시그니처가 실제로 검사된다
//
// std가 생기기 전에는 List.each가 모르는 이름이라 조용히 통과했다.
// "모르는 것을 틀렸다고 말하지 않는다"는 맞는 원칙이지만, 그 그늘에
// 검사되지 않는 영역이 숨어 있었다. 이제 그늘이 없다.
//
// 이 파일은 세 가지 오류를 낸다.
import "cool.dev/std/list" as List
import "cool.dev/std/int" as Int
pub capability Console {
fn print(s: String) effects {Console.print}
}
// (1) effect 다형성. e는 클로저의 시그니처에서 {Console.print}로 묶이고,
// 그것이 이 함수의 effects 절 {}를 넘는다. effect 변수가 호출 지점에서
// 실제로 해소된다는 증거다 — 이게 안 걸리면 규칙이 장식이다.
pub fn leaks_effect(c: Console, xs: List[Int]) {
List.each(xs, fn(n) {
c.print(Int.show(n))
})
}
// (2) 인자 개수
pub fn wrong_arity(xs: List[Int]) -> Int {
List.len(xs, 1)
}
// (3) 반환 타입
pub fn wrong_type(xs: List[Int]) -> String {
List.len(xs)
}
+4
View File
@@ -31,6 +31,10 @@
capability를 직접 정의해야 메서드의 effect가 알려지고, 05는 affinity의 뿌리가
capability라 자원 타입을 정의해야 affine임이 유도된다.
12는 표준 라이브러리가 생긴 뒤에야 가능해진 파일이다. std가 없을 때는
`List.each`가 모르는 이름이라 조용히 통과했다 — "모르는 것을 틀렸다고 말하지
않는다"는 맞는 원칙이지만 그 그늘에 검사되지 않는 영역이 있었다.
01~04, 06, 07은 `coolc check`를 통과한다 (exit 0).
`modules/`는 모듈 경계다. `coolc check modules/area.cool`이 import를 따라
+2
View File
@@ -6,6 +6,8 @@
// err
// in scope
import "cool.dev/std/int" as Int
pub capability Console {
fn print(s: String) effects {Console.print}
}
+12
View File
@@ -3,6 +3,18 @@
// main은 자기가 선언한 capability만 받는다. Console을 파라미터에서 지우면
// print할 방법이 프로그램 안에 없다 — ambient authority가 없다는 것의
// 실행 시점 의미다.
//
// 표준 라이브러리도 명시적으로 가져온다. prelude가 없다 —
// 암묵적으로 끌어오지 않는다는 규칙에 예외를 두지 않는다.
//
// 기대 출력:
// area = 12
// area = 9
// area = 3
import "cool.dev/std/list" as List
import "cool.dev/std/string" as String
import "cool.dev/std/int" as Int
pub capability Console {
fn print(s: String) effects {Console.print}