Compare commits

...
21 Commits
Author SHA1 Message Date
coolguyandClaude Opus 5 1baef87b2e Process Supervisor: 실패 위상, 그리고 이름 한정의 구멍 둘
문서의 주장 셋이 코드가 됐다:
- spawn은 하나의 개념이고 실패가 어디까지 가느냐가 권한으로 갈린다
- 재시작 가능한 자식은 copyable 클로저여야 한다 — affinity가 검사한다.
  lease를 들고 가는 워커는 supervise에 못 간다
- 격리 경계는 recover가 아니다 — Death에 담을 수 있는 것이 메시지뿐이다

D19: 가져온 모듈의 struct를 만들 수 없었다. 타입은 쓸 수 있는데 값은 못
만드는 상태였고, 개밥 넷이 같은 모듈 안에서만 struct를 만들어 안 걸렸다.
고치는 동안 세 검증 장치가 각각 다른 실수를 잡았다 — LL(1)이 점을 두
군데서 먹는 것을, 문법-파서 대조가 f().x { }를, 생성 커버리지가 밟히지
않는 프로덕션을.

D20: 가져온 열거형의 이름을 한정 없이 쓰면 조용히 통과했다.
- 패턴에서는 바인딩이 되어 그 팔이 전부를 잡고 오류가 다음 팔에 났다.
  틀린 줄을 가리키는 진단은 없느니만 못하다
- 식에서는 모르는 이름이 되어 move 검사가 생성자인 줄 몰랐다.
  Wrap(f)를 두 번 써서 affine 값을 복제할 수 있었다 (보안 정리 ii)

Mini Shell에 다섯 군데 있었다 — 그 개밥이 시험하려던 파이프 처리 전체가
검사되지 않고 있었다. 고친 뒤에야 ReadFd/WriteFd 구분과 fd의 이중 소비가
잡힌다. D1 → D15 → D20이 같은 병의 세 단계다: 모듈 전체 → 점 왼쪽 →
점을 안 찍은 이름.

D21(열림): spawn한 태스크의 값이 갈 데가 없다. 보고를 Log로 한다.
취소·backpressure 전에 결정해야 한다.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019ZVDeU6KLuUVL3gs18Hm3E
2026-08-30 20:15:39 +09:00
coolguyandClaude Opus 5 6bfdcaa0d6 Mini Shell: 네 번째 개밥, 보안 구멍 셋, 누수 lint
Mini Shell은 앞의 셋이 못 건드린 모양을 친다 — 파이프 하나는 끝이 둘이고
각각 다른 자식에게 간다. 부모에게 아무것도 안 남고 어느 블록에도 묶이지
않는다. 가장 어려운 자원 코드가 렉시컬하지 않다.

파싱을 자원에서 떼어 놓아 개밥이 처음으로 실제로 돈다 (test 5/5 통과).

보안 정리 (ii)의 구멍 셋 — 전부 "부분 move가 없다"는 하나의 규칙이
닿아야 할 자리가 비어 있던 것:
- 열거형 생성자가 인자를 소비하지 않았다. Some(f)를 두 번 쓰면 복제됐고,
  LRU가 쓰는 우회(affine 값을 열거형으로 나르기)가 그래서 무의미했다
- 소유한 열거형을 두 번 분해할 수 있었다. match가 scrutinee를 늘 빌려서
  조각만 owned로 나갔다 — 같은 파이프에서 같은 fd를 두 번 꺼낼 수 있었다
- 점 왼쪽을 이름 해소가 검사하지 않았다. Nonsense.whatever(1)이 통과했고
  samples 셋(02, 04, 06)이 이 구멍에 기대고 있었다 (D1과 같은 병)

누수 lint를 넣었다. ?와 return 자리에서 소유하고 있고 아직 안 넘긴 affine
값을 말한다. 저장소 전체 네 곳, 오탐 0건 — naive 판만 걸리고 careful 판은
조용하다. 타입 층 정책은 그대로다 (오용 금지, 누수 허용).

결정: resource라는 새 종류는 넣지 않는다. capability가 이미 그것이고
own self가 정리를 말한다. with는 미룬다 — 두 갈래 중 고르기에 데이터가
부족하고, 셸이 with가 닿지 않는 모양을 보여줬다.

List.rest 추가 (첫 원소를 뺀 나머지).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019ZVDeU6KLuUVL3gs18Hm3E
2026-08-30 19:49:57 +09:00
coolguyandClaude Opus 5 28001d9c1d own self: capability 메서드가 수신자를 소비한다 (D4)
정리에 바깥 권한이 따라붙으면 with가 자원만 보고 무엇을 부를지 알 수 없다
(D9). own self로 자원이 자기 정리를 스스로 선언한다.

- 문법: cap_method의 첫 자리에만 온다. 좌인수분해로 LL(1) 유지 (충돌 0건)
- 빌림에는 적을 말이 없다 — self만 적는 문장은 존재하지 않는다
- own self는 interface hash에 들어간다 (호출 측 move 결과가 바뀌므로)

같이 막은 구멍 둘:
- 중첩 수신자(h.lease.release())가 시그니처를 못 찾아 조용히 빌림이 됐다.
  own self가 생기면 그것은 자원을 두 번 소비할 수 있다는 뜻이다
- 메서드 이름만으로 시그니처를 찾아 이름이 겹치면 진단이 틀렸다.
  수신자의 선언된 타입을 알면 그 capability 안에서만 찾는다

그리고 테스트 하네스의 결함:
요약과 exit 1이 파일 184줄에 있었다. 최상위 let ()은 파일 순서대로 돌므로
그 뒤 1500줄의 검사는 FAIL을 찍고도 종료 코드가 0이었다 — 실패한 테스트를
커밋할 수 있었다. 요약을 파일 끝으로 옮겼다.

개밥 먹기 네 파일을 2판으로 다시 썼다. 도우미의 시그니처가 정리의 진짜
의존을 말하게 됐다.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019ZVDeU6KLuUVL3gs18Hm3E
2026-08-30 19:36:04 +09:00
coolguyandClaude Opus 5 01d090b065 move: capability가 struct 필드로 복제되던 구멍을 막는다 (보안 정리 ii)
"발견들 다 고쳤나"를 확인하려고 전부 다시 돌려보다 드러났다.

  pub struct Wrapper { pay: Pay }
  pub fn duplicate(w: Wrapper) effects {Pay.charge} {
      consume(w.pay)
      consume(w.pay)      // 같은 capability를 두 번 소비 — 통과했다
  }

w가 빌린 값인데도 통과했다. 즉 "safe code에서 capability는 복제·위조되지
않는다"가 깨져 있었다. 이 세션에서 찾은 것 중 가장 심각하다.

원인은 단순하다. E_field가 빌린 값을 돌려주는데 Move 문맥에서 그것을
검사하는 곳이 E_ident 분기에만 있었고, 필드 접근은 그 분기를 지나가지
않는다. 규칙("v0에 부분 move는 없다")은 주석에 적혀 있었으나 강제되지
않았다.

필드의 affinity를 알아야 정확히 막을 수 있어서 move 검사기에 struct 필드
표와 바인딩의 선언 타입을 넣었다. copyable 필드는 막지 않는다 — w.label은
통과한다.

이것이 LRU에서 열거형으로 우회한 것을 사후에 정당화한다. 그때는 "struct로는
안 되고 열거형으로는 된다"가 우연처럼 보였는데, 열거형이 유일한 길인 것이
규칙이었고 struct 쪽이 새고 있었을 뿐이다.

여태 안 보인 이유: samples/05는 자원 타입을 직접 다루고 struct에 담지
않는다. 개밥 먹기에서 자원을 자료구조에 담는 코드를 처음 쓰면서 드러났다.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019ZVDeU6KLuUVL3gs18Hm3E
2026-08-30 19:21:16 +09:00
coolguyandClaude Opus 5 450e96d326 dogfoods: LRU 캐시 — 비렉시컬 정리, 그리고 Option/Result 패턴의 큰 버그
파일과 풀은 정리 지점이 렉시컬이었다. LRU는 아니다 — 축출은 삽입의 부작용으로
예측할 수 없는 때에 일어난다. with를 구현하기 전에 알아야 할 자리였다.

D10 (큰 것) — Option/Result 패턴이 타입을 통째로 잃고 있었다.
  match r { Ok(v) => v, Err(e) => e }   // Int를 String 자리에 두는데 통과
typecheck의 env.ctors에 내장 생성자가 없어서 None이 "None이라는 이름의 변수
바인딩"이 되고 Ok(v)의 v가 TUnknown이 됐다. 즉 이 언어의 핵심 오류 처리
수단의 패턴 매칭이 타입을 전혀 검사하지 않고 있었다. 소진성 검사는 자기
is_ctor에 내장을 갖고 있어 이 사실을 덮었다 — 빠진 경우는 잡으면서 타입은
안 봤다. 내장 열거형을 등록해 고쳤고 기존 코드는 하나도 안 깨졌다.
여태 안 보인 이유: Ok(v) => Int.show(v) 같은 정상 코드는 v가 TUnknown이어도
통과하므로 아무도 이상함을 못 느낀다. 틀린 코드를 써봐야 드러난다.

D11 — struct 필드에서 affine 값을 꺼낼 수 없다. 필드 접근은 빌림이고 struct
분해 패턴이 없다. 열거형은 패턴이 분해하므로 가능하다. 즉 "둘을 함께
돌려주기"가 열거형으로만 된다. 튜플이 없는 대가가 표현 가능성 문제로 나왔다.

D12 — 축출은 렉시컬이 아니다. 그래서 자원이 렉시컬 전용이면 자원 컨테이너를
언어로 만들 수 없다. 이미 그렇게 되어 있었다 — std-draft의 Pool이 불투명한
런타임 capability인 이유가 만들 수 없어서였다. (a) 렉시컬 전용을 택하고,
사용자가 자기 자원 풀을 못 만든다는 대가를 기록한다.

자료구조 자체의 마찰: Map이 없어 O(n), 튜플이 없어 운반용 struct 세 개,
get이 캐시를 새로 돌려줘야 함, 뒤에서 자르는 std 함수 없음.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019ZVDeU6KLuUVL3gs18Hm3E
2026-08-30 19:16:36 +09:00
coolguyandClaude Opus 5 088e1992a5 dogfoods: 6번 DB Pool — 두 번째 데이터 점, 그리고 resource/with 결정
파일에 없던 셋을 압박했다: 자원 두 층(풀/lease), 반납이 파괴가 아님, 획득이
실패할 수 있음. 셋 다 설계가 버텼다.

D8 — 인자 안에서 소비와 사용을 섞을 수 없다.
release_then(db, pool, l, db.query(l, sql))가 거부된다. move 검사가 인자를
왼쪽부터 걷기 때문이고, 실제 평가 순서와 무관하다. 우회는 쉽지만 왜 안 되는지가
코드에서 안 보인다.

두 사례 비교:
                naive  careful  배수  match 중첩
  Atomic          18      58    3.2    4단
  DB Pool         25      58    2.3    1단

공통(2/2): 자원을 들면 ?를 한 번도 못 쓴다. "정리하고 결과를 실어나르는
도우미"를 양쪽이 각자 발명했다. 누수는 조용히 통과한다.
다른 점: 고통이 자원 개수가 아니라 정리의 균일성에 비례한다. 파일은 단계마다
정리가 달라 4단이 되고, 풀은 언제나 반납이라 1단으로 접힌다.

결정: resource/with를 넣는다. 다만 D4가 먼저다.
with l = db.acquire(pool)? { } 가 나갈 때 무엇을 부를지 알려면 자원만으로
정리를 표현할 수 있어야 하는데, 반납은 db.release(pool, l)이라 풀과 권한이
필요하다. 자원이 자기 정리를 스스로 선언하려면 수신자를 소비하는 메서드가
있어야 하고(own self), 지금은 own이 파라미터에만 붙는다.
파일 쪽도 같다 — Fs.close(own f)는 Fs 권한을 요구한다.

따라서 순서: D4 → resource/with → Mini Shell로 세 번째 점.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019ZVDeU6KLuUVL3gs18Hm3E
2026-08-30 19:09:09 +09:00
coolguyandClaude Opus 5 d85816abae own: 함수 타입과 클로저 파라미터의 소유권 — 구멍이 숨기던 버그가 나왔다
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
2026-08-30 19:06:17 +09:00
coolguyandClaude Opus 5 410363b230 dogfoods: 2번 Atomic File Updater — ?를 자원과 함께 쓸 수 없다
프로그램을 쓰되 돌리지 않는 방식을 시작한다. std가 원래 선언-전용이므로
실행만 빼고 전부 진짜로 검사된다 — 종이 스케치가 아니라 컴파일러가 검증한
설계다.

합격 기준을 둘로 잡았다: check exit 0 + 모듈이 실제로 해소될 것. 후자가
없으면 전자가 공허한데, 그것을 첫 시도에서 겪었다.

D1 — 상대 경로 import가 패키지로 오인됐다. is_package가 첫 세그먼트에 점이
있는지만 봐서 ".."이 걸렸다. 무서운 것은 버그가 아니라 결과였다: import가
해소되지 않으면 그 모듈의 이름이 전부 불투명해지고, "모르는 것을 틀렸다고
말하지 않는다"는 원칙에 따라 무엇이든 통과한다. 첫 check가 exit 0이었는데
없는 메서드를 불러도 통과하는 상태였다.

D2 — 값 있는 식을 문으로 버릴 수 있었다. fs.remove(path)를 문으로 쓰면
Result가 조용히 사라졌다. 즉 실패를 버리는 방법이 있었고, 내가 개밥 먹기 1차
보고서와 투어에 "이 언어에는 실패를 버릴 방법이 없다"고 적은 것은 틀렸다 —
List.each 하나의 좁은 사실을 언어 전체로 일반화했다. 이제 오류이고, 일부러
버리려면 let _ = 로 적는다. 부산물로 정리 경로의 관용구가 생겼다.

D3(본체) — ?를 자원과 함께 쓸 수 없다. naive.cool 18줄은 조기 반환으로 핸들을
누수하는데 통과한다(v0 정책). 제대로 정리한 careful.cool은 58줄이고 5단 중첩
match이며 ?를 한 번도 못 쓴다. 3.2배다. resource/with가 필요한 이유가 여기
숫자로 있다.

열어둔 것: 함수 타입에 own이 없어 고차 경계에서 소유권이 뚫린다(D5).
Mini Shell과 DB Pool이 정면으로 걸리므로 그 둘 전에 결정해야 한다.
문자 접근이 없어 어휘 분석을 못 쓴다(D6).

소유권 검사가 잡는 것은 확인했다: 두 번 닫기, 닫은 뒤 쓰기, 빌린 핸들 반환.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019ZVDeU6KLuUVL3gs18Hm3E
2026-08-30 18:56:05 +09:00
coolguyandClaude Opus 5 2307bafda2 naming: panic을 crash로 — 그리고 이름을 고르는 원칙을 철학에 넣는다
철학 6번을 추가했다:

  이름은 관례가 아니라 뜻에서 고른다 — 낯섦은 한 번 치르고 끝나지만
  부정확함은 읽는 사람마다 매번 치른다.

판정 방법도 같이 적었다. 그 단어로 평범한 문장을 써 보고, 단어가 문장을
도우면 맞는 이름이고 싸우면 틀린 이름이다.
  "크래시는 복구하는 것이 아니라 조사하는 것이다" — 돕는다
  "패닉은 복구할 수 없다" — 다른 언어에서는 할 수 있어 싸운다

panic의 자연어 뜻은 "갑작스러운 공포"다. 반응하는 쪽의 감정이지 결함에
대한 말이 아니다. 그리고 Go/Rust에서는 붙잡을 수 있어 이름이 거짓말을 한다.
crash는 "계획 없이 갑자기 완전히 망가져 끝남"이고 복구의 함의가 없다 —
크래시는 복구하는 게 아니라 조사하는 것이다.

어휘의 출신도 이유가 됐다. panic+recover는 Go 전통이고 거기엔 감독이 없다.
crash+supervision은 얼랭 전통이며, 우리가 만드는 것이 그쪽이다.

한국어 용어도 세 층으로 정리했다: 실패(Result) / 결함(crash) / 감독.
세 층이 세 가지 다른 기제로 규율된다 — 타입, 없음(발산), capability.
"상황이 나쁨"은 결함이 아니라 실패다. 이 선을 안 그으면 crash가 게으름의
배출구가 된다. "오류"는 컴파일러 진단에만 쓴다.

얼랭 질문에 대한 답도 기록했다: 감독은 가져오고 비구조적 spawn은 안
가져온다. sc.spawn과 sup.spawn(sc, f)로 갈리며 문법 변경이 없다 — 실패를
삼키려면 Supervisor를 받았어야 하고 그것이 시그니처에 보인다.

개명은 문법을 먼저 고치고 대조 장치로 확인했다. 문장 500개, 파일 26개
모두 갈림 0건.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019ZVDeU6KLuUVL3gs18Hm3E
2026-08-30 18:21:05 +09:00
coolguyandClaude Opus 5 5dc214baee docs: 실패의 단위를 정한다 — panic은 scope 트리를 타고 올라간다
"panic이 unrecoverable한 건 프로세스까지인가"라는 질문에 답이 없었다.
답이 없었던 이유는 동시성 모델을 안 정했기 때문이고, 두 질문이 사실 하나다.

정한 것: 실패의 단위는 프로세스가 아니라 태스크다. panic은 콜 스택의
root가 아니라 scope 트리의 root까지 올라간다. scope가 이미 그 모양이기
때문이다 — 렉시컬이고, 블록을 나가는 것이 join이며, 자식이 죽었다는 사실이
부모에게 도달하는 지점이 문법에 이미 있다.

전파는 취소를 정하지 않고도 정의된다. 취소가 생기면 형제들이 언제 멈추는지가
바뀔 뿐 scope가 실패한다는 사실은 안 바뀐다. 그래서 지금 적어도 v1의 취소
설계를 앞당겨 닫지 않는다.

여전히 미정인 것과 그 대가도 적었다: 취소가 없으면 형제 하나가 끝나지
않을 때 죽은 자식의 panic이 join에 도달하지 못한다. 실패가 hang에 가려진다.

그리고 "격리 경계는 recover가 아니다"의 구분선을 명시했다 — 실패한 계산이
만든 값은 경계를 넘지 못하고, 경계가 얻는 것은 죽었다는 사실과 메시지뿐이다.
테스트 러너가 이미 그 원칙대로 돈다.

v0는 태스크가 하나라 규칙이 축약된 형태로만 관측되지만, 그 형태로 테스트에
고정했다. 테스트 주석에 "자식 셋이 안 도는 것은 취소가 아니라 순차 실행의
부산물"이라고 적어 뒀다 — 나중에 이걸 취소로 오해하지 않도록.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019ZVDeU6KLuUVL3gs18Hm3E
2026-08-30 17:59:37 +09:00
coolguyandClaude Opus 5 df775ac4a6 fix: std 런타임 대조가 본문 있는 함수를 요구하던 것
std/test.cool의 assert는 coollang으로 쓰여 본문이 있으므로 런타임 구현이
필요 없다. 양방향 대조가 그걸 구분하지 못했다.

앞 커밋에서 이 수정이 파일에 반영되지 않은 채 푸시됐다 — dune이 테스트를
캐시해 통과로 보였다. 이제부터 검증은 dune test --force로 한다.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019ZVDeU6KLuUVL3gs18Hm3E
2026-08-30 17:52:36 +09:00
coolguyandClaude Opus 5 78ef07d2ee panic: panic/Never와 내장 테스트 — 문법을 먼저 고치고 대조 장치가 파서를 지적했다
순서가 요점이다. 문법에 test_decl과 panic_expr을 넣고 파서는 안 고친 채로
대조 장치를 돌렸더니 즉시 잡혔다:

  문장 300개 중 파서가 거부한 것 140개
  [1] 선언 (fn, struct, enum, capability, const)이(가) 필요합니다 — test 발견

파서를 따라가게 하니 다시 0건. 문법과 구현이 어긋나는 상태가 관측 가능한
것이 되었다는 뜻이다.

panic:
- 키워드다. prelude가 없어 함수로 두면 쓸 때마다 import해야 한다
- effect가 아니다. 경계 검사 하나에 {Panic}이 호출자 전부로 전염되면
  effect 절은 신호가 아니라 잡음이 된다
- Never는 어떤 타입 자리에도 놓인다. 없으면 panic을 match 팔에서 못 쓴다
- 언어 수준 recover 없음. 되감기 없음. 자원 해제 여부는 열어둔다
- 0으로 나누기, assert 실패가 이 하나로 모인다

test:
- 파라미터가 없어 capability를 받을 수 없고, 만들 문법도 없다. 그래서
  effect-free임이 증명된다 — 관례가 아니라 검사다. 시험해 보니 실제로
  "테스트는 effect를 수행할 수 없습니다"로 거부한다
- 일반 코드와 같은 타입/effect/move 검사를 받는다
- interface hash에서 제외 — 테스트를 고쳤다고 downstream이 재검사되면 안 된다
- 격리는 런타임의 일이다. 하나가 죽어도 나머지는 돈다

assert는 std/test.cool에 coollang으로 쓰였다 — panic 위의 설탕임이 코드로
보이고, std에서 본문이 있는 첫 함수가 됐다. 그 바람에 std/런타임 양방향
테스트가 걸렸고(본문 있는 함수에 런타임 구현을 요구했다), 그 구분을 넣었다.

samples/app/config.cool에 첫 테스트 넷.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019ZVDeU6KLuUVL3gs18Hm3E
2026-08-30 17:51:56 +09:00
coolguyandClaude Opus 5 8ff35c5d9b interp: 실패해도 그때까지의 출력을 보여준다
0으로 나누는 프로그램을 돌려보니 실패 전에 출력한 것이 통째로 사라졌다.
Interp.run이 출력을 버퍼에 모았다가 성공했을 때만 돌려주고 실패하면 버렸다.

print는 실제로 일어난 effect다. 일어난 일을 안 보여주면 "어디까지 갔나"를
알 수 없고, 그게 실패했을 때 가장 먼저 보고 싶은 것이다.

Interp.run과 Session.run이 이제 (출력, 실패 여부)를 함께 돌려준다.
result가 아니라 짝인 이유는 둘이 배타가 아니기 때문이다 — 실패했어도
출력은 있다.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019ZVDeU6KLuUVL3gs18Hm3E
2026-08-30 17:21:34 +09:00
coolguyandClaude Opus 5 bb19a39e04 fuzz: 문법에서 문장을 만들어 파서에 먹인다 — 갈림 하나를 잡았다
저장소의 .cool 파일로만 대조하면 사람이 쓴 코드만 훑는다. 문법이 약속했는데
파서가 못 읽는 구석은 아무도 안 밟으면 드러나지 않는다.

lib/ebnf_gen.ml이 문법에서 문장을 만든다. 텍스트가 아니라 토큰 열을 만드는
이유는, 렉서의 줄바꿈 삽입을 거치면 문법이 허용해도 렉서가 만들 수 없는
문장이 생기는데 그건 파서의 잘못이 아니기 때문이다. 검사하려는 것은 문법과
파서 사이지 렉서가 아니다.

커버리지를 같이 잰다. 안 밟은 규칙은 시험되지 않은 규칙이므로, 통과했다는
말에 값이 없다. 현재 프로덕션 95개 전부를 밟고 거부 0건이다.

이 퍼저가 잡은 갈림 하나: 대입 왼쪽 제약. 문법은 expr_stmt = expr, ["=" expr]
로 적었는데 파서는 파싱 중에 "변수나 필드만"을 강제하고 있었다. 구문으로
가르면 ident 하나로 대입과 식이 갈리지 않아 LL(1)이 깨지므로, 제약을 이름
해소로 옮겼다. 파서는 이제 순수하게 구문만 본다.

만드는 과정에서 퍼저 자체의 함정도 하나 지났다. 처음엔 연료를 총 확장
횟수로 셌더니 선언 머리에서 다 써 버려 식과 문에 도달하지 못했고, 커버리지를
재기 전까지는 "3000개 통과"가 아무 뜻도 아니었다.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019ZVDeU6KLuUVL3gs18Hm3E
2026-08-30 17:10:20 +09:00
coolguyandClaude Opus 5 61b1920909 recognize: 문법이 직접 읽는 인식기 — 파서와 판정을 대조한다
lib/recognize.ml은 docs/grammar.ebnf를 그대로 해석해 토큰 열을 받아들일지
판정한다. AST를 만들지 않는다 — 판정만 하므로 손 파서의 진단은 그대로
남는다.

문법이 LL(1)임을 이미 검증했으므로 선택이 결정적이다. 다음 토큰이 어느
대안의 FIRST에 있는지만 보고 되돌아가지 않는다.

같은 토큰 열을 손 파서와 인식기 양쪽에 주고 판정이 갈리는지 테스트가
검사한다. 저장소의 .cool 25개 전부에서 일치한다. 갈리면 빌드가 깨진다.

이제 "설명서를 잘 관리하자"에 기대지 않는다. 그 방법은 이미 실패했다 —
설명서가 어긋났고 아무도 몰랐고 나조차 안 읽었다.

확인: 문법에서 한정 이름(named_type의 "." ident)을 빼 보면 즉시
"파서는 받고 문법은 거부"가 두 파일에서 잡힌다.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019ZVDeU6KLuUVL3gs18Hm3E
2026-08-30 17:06:02 +09:00
coolguyandClaude Opus 5 bde940eda3 grammar: 문법 문서를 기계가 읽는 소스로 — LL(1)이 처음으로 검증된다
설명서와 파서가 어긋나 있었다. 그냥 어긋난 게 아니라, 그 설명서를 안 읽고
"else if가 안 된다"고 마찰 보고서에 잘못 적었다 — 설명서로서 제 역할을 한
번도 못 했다는 뜻이다.

lib/ebnf.ml: EBNF를 데이터로 읽는다. nullable, FIRST, FOLLOW를 계산하고
LL(1) 충돌을 보고한다. 매개변수 프로덕션(name<p>)을 지원한다 — expr_ns와
목록을 복제 없이 적기 위한 것이다.

문법 첫머리의 "설계 제약: LL(1)"은 지금까지 사람의 주장이었다. 기계로 재니
충돌 18건이 나왔다. 세 부류였다:
- 구조적 3건: stmt(assign/expr), primary(ident/struct_lit), pattern —
  파서는 왼쪽 인수분해를 손으로 했는데 문법에 안 적혀 있었다
- 후행 콤마 9건: X , { "," , X } , [ "," ]는 콤마를 본 시점에 갈리지 않는다.
  우재귀로 다시 적었다
- 줄바꿈 흡수 6건: 어느 쪽이 먹어도 파스 트리가 같다. greedy 규칙을 표기에
  명시하고 그렇게 해소되는 것만 따로 분류한다

지금은 진짜 충돌 0건이고, 테스트가 이를 고정한다.

병렬 조사에서 나온 드리프트도 모두 반영했다:
- named_type, name_pattern에 한정 이름(Shapes.Shape, Shapes.Circle)
- name_pattern이 인자 0개와 괄호 없는 한정 생성자를 받는다
- string_lit의 이스케이프
- cap_method의 gen_params (파서가 이미 허용하고 있었다)
- field/variant/field_init/arm 사이의 콤마는 필수다
- stmt 사이의 NEWLINE도 필수다 — 선택적으로 적었더니 그것 하나가 LL(1)
  충돌 셋을 만들었다
- { NEWLINE }을 전부 [ NEWLINE ]으로. 렉서가 연속 줄바꿈을 만들 수 없다

expr_ns는 산문 주석이었고 파서 상태 플래그로 구현돼 있었다. 매개변수
프로덕션으로 형식화해 문맥자유가 됐다.

렉서 쪽: Token.next_kind가 모든 토큰을 한 번씩 잇는다. 나열이 빠지면
컴파일러가 지적한다. 문법 문서의 키워드 표와 줄 끝 판정 목록은 이제
lib/lexical_doc.ml이 거기서 생성하고 테스트가 대조한다.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019ZVDeU6KLuUVL3gs18Hm3E
2026-08-30 17:03:25 +09:00
coolguyandClaude Opus 5 6c0d08b6b0 std: F5 해결(List.enumerate), F4·F8은 넣지 않는 것으로 닫음
F5 — "튜플이 없어서 enumerate가 불가능하다"고 적었는데 틀렸다. 제네릭
struct 하나면 된다. fold_indexed보다 이쪽이 낫다: enumerate 하나가 기존
each/map/filter/fold 전부와 조합되고, fold_indexed를 만들면 map_indexed,
each_indexed가 따라와 "한 개념 한 방식"을 깬다.
samples/app에서 Numbered struct가 사라졌다 (244줄 → 232줄).

F2에 이어 두 번째로 관찰자가 틀린 사례다. 마찰 8건 중 2건이 "언어가 못
한다"고 적었다가 확인해 보니 되는 것이었다.

F4 — 넣지 않는다. 전체 나열이 귀찮은 것은 맞지만 그 귀찮음이 값을 한다.
필드를 추가하면 모든 생성 지점이 컴파일 오류를 내고, 컴파일러가 전부
방문하도록 강제한다. ..base는 그것을 없앤다. "오류를 더 빨리 잡는다"가
1번 목표인데 F4는 정확히 그것을 깎는 거래다.

F8 — 넣지 않는다. 진짜 질문은 "미사용 지역 변수를 잡을 것인가"가 아니라
"경고 등급을 만들 것인가"였다. 오류로 넣으면 성가시고, 경고로 넣으면 경고
등급의 첫 입주자가 된다. 경고가 없다는 것은 이 언어의 좋은 성질이고 죽은
지역 변수 하나 때문에 팔 것이 아니다.

둘 다 "보류"가 아니라 "닫음"으로 적는다. 근거를 적어두지 않으면 다음에
같은 논의를 처음부터 다시 한다. 다시 열 조건도 함께 적었다.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019ZVDeU6KLuUVL3gs18Hm3E
2026-08-30 16:30:56 +09:00
coolguyandClaude Opus 5 5e70712578 friction: F8 추가 — 미사용 지역 변수를 안 잡는다, 그리고 현황표
투어용 예제를 쓰다 발견했다. 미사용 import는 오류로 막으면서 미사용 지역
변수와 파라미터는 통과시킨다. 규칙이 고르지 않아 보인다.

다만 근거의 성격이 다르다. import를 막은 이유는 재검사 범위를 넓히기
때문이고 그건 이 아키텍처의 실제 비용인데, 죽은 지역 변수에는 그 비용이
없다. 그래서 "고치면 되는 항목"이 아니라 판단이 필요한 항목으로 적는다.

지금은 고치지 않는다. 다음 개밥 먹기에서 죽은 지역 변수를 실제로 남긴 적이
있는지 세어보고 결정한다. 셀 근거가 없으면 넣지 않는 것이 기능 추가 관문의
기본값이다.

문서 맨 앞에 현황표를 넣었다 — 무엇이 해결됐고 무엇이 열려 있는지가 한눈에
보여야 한다.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019ZVDeU6KLuUVL3gs18Hm3E
2026-08-30 16:27:08 +09:00
coolguyandClaude Opus 5 a9f6527825 std: 마찰 보고의 F1/F3/F6/F7 처리 — 292줄이 244줄로
문법은 건드리지 않았다. 표본이 한 사람이 쓴 300줄 하나뿐인데 되돌리기
비싼 축을 움직일 수는 없다. std 보강만 했다.

추가: List.first/nth, String.join, std/option.cool, std/result.cool.

samples/app 재작성 결과 config.cool 196 → 148줄. head_or, second_or,
Pick, take_at, first_text, first_entry가 통째로 사라졌다. 예측 40줄,
실제 48줄 — F1의 값이 확인됐다.

F3은 줄 수로 값이 안 보인다. main.cool은 96줄 그대로다. 4단 중첩
String.concat이 4줄짜리 String.join 배열이 됐으니 줄 수가 같다. 읽기는
확실히 나아졌다. 줄 수는 읽기 좋음의 대리 지표일 뿐이고 여기서 그 대리가
깨진다 — 다음 개밥 먹기는 다른 것을 재야 한다.

F7: Interp.implemented와 std/*.cool 선언이 서로를 덮는지 테스트가 양방향
으로 검사한다. 어긋나면 빌드가 깨진다.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019ZVDeU6KLuUVL3gs18Hm3E
2026-08-30 16:20:23 +09:00
coolguyandClaude Opus 5 7be05be77f friction: F2 취소 — else if는 원래 된다
"else if가 없어서 3~4단 중첩이 된다"고 적었는데 틀렸다. parser.ml:480이
처음부터 else 뒤의 if를 처리한다. 확인하지 않고 습관대로 중첩해 쓰고
언어를 탓한 것이다.

고쳐 쓰니 config.cool이 208줄에서 196줄이 되고 parse_line은 4단 중첩에서
평평한 5갈래가 됐다.

개밥 먹기 자체에 대한 교훈이라 문서에 남긴다. 한 사람이 쓴 300줄에서 나온
불편은 언어의 성질일 수도 있고 그 사람의 습관일 수도 있다. 확인 없이 적은
것 하나가 "심각" 등급을 달고 v1 설계 입력이 될 뻔했다.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019ZVDeU6KLuUVL3gs18Hm3E
2026-08-30 16:17:30 +09:00
coolguyandClaude Opus 5 5831af7760 app: 개밥 먹기 — 일하는 프로그램 하나와 그 마찰 보고
1단계 최소 IO: File(읽기), Args capability. 권한의 출처는 여전히 런타임
하나이고, IO 오류는 Result[a, String]이다 — 런타임이 사용자 정의 enum을
만들 수 없고, 만들 수 있게 하면 런타임이 프로그램의 타입을 알아야 한다.

2단계 std 확장: fold, filter, push, concat, reverse, is_empty,
String.split/trim/starts_with/contains, Int.parse.

3단계 samples/app: 설정 파서 + 리포트 도구, 2모듈 304줄. 검사기를 시험
하려고 쓴 것이 아니라 일을 하려고 쓴 첫 프로그램이다.

산출물은 프로그램이 아니라 docs/friction.md다. 요약:
- 되돌리기 비싼 결정은 하나도 후회되지 않았다. capability 전달, effect
  명시, 실패를 버릴 수 없음, 소진적 match — 300줄 내내 거추장스럽지
  않았고 소진성은 실제로 실수를 잡았다(Value에 경우 하나 추가하니 고칠
  자리 넷을 정확히 짚었다).
- 불편은 전부 되돌리기 싼 것들이었다. 리스트 n번째 접근이 없어 fold로
  우회(40줄), else if가 없어 3~4단 중첩, String.concat이 2항이라 중첩
  지옥. 304줄 중 70줄쯤이 이 셋 때문에 존재한다.

v0의 질문은 "되돌리기 비싼 결정이 옳은가"였고 답은 그렇다이다.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019ZVDeU6KLuUVL3gs18Hm3E
2026-08-30 16:06:08 +09:00
51 changed files with 6224 additions and 264 deletions
+22
View File
@@ -60,6 +60,28 @@ m100.cool:15:3: match가 모든 경우를 덮지 않습니다 (빠진 경우: Tr
m101.cool:21:3: match가 모든 경우를 덮지 않습니다 (빠진 경우: Up.Tri(_)) m101.cool:21:3: match가 모든 경우를 덮지 않습니다 (빠진 경우: Up.Tri(_))
``` ```
## 실제로 써본 결과
`samples/app`은 검사기를 시험하려고 쓴 것이 아니라 일을 하려고 쓴
프로그램이다 — 설정 파서 + 리포트 도구, 2모듈 232줄.
```
$ coolc run samples/app/main.cool samples/app/example.conf
설정
name = "coollang" (text)
threads = 4 (number)
...
항목 5개, 문제 2개
문제
10행: 이름이 비어 있습니다
11행: = 가 하나여야 합니다: broken = a = b
```
쓰면서 걸린 것들을 [`docs/friction.md`](docs/friction.md)에 남겼다.
요약하면: **되돌리기 비싼 결정은 하나도 후회되지 않았고, 불편은 전부
되돌리기 싼 것들이었다.**
## 빌드 ## 빌드
OCaml 5.x와 dune이 필요하다. OCaml 5.x와 dune이 필요하다.
+38 -5
View File
@@ -3,8 +3,9 @@ let usage =
사용법: 사용법:
coolc check <file.cool>... 타입/effect/capability 검사 (import를 따라 모듈 그래프 전체) coolc check <file.cool>... 타입/effect/capability 검사 (import를 따라 모듈 그래프 전체)
coolc test <file.cool> [필터] 모듈 그래프의 test 블록 실행
coolc iface <file.cool> interface 표면과 해시 출력 coolc iface <file.cool> interface 표면과 해시 출력
coolc run <file.cool> typed IR 인터프리터로 실행 coolc run <file.cool> [인자...] typed IR 인터프리터로 실행
coolc tokens <file.cool> 토큰 덤프 (렉서 디버깅) coolc tokens <file.cool> 토큰 덤프 (렉서 디버깅)
coolc ast <file.cool> 구문 트리 덤프 (파서 디버깅) coolc ast <file.cool> 구문 트리 덤프 (파서 디버깅)
coolc deps <file.cool> 외부 참조 목록 (모듈의 의존 표면) coolc deps <file.cool> 외부 참조 목록 (모듈의 의존 표면)
@@ -76,13 +77,45 @@ let () =
match List.tl argv with match List.tl argv with
| "check" :: files -> check_graph files | "check" :: files -> check_graph files
| [ "iface"; file ] -> dump_iface file | [ "iface"; file ] -> dump_iface file
| [ "run"; file ] -> ( | "test" :: file :: rest -> (
let filter = match rest with f :: _ -> f | [] -> "" in
let st = Coollang.Session.create ~root:(Filename.dirname file) () in let st = Coollang.Session.create ~root:(Filename.dirname file) () in
match Coollang.Session.run st file with match Coollang.Session.test ~filter st file with
| Ok out -> | Error errors ->
List.iter
(fun e -> prerr_endline (Coollang.Session.string_of_error e))
errors;
1
| Ok results ->
let failed =
List.filter
(fun (r : Coollang.Interp.test_result) -> r.t_failure <> None)
results
in
List.iter
(fun (r : Coollang.Interp.test_result) ->
match r.t_failure with
| None -> ()
| Some (p, msg) ->
Printf.printf "FAIL %s:%d %s\n %s\n" r.t_module
r.t_pos.line r.t_name msg;
ignore p)
results;
Printf.printf "%s 테스트 %d개 중 %d개 통과\n"
(if failed = [] then "ok " else "실패")
(List.length results)
(List.length results - List.length failed);
if failed = [] then 0 else 1)
| "run" :: file :: args -> (
let st = Coollang.Session.create ~root:(Filename.dirname file) () in
match Coollang.Session.run ~args st file with
| out, None ->
print_string out; print_string out;
0 0
| Error e -> | out, Some e ->
(* 실패해도 그때까지의 출력을 먼저 보여준다 *)
print_string out;
flush stdout;
prerr_endline (Coollang.Session.string_of_error e); prerr_endline (Coollang.Session.string_of_error e);
1) 1)
| [ "tokens"; file ] -> dump_tokens file | [ "tokens"; file ] -> dump_tokens file
+383
View File
@@ -0,0 +1,383 @@
# 개밥 먹기 보고 — samples/app을 쓰면서 걸린 것들
2026-08-30. coollang으로 처음 쓴 "일하는 프로그램" 하나(설정 파서 + 리포트
도구, 2모듈 292줄)에서 실제로 걸린 마찰을 적는다.
v0의 샘플 16개는 전부 검사기를 시험하려고 쓴 것이고, 그래서 "언어가 쓸
만한가"에 대해서는 아무것도 말해주지 않았다. 이 문서가 v0가 남기는 마지막
데이터이자 v1 설계의 첫 입력이다.
기록 원칙: **불편은 증거와 함께 적고, 해법은 제안까지만 한다.** 여기서
바로 고치면 그것은 개밥 먹기가 아니라 기능 추가가 된다.
---
## 현황 한눈에
| | 항목 | 상태 |
|---|---|---|
| F1 | 리스트의 n번째를 꺼낼 방법이 없다 | **해결**`List.first`, `List.nth` |
| F2 | ~~`else if`가 없다~~ | **취소** — 관찰자가 틀렸다 |
| F3 | `String.concat`이 2항이라 중첩 지옥 | **해결**`String.join` |
| F4 | struct의 한 필드만 바꿀 방법이 없다 | **닫음 — 넣지 않는다** |
| F5 | fold에 인덱스가 없다 | **해결**`List.enumerate` |
| F6 | Option/Result에 조작 함수가 없다 | **해결**`std/option`, `std/result` |
| F7 | std와 인터프리터가 어긋날 수 있다 | **해결** — 양방향 테스트 |
| F8 | 미사용 지역 변수·파라미터를 안 잡는다 | **닫음 — 넣지 않는다** |
여덟 항목이 전부 처리됐다. 다섯은 해결, 하나는 취소(관찰자 오류), 둘은
"넣지 않는다"로 닫았다.
**닫은 둘을 "보류"가 아니라 "닫음"으로 적는 이유**: 근거까지 적어두지
않으면 다음에 같은 논의를 처음부터 다시 하게 된다. 마음이 바뀔 조건도
같이 적었다 — 그 조건이 오면 다시 연다.
## 잘 된 것부터
**1. 시그니처가 프로그램의 전부를 말한다.**
```cool
pub fn main(c: Console, f: File, a: Args)
effects {Console.print, File.read, Args.all}
```
이 한 줄을 읽으면 이 프로그램이 할 수 있는 일이 끝난다. 네트워크를 쓸 수
없고, 다른 파일을 쓸 수 없고, 프로세스를 띄울 수 없다 — 문서가 아니라
컴파일러가 보장한다. 300줄을 쓰는 내내 이것이 어색하지 않았다. 오히려
`f.read`를 쓰려고 `File`을 인자에 추가하는 순간이 "이 함수가 권한을 하나
더 갖는다"는 사실을 자각하게 만들었다.
**2. `?`가 기대대로 동작했다.**
```cool
let path = first_arg(a.all())?
let text = f.read(path)?
```
읽기 좋고, 실패 경로가 보이고, 삼켜지지 않는다.
**3. 소진성이 실전에서 값을 했다.**
`Value`에 경우 하나(`List`)를 추가해 봤다:
```
config.cool:54:5: 빠진 경우: List(_)
config.cool:62:5: 빠진 경우: List(_)
config.cool:196:5: 빠진 경우: List(_)
config.cool:204:5: 빠진 경우: List(_)
```
고쳐야 할 자리 넷을 전부, 정확히 짚었다. 이것이 없으면 `show_value`
고치고 `get_int`를 잊는다.
**4. 모듈 경계가 자연스러웠다.** 파싱(`config`)과 출력(`main`)을 나누는 데
마찰이 없었고, `Cfg.Config` 같은 한정 이름이 오히려 읽기 좋았다.
---
## 걸린 것 — 심각한 순서로
### F1. 리스트의 n번째를 꺼낼 방법이 없다 (심각)
인덱싱 연산자를 뺀 결정 자체는 옳다고 본다. 그런데 `std`에 대안이 없다.
"첫 원소"를 꺼내려고 이 코드를 네 번 썼다:
```cool
pub fn first_text(xs: List[String]) -> Option[String] {
List.fold(xs, None, fn(acc, x) {
match acc {
Some(prev) => Some(prev),
None => Some(x),
}
})
}
```
`String.split(line, "=")`의 결과에서 앞의 둘을 꺼내는 데는 보조 struct
`Pick`까지 만들어야 했다 — 순전히 "몇 번째를 보고 있는가"를 나르려고.
**304줄 중 약 40줄이 이 문제 하나 때문에 존재한다.**
> 제안: `List.first`, `List.nth(xs, i) -> Option[a]`, 그리고 `String.split`
> 같은 자리에서 흔한 `List.split_first(xs) -> Option[(a, List[a])]`.
> 튜플이 없으므로 마지막 것은 문법 결정을 동반한다.
### F2. ~~`else if`가 없다~~ — 취소. 관찰자가 틀렸다
처음 이 문서를 쓸 때 "`else if`가 없어서 3~4단 중첩이 된다"고 적었다.
**틀렸다.** `else if`는 처음부터 된다 (`lib/parser.ml:480`이 명시적으로
`else` 뒤의 `if`를 처리한다).
그런데도 `config.cool`을 중첩 `if`로 썼다. 언어가 강제한 것이 아니라
내가 확인하지 않고 습관대로 쓴 것이고, 그다음 언어를 탓했다.
고쳐 쓰니 208줄이 196줄이 됐고 `parse_line`은 4단에서 평평한 5갈래가
됐다:
```cool
if String.is_empty(line) {
cfg
} else if String.starts_with(line, "#") {
cfg
} else if List.len(parts) != 2 {
add_problem(cfg, no, ...)
} else if String.is_empty(String.trim(head_or(parts, ""))) {
add_problem(cfg, no, "이름이 비어 있습니다")
} else {
add_entry(cfg, ...)
}
```
**개밥 먹기 자체에 대한 교훈이다.** 한 사람이 쓴 300줄에서 나온 불편은
언어의 성질일 수도 있고 그 사람의 습관일 수도 있다. 둘을 나누려면 불편을
적을 때마다 "언어가 정말 막는가"를 확인해야 한다. 확인 없이 적은 것 하나가
"심각" 등급을 달고 v1 설계 입력이 될 뻔했다.
나머지 항목들은 확인했다 — F1은 `List.first`/`nth`가 std에 실제로 없고,
F3은 `String.join`이 실제로 없다.
### F3. `String.concat`이 2항이라 중첩 지옥이 된다 (심각)
리포트 한 줄이 이렇게 생겼다:
```cool
c.print(String.concat(" ", String.concat(e.key,
String.concat(" = ", String.concat(Cfg.show_value(e.value),
String.concat(" (", String.concat(Cfg.type_name(e.value), ")")))))))
```
읽을 수 없다. 이 프로그램에서 가장 나쁜 코드이고, 원인은 명확하다.
> 제안 두 가지. (a) `String.join(sep, List[String])` — 작고 안전하다.
> (b) 문자열 보간 `"${key} = ${value}"` — 훨씬 낫지만 문법과 타입 규칙을
> 정해야 하고, "한 개념 한 방식"에서 concat과 겹친다.
> 지금 판단으로는 (a)를 먼저 넣고 (b)는 체감 데이터를 더 모은 뒤.
### F4. struct의 한 필드만 바꿀 방법이 없다 — 닫음. 넣지 않는다
```cool
pub fn add_entry(cfg: Config, e: Entry) -> Config {
Config {
entries: List.push(cfg.entries, e),
problems: cfg.problems, // ← 안 바뀌는데 적어야 한다
}
}
```
필드가 둘이라 견딜 만하지만 다섯이면 못 쓴다. `take_at`은 세 필드를 매번
전부 나열한다.
> 처음 제안: `Config { ..cfg, entries: x }`. affine 타입에서는 `cfg`가
> move되는 것이므로 소유권 규칙과 충돌하지 않는다고 적었다.
**결론: 넣지 않는다.** 처음 적을 때 놓친 것이 있다.
전체 나열이 귀찮은 것은 맞다. 그런데 **그 귀찮음이 값을 한다.** `Config`
필드를 하나 추가하면 지금은 모든 생성 지점이 "필드가 빠졌다"고 컴파일
오류를 낸다. 컴파일러가 전부 방문하도록 강제한다.
`..cfg`를 넣으면 그것이 사라진다. 새 필드가 조용히 base의 값을 이어받고,
정말로 손봐야 했던 자리를 지나친다.
**"오류를 더 빨리 잡는다"가 1번 목표인데 F4는 정확히 그것을 깎는 거래다.**
브레비티를 얻고 강제 방문을 잃는다. 그 거래가 옳다는 근거가 지금 없다.
소유권 규칙과 충돌하지 않는다는 처음의 관찰은 맞지만, 충돌하지 않는 것과
넣을 값이 있는 것은 다른 문제다.
> 다시 열 조건: 다음 개밥 먹기에서 필드 5개 이상인 struct의 갱신 함수가
> 여러 개 나오고, **매번의 전체 나열이 실제로 아무것도 잡지 못했다면**
> 그때 넣는다. 반대로 한 번이라도 "필드가 빠졌다"가 진짜 버그를 잡았다면
> 이 항목은 영구히 닫힌다.
### F5. fold에 인덱스가 없다 — 해결 (`List.enumerate`)
줄 번호를 세려고 struct를 하나 더 만들었다:
```cool
pub copyable struct Numbered {
no: Int,
cfg: Config,
}
```
"인덱스가 필요한 fold"는 드문 요구가 아니다.
> 처음 제안: `List.fold_indexed`. `List.enumerate`가 더 조합적이지만
> 튜플이 없어서 불가능하다고 적었다.
**"튜플이 없어서 불가능하다"가 틀렸다.** 제네릭 struct 하나면 된다:
```cool
pub copyable struct Indexed[a] {
i: Int,
value: a,
}
pub fn enumerate[a](xs: List[a]) -> List[Indexed[a]]
```
`fold_indexed`보다 이쪽이 낫다. `enumerate` 하나가 기존 `each`/`map`/
`filter`/`fold` 전부와 조합된다. `fold_indexed`를 만들면 `map_indexed`,
`each_indexed`가 따라오고 그것이 "한 개념 한 방식"을 깨는 방향이다.
`samples/app`에서 `Numbered` struct가 사라졌다 (244줄 → 232줄):
```cool
// 전 — 줄 번호를 나르려고 struct를 하나 더 만들었다
let start = Numbered { no: 1, cfg: Config { entries: [], problems: [] } }
List.fold(lines, start, fn(acc, line) {
Numbered { no: acc.no + 1, cfg: parse_line(acc.cfg, acc.no, line) }
}).cfg
// 후
let lines = List.enumerate(String.split(text, "\n"))
List.fold(lines, empty, fn(cfg, l) { parse_line(cfg, l.i + 1, l.value) })
```
이것이 **관찰자가 틀린 두 번째 사례**다(F2에 이어). 둘 다 "언어가 못
한다"고 적었는데 확인해 보니 됐다. 마찰을 적을 때 "정말 막히는가"를
확인하는 절차가 없으면 이런 것이 v1 설계 입력으로 들어간다.
### F6. Option/Result에 조작 함수가 하나도 없다 (중간)
`map`, `unwrap_or`, `or_else`가 없어서 전부 `match`로 풀었다. `match`
나쁜 것은 아니지만, 세 줄이면 될 것이 여섯 줄이 된다.
> 제안: `Option.map/unwrap_or`, `Result.map/map_err/unwrap_or`.
> 타입 이름이 함수의 이름공간이라는 규칙이 이미 있으므로 문법 결정은 없다.
### F7. std를 늘릴 때마다 인터프리터를 고쳐야 한다 (구조)
`std/list.cool``filter`를 적으면 `lib/interp.ml`에도 구현을 넣어야
한다. 두 곳이 어긋나면 검사는 통과하고 실행이 죽는다.
이것은 v0의 구조적 한계이고 v1에서 사라진다(std를 coollang으로 구현).
다만 v0 동안은 **std 시그니처와 런타임 구현이 일치하는지 검사하는 테스트**가
있어야 한다. 지금은 없다.
---
### F8. 미사용 지역 변수와 파라미터를 안 잡는다 — 닫음. 넣지 않는다
투어용 예제를 쓰다 발견했다. 이 코드가 아무 말 없이 통과한다:
```cool
pub fn f(used: Int, never_used: Int) -> Int {
let alive = used + 1
let dead = 999
alive
}
```
**미사용 import는 오류로 막아놓고 미사용 지역 변수는 통과시킨다.** 둘 다
"쓰지 않는 선언"인데 한쪽만 잡으니 규칙이 고르지 않아 보인다.
다만 근거의 성격은 다르다. import를 막은 이유는 재검사 범위를 넓히기
때문이었고 — 그건 이 아키텍처의 실제 비용이다 — 미사용 지역 변수에는
그런 비용이 없다. 그냥 죽은 코드다.
그래서 이것은 "고치면 되는 항목"이 아니라 **판단이 필요한 항목**이다.
철학 1(오류를 더 빨리 잡는다)에는 부합하지만, 디버깅 중에 한 줄 주석
처리했다고 컴파일이 막히는 것은 실제로 성가시다. 그 성가심이 잡아주는
버그보다 큰지는 지금 데이터로 알 수 없다.
**결론: 넣지 않는다.** 그리고 처음에 질문을 잘못 잡았다.
진짜 질문은 "미사용 지역 변수를 잡을 것인가"가 아니라 **"coollang에 경고
등급을 만들 것인가"**다.
지금 이 언어에는 경고가 없다. 모든 진단이 오류이고 빌드를 멈춘다. 미사용
지역 변수를 그 등급에 넣으면 디버깅 중에 한 줄 주석 처리했다고 컴파일이
막힌다 — 실제로 성가시다. 그래서 자연스럽게 "경고로 만들자"가 나오는데,
그것이 **경고 등급의 첫 입주자**가 된다. 경고 등급은 한 번 생기면 자란다.
무시되는 진단이 쌓이는 언어가 된다.
**경고가 없다는 것은 지금 이 언어의 좋은 성질이고, 죽은 지역 변수 하나
때문에 팔 것이 아니다.**
미사용 import와의 불일치는 감수한다. 근거가 다르다 — import는 재검사
범위라는 이 아키텍처의 실제 비용을 만들고, 죽은 지역 변수는 아무 비용도
만들지 않는다. 규칙이 고르지 않아 보이는 것과 근거가 없는 것은 다르다.
> 다시 열 조건: 경고 등급을 다른 이유로 만들게 되는 날. 그때는 이 항목이
> 첫 입주자가 아니라 두 번째가 되므로 비용 계산이 달라진다.
---
## 후속 (같은 날)
F1, F3, F6을 std 보강으로 처리하고 `samples/app`을 다시 썼다. 문법은
건드리지 않았다 — 표본이 한 사람이 쓴 300줄 하나뿐인데 되돌리기 비싼 축을
움직일 수는 없다.
추가한 것: `List.first`, `List.nth`, `String.join`, 그리고 `std/option.cool`,
`std/result.cool` (`map`, `unwrap_or`, `ok_or`, `map_err`, `is_ok`).
결과:
| | 전 | 후 |
|---|---|---|
| config.cool | 196줄 | **148줄** |
| main.cool | 96줄 | 96줄 |
| 합계 | 292줄 | **244줄** |
출력은 한 글자도 다르지 않다.
**F1의 값이 확인됐다.** 48줄이 사라졌고 전부 config.cool에서 나왔다 —
`head_or`, `second_or`, `Pick`, `take_at`, `first_text`, `first_entry`
통째로 없어졌다. 예측(40줄쯤)과 실제(48줄)가 맞았다.
```cool
// 전
let key = String.trim(head_or(parts, "")) // + Pick struct + take_at 20줄
// 후
let key = String.trim(Option.unwrap_or(List.nth(parts, 0), ""))
```
**F3은 줄 수로는 값이 안 보인다.** main.cool이 96줄 그대로다. 4단 중첩
`String.concat`을 4줄짜리 `String.join` 배열로 바꿨으니 줄 수가 같다.
그런데 읽기는 확실히 낫다:
```cool
// 전
c.print(String.concat(" ", String.concat(e.key,
String.concat(" = ", String.concat(Cfg.show_value(e.value),
String.concat(" (", String.concat(Cfg.type_name(e.value), ")")))))))
// 후
c.print(String.join("", [
" ", e.key, " = ", Cfg.show_value(e.value),
" (", Cfg.type_name(e.value), ")",
]))
```
**줄 수는 읽기 좋음의 대리 지표일 뿐이고, F3에서 그 대리가 깨진다.**
다음 개밥 먹기에서는 줄 수 말고 다른 것을 재야 한다.
**F7도 처리했다.** `Interp.implemented` 목록과 `std/*.cool`의 선언이
서로를 덮는지 테스트가 양방향으로 검사한다. 어긋나면 빌드가 깨진다.
남은 것 없음. F5는 `List.enumerate`로 해결했고(244줄 → 232줄), F4와 F8은
"넣지 않는다"로 닫았다. 둘 다 다시 열 조건을 함께 적었다.
---
## 결론
(아래는 처음 쓸 때의 결론이다. 후속에서 F1·F3·F5·F6·F7이 해결되고 F4·F8이
닫혔으므로 지금은 역사적 기록이다.)
v1로 넘길 때 **F1과 F3이 먼저다.** 둘 다 "언어가 틀렸다"가 아니라
"std에 없어서 우회했다"이고, 우회 비용이 코드에 그대로 보인다 — 292줄 중
40줄쯤이 F1 하나 때문에 존재한다.
F2는 취소됐다. 그리고 그것이 이 문서에서 두 번째로 중요한 발견이다:
확인 없이 적은 불편 하나가 "심각" 등급을 달고 v1 설계 입력이 될 뻔했다.
반대로 **되돌리기 비싼 결정들은 하나도 후회되지 않았다.** capability를
인자로 나르는 것, effect를 시그니처에 적는 것, 실패를 버릴 수 없는 것,
소진적 match — 300줄을 쓰는 동안 이 넷이 거추장스러웠던 순간이 없었고,
소진성은 오히려 실수를 잡아줬다.
v0의 질문은 "되돌리기 비싼 결정이 옳은가"였다. 답은 **그렇다**이고,
남은 불편은 전부 되돌리기 싼 것들이다.
+205 -83
View File
@@ -7,10 +7,24 @@
* { } (0 ) * { } (0 )
* ( ) * ( )
* " " * " "
* < > ( )
* (* *) * (* *)
* *
* : LL(1). backtracking 없음, . * : LL(1). backtracking 없음, .
* ( 2). * ( 2).
* tools/ebnf_tool.exe가 이 파일을 읽어
* FIRST/FOLLOW를 계산하고 충돌을 보고하며, .
*
* :
* name<p> = ... p ... name<arg> .
* .
* (name<yes> = ... ; name<no> = ... ;).
* expr_ns를 복제 없이 적기 위한 것이다.
*
* :
* [ NEWLINE ] (greedy).
* .
* , .
*) *)
(* ------------------------------------------------------------------ *) (* ------------------------------------------------------------------ *)
@@ -20,43 +34,84 @@
(* // . ( ) *) (* // . ( ) *)
(* NEWLINE : (* NEWLINE :
* ident, , ")", "]", "}", "?", "return" * NEWLINE .
* NEWLINE . . * .
* . * . .
* NEWLINE . * NEWLINE .
* *
* NEWLINE . " "
* NEWLINE .
* { NEWLINE } [ NEWLINE ].
*
(* lib/token.ml *)
* .
* NEWLINE이 삽입된다. Token.can_end_statement가 원본이다.
*
* ident | int_lit | string_lit | "return" | "true" | "false" | ")" |
* "}" | "]" | "?"
*
* . . Token.keyword가 원본이다.
*
* "pub" | "fn" | "struct" | "enum" | "capability" | "const" |
* "import" | "as" | "reexport" | "let" | "mut" | "own" | "self" |
* "affine" | "copyable" | "effects" | "return" | "if" | "else" |
* "match" | "scope" | "crash" | "test" | "true" | "false"
(* *)
*
* (, , , variant, ) * (, , , variant, )
* . NEWLINE을 만들지 않으므로 목록이 자연히 이어진다. * . NEWLINE을 만들지 않으므로 목록이 자연히 이어진다.
* formatter가 이를 강제한다. * formatter가 이를 강제한다.
* *
* (struct , enum , struct , effect , match )
* NEWLINE .
* . { NEWLINE } .
*
* NEWLINE이 문법적으로 허용되고 무시된다. effects 절이 * NEWLINE이 문법적으로 허용되고 무시된다. effects 절이
* "}" NEWLINE이 삽입되는데, * "}" NEWLINE이 삽입되는데,
* { NEWLINE } . * [ NEWLINE ] .
* . * .
*) *)
ident = letter , { letter | digit | "_" } ; ident = letter , { letter | digit | "_" } ;
int_lit = digit , { digit | "_" } ; int_lit = digit , { digit | "_" } ;
string_lit = '"' , { char - '"' } , '"' ; string_lit = '"' , { str_char } , '"' ;
str_char = ( char - '"' - "\" ) | escape ;
escape = "\" , ( "n" | "t" | "\" | '"' ) ;
bool_lit = "true" | "false" ; bool_lit = "true" | "false" ;
literal = int_lit | string_lit | bool_lit ; literal = int_lit | string_lit | bool_lit ;
(* ------------------------------------------------------------------ *)
(* *)
(* ------------------------------------------------------------------ *)
(* . LL(1)
* X , { "," , X } , [ "," ] " "
* " " .
* , . *)
list<item> = item , list_rest<item> ;
list_rest<item> = [ "," , [ list<item> ] ] ;
(* . .
* . *)
brace_list<item> = [ NEWLINE ] , [ item , brace_rest<item> ] ;
brace_rest<item> = [ NEWLINE ] ,
[ "," , [ NEWLINE ] , [ item , brace_rest<item> ] ] ;
(* ------------------------------------------------------------------ *) (* ------------------------------------------------------------------ *)
(* *) (* *)
(* ------------------------------------------------------------------ *) (* ------------------------------------------------------------------ *)
module = { NEWLINE } , { item } ; module = [ NEWLINE ] , { item } ;
item = ( import | reexport | decl ) , { NEWLINE } ; item = ( import | reexport | test_decl | decl ) , [ NEWLINE ] ;
(* decl NEWLINE 0 .
* stmt NEWLINE *)
import = "import" , string_lit , "as" , ident ; import = "import" , string_lit , "as" , ident ;
reexport = "reexport" , ident ; reexport = "reexport" , ident ;
(* . pub
* .
* .
* capability , capability
* . effect-free .
* , , .
* interface hash ( ) *)
test_decl = "test" , string_lit , block ;
decl = [ "pub" ] , ( fn_decl | struct_decl | enum_decl decl = [ "pub" ] , ( fn_decl | struct_decl | enum_decl
| capability_decl | const_decl ) ; | capability_decl | const_decl ) ;
@@ -65,53 +120,62 @@ decl = [ "pub" ] , ( fn_decl | struct_decl | enum_decl
(* ------------------------------------------------------------------ *) (* ------------------------------------------------------------------ *)
fn_decl = "fn" , ident , [ gen_params ] , "(" , [ params ] , ")" , fn_decl = "fn" , ident , [ gen_params ] , "(" , [ params ] , ")" ,
{ NEWLINE } , [ NEWLINE ] ,
[ eff_result , { NEWLINE } ] , [ eff_result , [ NEWLINE ] ] ,
[ "->" , type , { NEWLINE } ] , [ "->" , type , [ NEWLINE ] ] ,
[ block ] ; [ block ] ;
(* block . interface capability *) (* block . std/*.cool capability *)
struct_decl = [ "copyable" ] , "struct" , ident , [ gen_params ] , struct_decl = [ "copyable" ] , "struct" , ident , [ gen_params ] ,
"{" , { field } , "}" ; "{" , brace_list<field> , "}" ;
field = ident , ":" , type , { NEWLINE } , field = ident , ":" , type ;
[ "," , { NEWLINE } ] ;
enum_decl = "enum" , ident , [ gen_params ] , "{" , { variant } , "}" ; enum_decl = "enum" , ident , [ gen_params ] ,
variant = ident , [ "(" , type_list , ")" ] , { NEWLINE } , "{" , brace_list<variant> , "}" ;
[ "," , { NEWLINE } ] ; variant = ident , [ "(" , type_list , ")" ] ;
capability_decl = "capability" , ident , "{" , { NEWLINE } , { cap_method } , "}" ; capability_decl = "capability" , ident , "{" , [ NEWLINE ] , { cap_method } , "}" ;
cap_method = "fn" , ident , "(" , [ params ] , ")" , { NEWLINE } , cap_method = "fn" , ident , [ gen_params ] , "(" , [ cap_params ] , ")" ,
[ eff_result , { NEWLINE } ] , [ "->" , type ] , [ NEWLINE ] ,
{ NEWLINE } ; [ eff_result , [ NEWLINE ] ] ,
[ "->" , type ] ,
[ NEWLINE ] ;
(* capability .
*
* `self`
* . .
*
* fn .
* .
*
* "own" *)
cap_params = "own" , cap_own_rest
| plain_param , list_rest<param> ;
cap_own_rest = "self" , list_rest<param>
| plain_param , list_rest<param> ;
const_decl = "const" , ident , ":" , type , "=" , expr ; const_decl = "const" , ident , ":" , type , "=" , expr ;
gen_params = "[" , gen_param , { "," , gen_param } , [ "," ] , "]" ; gen_params = "[" , list<gen_param> , "]" ;
gen_param = ident , [ ":" , "effects" ] ; gen_param = ident , [ ":" , "effects" ] ;
(* ident = , ": effects" = effect *) params = list<param> ;
param = [ "own" ] , plain_param ;
params = param , { "," , param } , [ "," ] ; plain_param = [ "mut" ] , ident , ":" , type ;
param = [ "own" ] , [ "mut" ] , ident , ":" , type ;
(* = use(). own .
* : (own, mut) , (affine) type *)
(* ------------------------------------------------------------------ *) (* ------------------------------------------------------------------ *)
(* effect *) (* effect *)
(* ------------------------------------------------------------------ *) (* ------------------------------------------------------------------ *)
(* effect. *) (* *)
eff_result = "effects" , eff_union ; eff_result = "effects" , eff_atom , { "|" , eff_atom } ;
eff_union = eff_atom , { "|" , eff_atom } ;
(* effect. . (* . .
* " " " " *) * " " " " *)
eff_param = "effects" , eff_atom ; eff_param = "effects" , eff_atom ;
eff_atom = ident | eff_set ; eff_atom = ident | eff_set ;
eff_set = "{" , { NEWLINE } , eff_set = "{" , brace_list<eff_name> , "}" ;
[ eff_name , { { NEWLINE } , "," , { NEWLINE } , eff_name } ,
{ NEWLINE } , [ "," , { NEWLINE } ] ] , "}" ;
eff_name = ident , "." , ident ; eff_name = ident , "." , ident ;
(* . capability identity *) (* . capability identity *)
@@ -121,31 +185,41 @@ eff_name = ident , "." , ident ;
type = fn_type | named_type ; type = fn_type | named_type ;
fn_type = [ "affine" ] , "fn" , "(" , [ type_list ] , ")" , (* own . "
* " , *)
fn_type = [ "affine" ] , "fn" , "(" , [ list<fn_param_ty> ] , ")" ,
[ eff_param ] , [ "->" , type ] ; [ eff_param ] , [ "->" , type ] ;
fn_param_ty = [ "own" ] , type ;
named_type = ident , [ type_args ] ; (* : Shapes.Shape.
type_args = "[" , targ , { "," , targ } , [ "," ] , "]" ; * *)
named_type = ident , [ "." , ident ] , [ type_args ] ;
type_args = "[" , list<targ> , "]" ;
targ = type | eff_set ; targ = type | eff_set ;
(* effect. (* effect.
* *) * *)
type_list = type , { "," , type } , [ "," ] ; type_list = list<type> ;
(* ------------------------------------------------------------------ *) (* ------------------------------------------------------------------ *)
(* *) (* *)
(* ------------------------------------------------------------------ *) (* ------------------------------------------------------------------ *)
block = "{" , { NEWLINE } , block = "{" , [ NEWLINE ] , [ stmt , stmt_rest ] , "}" ;
[ stmt , { stmt_sep , stmt } , [ stmt_sep ] ] , "}" ; stmt_rest = [ NEWLINE , [ stmt , stmt_rest ] ] ;
stmt = let_stmt | return_stmt | assign_stmt | expr ; (* NEWLINE .
stmt_sep = NEWLINE , { NEWLINE } ; * , "-" "("
(* "}" . * .
* "}" .
* fn(s) { String.concat(prefix, s) } *) * fn(s) { String.concat(prefix, s) } *)
stmt = let_stmt | return_stmt | expr_stmt ;
let_stmt = "let" , [ "mut" ] , pattern , [ ":" , type ] , "=" , expr ; let_stmt = "let" , [ "mut" ] , pattern , [ ":" , type ] , "=" , expr ;
return_stmt = "return" , [ expr ] ; return_stmt = "return" , [ expr ] ;
assign_stmt = place , "=" , expr ; (* NEWLINE "}" *)
place = ident , { "." , ident } ;
expr_stmt = expr , [ "=" , expr ] ;
(* ( )
* . ident *)
(* = stmt expr , Unit. (* = stmt expr , Unit.
* return , return formatter *) * return , return formatter *)
@@ -154,62 +228,110 @@ place = ident , { "." , ident } ;
(* *) (* *)
(* ------------------------------------------------------------------ *) (* ------------------------------------------------------------------ *)
expr = or_expr ; (* s struct .
or_expr = and_expr , { "||" , and_expr } ; * expr = ( )
and_expr = cmp_expr , { "&&" , cmp_expr } ; * expr_ns = (if/match )
cmp_expr = add_expr , [ cmp_op , add_expr ] ; * if/match "{" struct
cmp_op = "==" | "!=" | "<" | "<=" | ">" | ">=" ; * , struct .
add_expr = mul_expr , { ( "+" | "-" ) , mul_expr } ; * , , ,
mul_expr = unary , { ( "*" | "/" | "%" ) , unary } ; * . primary<no>
unary = [ "!" | "-" ] , postfix ; * expr(= yes).
* scope ( ) *)
expr = or_expr<yes> ;
expr_ns = or_expr<no> ;
postfix = primary , { call_sfx | field_sfx | inst_sfx | "?" } ; or_expr<s> = and_expr<s> , { "||" , and_expr<s> } ;
and_expr<s> = cmp_expr<s> , { "&&" , cmp_expr<s> } ;
cmp_expr<s> = add_expr<s> , [ cmp_op , add_expr<s> ] ;
cmp_op = "==" | "!=" | "<" | "<=" | ">" | ">=" ;
add_expr<s> = mul_expr<s> , { ( "+" | "-" ) , mul_expr<s> } ;
mul_expr<s> = unary<s> , { ( "*" | "/" | "%" ) , unary<s> } ;
unary<s> = [ "!" | "-" ] , postfix<s> ;
postfix<s> = primary<s> , { call_sfx | field_sfx | inst_sfx | "?" } ;
call_sfx = "(" , [ args ] , ")" ; call_sfx = "(" , [ args ] , ")" ;
field_sfx = "." , ident ; field_sfx = "." , ident ;
inst_sfx = type_args ; inst_sfx = type_args ;
(* "[" = , "[" = . *) (* "[" = , "[" = . *)
args = expr , { "," , expr } , [ "," ] ; args = list<expr> ;
primary = literal primary<s> = literal
| ident
| list_lit | list_lit
| struct_lit
| closure | closure
| if_expr | if_expr
| match_expr | match_expr
| scope_expr | scope_expr
| "(" , expr , ")" ; | "(" , expr , ")"
| crash_expr
| name_or_struct<s> ;
list_lit = "[" , [ expr , { "," , expr } , [ "," ] ] , "]" ; (* crash . Never,
* Never match .
*
* effect . {Crash}
* effect . effect
* .
*
* recover . ,
* 1 .
* ( ).
*
* . crash
* .
*
* : prelude import *)
crash_expr = "crash" , "(" , expr , ")" ;
(* ident struct . "{"
* *)
(* struct . Alias.Name { ... }
* postfix "{" ,
* struct .
* "." *)
(* Alias.Name { ... } struct .
* .
*
* struct postfix : "{"
* , "{" .
* struct (<yes>) .
* <no> if x.y { } "{" *)
name_or_struct<yes> = ident , ( "." , ident , [ struct_body ]
| [ struct_body ] ) ;
name_or_struct<no> = ident ;
struct_body = "{" , brace_list<field_init> , "}" ;
field_init = ident , ":" , expr ;
list_lit = "[" , [ args ] , "]" ;
(* : let xs: List[Int] = [] *) (* : let xs: List[Int] = [] *)
struct_lit = ident , "{" , { field_init } , "}" ;
field_init = ident , ":" , expr , { NEWLINE } , [ "," , { NEWLINE } ] ;
closure = "fn" , "(" , [ cl_params ] , ")" , closure = "fn" , "(" , [ cl_params ] , ")" ,
[ eff_param ] , [ "->" , type ] , block ; [ eff_param ] , [ "->" , type ] , block ;
cl_params = cl_param , { "," , cl_param } , [ "," ] ; cl_params = list<cl_param> ;
cl_param = ident , [ ":" , type ] ; (* .
* move
* , .
* ( ) *)
cl_param = [ "own" ] , ident , [ ":" , type ] ;
(* . . (* . .
* error *) * error *)
if_expr = "if" , expr_ns , block , [ "else" , ( block | if_expr ) ] ; if_expr = "if" , expr_ns , block , [ "else" , ( block | if_expr ) ] ;
match_expr = "match" , expr_ns , "{" , { arm } , "}" ; match_expr = "match" , expr_ns , "{" , brace_list<arm> , "}" ;
arm = pattern , "=>" , ( expr | block ) , { NEWLINE } , arm = pattern , "=>" , ( expr | block ) ;
[ "," , { NEWLINE } ] ;
scope_expr = "scope" , ident , "=" , ident , block ; scope_expr = "scope" , ident , "=" , ident , block ;
(* scope = { ... } (* scope = { ... }
* . " " * . " "
* ambient authority *) * ambient authority *)
(* expr_ns = struct_lit expr.
* if/match/scope "{" struct
* , struct *)
(* ------------------------------------------------------------------ *) (* ------------------------------------------------------------------ *)
(* *) (* *)
(* ------------------------------------------------------------------ *) (* ------------------------------------------------------------------ *)
pattern = "_" | literal | ctor_pattern | ident ; pattern = "_" | literal | name_pattern ;
ctor_pattern = ident , "(" , pattern , { "," , pattern } , [ "," ] , ")" ;
(* . :
* (Shapes.Dot)
*
* , *)
name_pattern = ident , [ "." , ident ] , [ "(" , [ list<pattern> ] , ")" ] ;
(* . (exhaustiveness ) *) (* . (exhaustiveness ) *)
+191 -2
View File
@@ -28,6 +28,16 @@ AI의 코드 생성 속도 >> 신뢰 확보 속도.
→ "Effects: +payment.refund" 수준으로 리뷰 압축 → "Effects: +payment.refund" 수준으로 리뷰 압축
5. 한 개념 = 한 방식: syntax variant 최소화, 공식 formatter 하나, 5. 한 개념 = 한 방식: syntax variant 최소화, 공식 formatter 하나,
공식 toolchain 하나. 단순성의 기준은 "작성자"가 아니라 "리뷰어와 검증기" 공식 toolchain 하나. 단순성의 기준은 "작성자"가 아니라 "리뷰어와 검증기"
6. 이름은 관례가 아니라 뜻에서 고른다 — 낯섦은 한 번 치르고 끝나지만
부정확함은 읽는 사람마다 매번 치른다
→ 판정: 그 단어로 그것이 무엇인지 평범한 문장을 써 보라. 단어가 문장을
도우면 맞는 이름, 문장과 싸우면 틀린 이름이다
"크래시는 복구하는 것이 아니라 조사하는 것이다" — 돕는다
"패닉은 복구할 수 없다" — 다른 언어에서는 할 수 있어 싸운다
→ 널리 쓰인다는 것은 옳다는 증거가 아니다. 운영체제의 "fault"가 그렇다 —
잘못이 아닌 것을 잘못이라 부르고, 그 오류까지 함께 전파된다
※ 관례가 정확하면 관례를 따른다. 관례라서가 아니라 정확해서다
※ 이름에만 적용된다. 동작을 관례와 다르게 만들 이유는 아니다
■ 파생 결정 ■ 파생 결정
@@ -47,8 +57,14 @@ Alias/Move 모델 (철학 1,3에서 파생 — 언어 전체의 토대):
"소비 여부를 타입에 드러내라"는 압력이라 리뷰 가능성에는 순기능이고, "소비 여부를 타입에 드러내라"는 압력이라 리뷰 가능성에는 순기능이고,
실제로 고통스러우면 v1에서 Option::take류 표준 관용구로 흡수한다 실제로 고통스러우면 v1에서 Option::take류 표준 관용구로 흡수한다
- v0의 자원·capability는 affine이다: 중복 사용은 compile error, - v0의 자원·capability는 affine이다: 중복 사용은 compile error,
미해제는 검사하지 않는다 (오용 금지, 누수 허용). 미해제는 타입 층에서 검사하지 않는다 (오용 금지, 누수 허용).
linear 검사와 해제 보장은 v1 과제 linear 검사와 해제 보장은 v1 과제.
※ 다만 lint가 조기 탈출(?, return) 자리에서 새는 것을 말한다. 막지 않고
말한다 — 언어가 강제할지는 with가 결정할 문제이고 그 결정에는 "실제로
얼마나 새는가"라는 측정이 필요하다. 저장소 전체에서 네 곳, 오탐 0건
- v0에 부분 move는 없다. affine 값이 합성 구조를 드나드는 모든 자리가 Move
자리다: struct 필드, 열거형 생성자, 열거형 분해, 리스트·struct 리터럴,
함수 인자, 반환, 클로저 capture. 앞의 셋이 뚫려 있었고 개밥 먹기가 찾았다
- v0에 first-class reference는 없다. mutable 데이터는 소유 변수를 통해서만 변경 - v0에 first-class reference는 없다. mutable 데이터는 소유 변수를 통해서만 변경
- 클로저는 mut 바인딩을 capture할 수 없다. 참조가 없으므로 별칭을 만들 수도, - 클로저는 mut 바인딩을 capture할 수 없다. 참조가 없으므로 별칭을 만들 수도,
조용히 복사할 수도 없기 때문이다. spawn 클로저 제한은 이 일반 규칙의 특수 사례다 조용히 복사할 수도 없기 때문이다. spawn 클로저 제한은 이 일반 규칙의 특수 사례다
@@ -338,6 +354,111 @@ L2 빠른 테스트 / L3 fuzzing / L4 formal proof → 요청 시, 분리 실행
invalidation 범위를 넓히는가? / unrelated code 의미를 바꾸는가? / invalidation 범위를 넓히는가? / unrelated code 의미를 바꾸는가? /
기존 개념의 중복 표현인가? → 강한 이유 없으면 거절 기존 개념의 중복 표현인가? → 강한 이유 없으면 거절
■ 실패의 세 층 — 실패 / 결함 / 감독
서로 다른 세 질문에 답한다. 아래 층은 위 층 없이도 성립한다.
실패 (Result) 호출자가 대처할 수 있는가? 타입이 강제한다
결함 (crash) 이 태스크가 계속할 수 있는가? 컴파일러는 추적하지 않는다
감독 죽은 태스크를 어떻게 다루는가? capability가 강제한다
세 층이 세 가지 다른 기제로 규율된다는 것이 제대로 나뉘었다는 증거다.
"상황이 나쁨"은 결함이 아니라 실패다 — 디스크가 가득 찬 것은 Result다.
이 선을 긋지 않으면 crash가 게으름의 배출구가 된다.
※ 취소는 이 셋 중 어디에도 없다. 취소는 실패가 아니라 "더 이상 필요 없음"
이며 별도의 결정으로 남는다.
"오류"는 컴파일러가 내는 진단에만 쓴다.
■ 결함 — crash
crash(message) -> Never. 키워드다 (prelude가 없어 함수로 두면 매번 import).
- effect가 아니다. 경계 검사 하나에 {Crash}가 호출자 전부로 전염되면
effect 절은 신호가 아니라 잡음이 된다. 발산이 effect가 아닌 것과 같은
이유다 — 무한 루프도 추적하지 않는다.
- Never는 어떤 타입 자리에도 놓인다. 그래야 match 팔에서 쓸 수 있고,
그게 없으면 crash는 식 자리에서 못 쓴다.
- 언어 수준 recover가 없다. 붙잡는 것이 있으면 그것은 예외이고, 예외는
시그니처에 안 적히므로 철학 1과 충돌한다.
- 되감기를 하지 않는다. ※ crash 시 자원 해제 여부는 자원 모델과 함께
결정한다 — 지금은 열어둔다.
- 0으로 나누기, assert 실패, 미래의 범위·오버플로가 전부 이 하나로 모인다.
※ crash는 프로그램의 결함을 말한다. 예상되는 실패는 Result다. 호출자가
대처할 수 있는 것을 crash로 처리하면 오용이다.
■ crash는 어디까지 올라가는가 (실패의 단위)
콜 스택의 root가 아니라 scope 트리의 root다. 즉 실패의 단위는 프로세스가
아니라 태스크다. scope가 이미 그 모양이기 때문이다 — 렉시컬이고, 블록을
나가는 것이 join이며, 자식이 죽었다는 사실이 부모에게 도달하는 지점이
문법에 이미 있다(닫는 중괄호).
crash는 scope 트리를 타고 올라간다. 자식이 crash하면 그 scope는 join
지점에서 crash한다. root까지 도달하면 실행이 끝난다.
v0는 태스크가 하나뿐이라 이 규칙이 관측되지 않는다 — "태스크가 죽는다"와
"프로그램이 죽는다"가 같은 사건이다. 그래도 지금 적는 이유는, 동시성이
진짜가 될 때 이 결정을 새로 하면 이미 쓰인 코드의 의미가 바뀌기 때문이다.
전파는 취소를 정하지 않고도 정의된다. 취소가 생기면 형제들이 언제 멈추는지가
바뀔 뿐, scope가 실패한다는 사실은 바뀌지 않는다.
※ 형제 취소는 여전히 미정이다. 대가를 알고 미룬다 — 취소가 없으면 형제
하나가 끝나지 않을 때 죽은 자식의 crash가 join에 도달하지 못한다.
실패가 hang에 가려진다. 이것이 취소가 필요한 이유이기도 하다.
■ 얼랭 방식은 되는가 — 감독은 되고 비구조적 spawn은 안 된다
공유 가변 상태 없음, let it crash, 죽은 쪽의 상태를 물려받지 않음 —
얼랭과 이미 같다. 다른 경로로 같은 결론에 도착했다.
가져오지 않는 것: 얼랭의 비구조적 spawn(Pid가 부모보다 오래 산다).
부모 없는 태스크는 ambient authority와 같은 문제이고, 구조적 동시성을 고른
이유가 그것이다.
가져오는 것: 감독. scope 트리가 이미 supervision 트리 모양이다.
sc.spawn(f) 실패가 부모로 전파된다
sup.spawn(sc, f) 실패가 여기서 멈춘다
spawn은 하나의 개념이고 실패가 어디까지 가느냐가 권한으로 갈린다 — 실패를
삼키려면 Supervisor를 받았어야 하고 그것이 시그니처에 보인다. 문법 변경 없음.
재시작 가능한 자식은 copyable 클로저여야 한다(fn vs affine fn). 얼랭이 관례로
지키는 것을 affinity가 이미 검사한다. 재시작 전략은 라이브러리다(얼랭도 OTP).
※ 전제조건은 자원 모델이다. 죽은 태스크가 잡았던 것이 정리되지 않으면
재시작이 곧 누수다.
※ 얼랭의 보장은 BEAM의 선점 스케줄링과 프로세스별 힙 위에 선다. 우리에겐
아직 둘 다 없다 — 이것은 설계 층의 답이지 v0가 보여줄 수 있는 것이 아니다.
■ 격리 경계는 recover가 아니다
런타임은 격리 경계를 가질 수 있다. 테스트 러너가 첫 사례이고, 서버의
요청 경계가 두 번째가 될 것이다. 구분선은 이것이다:
실패한 계산이 만든 값은 경계를 넘지 못한다.
경계가 얻는 것은 "죽었다"는 사실과 메시지뿐이다.
recover는 같은 스택에서 재개하고 지역 변수에 접근한다 — 금지다.
격리 경계는 죽었음을 관찰할 뿐이다 — 허용이다. Erlang의 supervisor가 죽은
프로세스의 상태를 물려받지 않는 것과 같다.
테스트 러너가 이 원칙대로다: 실패한 테스트에서 아무 값도 가져오지 않고
이름과 메시지만 얻는다. 서버 경계도 언어 기능이 아니라 scope 위에 얹는다.
■ 내장 테스트
test "이름" { ... }. 파라미터가 없으므로 capability를 받을 수 없고,
capability를 만드는 문법도 없다. 따라서 effect-free임이 증명된다 —
관례가 아니라 검사다. 그 결과:
- 파일도 시계도 못 건드린다. 같은 입력이면 같은 결과다
- 순서에 의존하지 않고 병렬로 돌려도 같다 → 결과를 캐시할 수 있다
(인터페이스 해시가 안 변하면 재검사하지 않는 것과 같은 논리)
- 자원을 가질 수 없다 (획득에 effect가 필요하므로). 그래서 "테스트가
죽으면 자원은?"이라는 질문이 애초에 생기지 않는다
interface hash에 들어가지 않는다 — 테스트를 고쳤다고 downstream이
재검사되면 안 된다.
컴파일 타임 메타프로그래밍 없음. assert는 std/test.cool에 coollang으로
쓰인다 (crash 위의 설탕) — std에서 본문이 있는 첫 함수다. 실패 메시지에
값이 안 나오는 것은 의도다. 표현식 텍스트를 잡으려면 매크로가 필요하다.
※ effect 있는 코드는 테스트할 수 없다. 가짜 capability를 만드는 수단이
없기 때문이다. 실제로 불편해진 뒤에 판단한다.
■ 버려지는 값
꼬리가 아닌 자리의 식이 값을 남기면 오류다. 남긴 값은 버려지는데, 그 값이
Result면 실패가 조용히 사라진다 — 철학 1과 정면으로 부딪힌다.
일부러 버리려면 let _ = 로 적는다. 버린다는 사실이 코드에 보여야 한다.
※ 개밥 먹기 2에서 발견했다. 그전까지 "이 언어에는 실패를 버릴 방법이 없다"고
문서와 투어에 적혀 있었는데 틀린 말이었다 — List.each에 Result 반환 클로저를
못 넘긴다는 좁은 사실을 언어 전체의 성질로 일반화한 것이었다.
부산물로 정리 경로의 관용구가 생겼다: let _ = fs.close(f).
■ lint (오류다, 경고가 아니다) ■ lint (오류다, 경고가 아니다)
- 미사용 import: 쓰지 않는 모듈의 시그니처가 바뀌면 이 모듈이 재검사된다. - 미사용 import: 쓰지 않는 모듈의 시그니처가 바뀌면 이 모듈이 재검사된다.
증분 루프의 비용을 이유 없이 넓히는 선언은 남겨둘 수 없다. 증분 루프의 비용을 이유 없이 넓히는 선언은 남겨둘 수 없다.
@@ -366,6 +487,74 @@ prelude는 없다. std도 명시적으로 가져온다 — 암묵적으로 끌
것이고, 이것이 std를 "부채 상환"이 아니라 "검증"으로 본 이유다. 것이고, 이것이 std를 "부채 상환"이 아니라 "검증"으로 본 이유다.
결과를 버릴 방법이 언어에 없다는 성질도 여기서 처음 확인됐다. 결과를 버릴 방법이 언어에 없다는 성질도 여기서 처음 확인됐다.
■ 개밥 먹기 2 — dogfoods/ (2026-08)
프로그램을 쓰되 돌리지 않는다. std를 선언만 두고 그것에 대고 실제 유스케이스를
쓴다. std가 원래 선언-전용이므로 실행만 빼고 전부 진짜로 검사된다 — 종이
스케치가 아니라 컴파일러가 검증한 설계다.
합격 기준이 둘이다: check exit 0 + 모듈이 실제로 해소될 것. 후자가 없으면
전자가 공허하다 — import가 조용히 해소되지 않으면 이름이 전부 불투명해져
검사기가 무엇이든 통과시킨다. 첫 시도에서 실제로 그 일이 일어났다.
2번(Atomic File Updater) 결과: ?를 자원과 함께 쓸 수 없다. 조기 반환이 정리를
건너뛰는데 검사기는 통과시킨다(v0 정책: 오용 금지, 누수 허용). 제대로 정리하면
18줄이 58줄이 되고 5단 중첩 match가 된다. resource/with가 필요한 이유가 여기
숫자로 있다.
6번(DB Pool)까지 두 점을 모아 resource/with를 넣기로 결정했다. 근거:
자원을 들면 ?를 한 번도 못 쓴다(2/2), 같은 정리 패턴을 각자 발명했다(2/2),
누수가 조용히 통과한다(2/2), 2.3~3.2배.
고통은 자원 개수가 아니라 정리의 균일성에 비례한다 — 파일은 단계마다 정리가
달라 4단 중첩이 되고, 풀은 언제나 반납이라 1단으로 접힌다.
순서가 정해졌다: 먼저 수신자를 소비하는 메서드(자원이 자기 정리를 스스로
선언할 수 있어야 한다), 그다음 resource/with. release(pool, own lease)처럼
정리에 다른 값이 필요하면 with가 자동으로 부를 수 없기 때문이다.
앞의 것을 넣었다 — own self. capability 메서드의 첫 자리에만 오고, 빌리는
것이 기본이므로 빌림에는 적을 말이 없다. 정리에 자원 자신만 있으면 되게
됐고, 부수로 권한 약화가 공짜로 생겼다: 핸들만 넘기면 그 파일에 쓸 권한만
넘어가고 파일 시스템 전체 권한은 안 넘어간다.
고침: 함수 타입과 클로저 파라미터의 own. 그 구멍이 std의 fold가 틀린 것을
덮고 있었다 — 누적자는 매 단계 소비되므로 own이다.
LRU 캐시가 제약 하나를 더했다: 축출은 렉시컬이 아니므로 with가 못 닿는다.
따라서 자원은 렉시컬 전용으로 간다 — 자원을 담는 컨테이너(풀, 캐시,
레지스트리)는 런타임만 만들 수 있고, 사용자는 언어로 못 만든다. 그 대가를
알고 택한다. 이미 그렇게 되어 있었다 — std-draft의 Pool이 불투명한 런타임
capability인 이유가 만들 수 없어서였다.
그리고 큰 버그 하나를 잡았다: Option/Result의 패턴이 타입을 통째로 잃고
있었다. 내장 생성자가 typecheck에 등록되지 않아 None이 변수 바인딩이 되고
Ok(v)의 v가 TUnknown이었다. Ok(v) => v 로 Int를 String 자리에 두는 코드가
통과했다. 소진성 검사는 자기 목록에 내장을 갖고 있어 이 사실을 덮고 있었다.
Mini Shell(1번)이 네 번째이자 다른 모양의 점을 줬다. 앞의 셋은 한 자원이
한 곳으로 갔다. 파이프는 끝이 둘이고 각각 다른 자식에게 간다 — 부모에게는
아무것도 안 남고 어느 블록에도 묶이지 않는다. 가장 어려운 자원 코드가
렉시컬하지 않다.
그래서 with를 미룬다. resource라는 새 종류는 넣지 않기로 확정했다 —
capability가 이미 그것이고 own self가 정리를 말하므로, 종류를 더 만들면
95%가 같은 둘이 생긴다. with는 두 갈래(자동 반납 / 소비 강제) 중 하나를
고르는 문제이고, 렉시컬 도구를 먼저 넣으면 그것이 닿지 않는 자리를 나중에
다시 설계해야 한다. 다시 여는 조건: Process Supervisor나 Reverse Proxy에서
렉시컬한 자원 코드가 다수면 고른다.
Mini Shell이 보안 구멍 셋을 더 찾았다. 열거형 생성자가 인자를 소비하지
않았고(Some(f)를 두 번), 소유한 열거형을 두 번 분해할 수 있었고(같은
파이프에서 같은 fd를 두 번), 점 왼쪽을 이름 해소가 검사하지 않아 가져오지도
않은 이름이 조용히 통과했다(Nonsense.whatever). 셋 다 D1·D11과 같은 병이다 —
규칙은 있었는데 그 규칙이 닿아야 할 자리 하나가 비어 있었다.
그리고 개밥이 처음으로 실제로 돌았다. 파싱을 자원에서 떼어 놓았더니
mini-shell/parse.cool의 내장 test 5개가 통과한다.
전문은 dogfoods/FINDINGS.md.
■ 개밥 먹기 1 — samples/app (2026-08)
samples/app — 설정 파서 + 리포트 도구, 2모듈 232줄. 검사기를 시험하려고
쓴 것이 아니라 일을 하려고 쓴 첫 프로그램이다.
결과: 되돌리기 비싼 결정은 하나도 후회되지 않았고(capability 전달, effect
명시, 실패를 버릴 수 없음, 소진적 match), 불편은 전부 되돌리기 싼 것들이었다
— 리스트 n번째 접근 없음, String.concat 2항. "else if가 없다"고 적은
항목 하나는 확인해 보니 관찰자가 틀린 것이었다 — 개밥 먹기의 불편은
언어의 성질일 수도, 쓴 사람의 습관일 수도 있다.
전문과 증거는 docs/friction.md. 이것이 v1 설계의 첫 입력이다.
후속: std 보강(List.first/nth/enumerate, String.join, Option/Result 함수)으로
292줄이 232줄이 됐다. 문법은 하나도 건드리지 않았다 — 걸린 것이 전부 std의
빈 곳이었지 문법의 문제가 아니었다는 뜻이고, 그것 자체가 결과다.
마찰 8건 중 2건은 "언어가 못 한다"고 적었다가 확인해 보니 되는 것이었다
(else if, enumerate). 개밥 먹기에는 확인 절차가 함께 있어야 한다.
■ 측정 (2026-08, v0 fast path) ■ 측정 (2026-08, v0 fast path)
100,391줄 / 200 모듈 (사슬 의존). bench/bench.ml로 재현. 100,391줄 / 200 모듈 (사슬 의존). bench/bench.ml로 재현.
cold 전체 검사 245ms cold 전체 검사 245ms
+836
View File
@@ -0,0 +1,836 @@
# 개밥 먹기 발견 기록
`dogfoods/`를 쓰면서 나온 것들. 고친 것과 열어둔 것을 같이 적는다.
---
## 2. Atomic File Updater (2026-08-30)
목적: 동시성을 안 섞고 **자원 모델만** 시험한다. 임시 파일에 쓰고, 디스크에
내리고, 원자적으로 바꿔치기한다. 실패 경로가 넷이고 전부 정리가 필요하다.
산출물: `std-draft/fs.cool` 65줄, `atomic-update/naive.cool` 18줄,
`atomic-update/careful.cool` 58줄.
### D1. 상대 경로 import가 패키지로 오인됐다 — 고침
`import "../std-draft/fs"`가 조용히 해소되지 않았다. `is_package`가 첫
세그먼트에 점이 있는지만 봤는데 `..`이 걸렸다.
**무서운 것은 버그 자체가 아니라 그 결과다.** import가 해소되지 않으면 그
모듈의 이름이 전부 불투명해지고, "모르는 것을 틀렸다고 말하지 않는다"는 원칙에
따라 검사기가 무엇이든 통과시킨다. 첫 `coolc check` 결과가 exit 0이었는데
**아무 뜻도 없었다.** 없는 메서드를 불러도 통과했다.
고친 뒤 같은 코드가 정확히 잡힌다:
```
capability Fs.Fs에 this_does_not_exist 메서드가 없습니다
```
`dogfoods/README.md`의 합격 기준 ②가 여기서 나왔다.
### D2. 값 있는 식을 문으로 버릴 수 있었다 — 고침
```cool
fs.remove(path) // Result가 조용히 사라진다
1
```
이것이 통과했다. 즉 **`Result`를 버리는 방법이 있었다.** 개밥 먹기 1차
보고서와 언어 투어에서 내가 "이 언어에는 실패를 버릴 방법이 없다"고 적었는데
틀렸다 — `List.each`에 Result 반환 클로저를 못 넘긴다는 좁은 사실을 언어 전체의
성질로 일반화했다.
이제 꼬리가 아닌 자리의 식이 값을 남기면 오류다:
```
이 식이 남기는 Result[Unit, IoError]이(가) 버려집니다
(일부러 버리려면 let _ = 로 적으십시오)
```
부산물이 좋다. 정리 경로에서 오류를 **일부러** 무시하는 관용구가 생겼고,
버린다는 사실이 코드에 보인다:
```cool
let _ = fs.close(f) // 정리 중의 실패는 삼킨다 — 원래 오류가 더 중요하다
discard(fs, tmp, e)
```
### D3. `?`를 자원과 함께 쓸 수 없다 — 이번 개밥 먹기의 본체
`naive.cool` 18줄은 이렇게 생겼고 **통과한다**:
```cool
let f = fs.create(tmp)?
fs.write(f, contents)? // 실패하면 f가 안 닫히고 tmp가 남는다
fs.sync(f)?
fs.close(f)?
fs.rename(tmp, path)?
```
`?`가 조기 반환하므로 정리를 건너뛴다. v0가 "오용 금지, 누수 허용"이라
검사기는 아무 말도 하지 않는다.
제대로 정리하면 `careful.cool` 58줄이 되고 **5단 중첩 match**가 된다.
`?`를 한 번도 못 쓴다. 같은 일에 **3.2배**다.
이것이 `resource` / `with` 제안이 필요한 이유의 전부다. 그리고 그 제안이
암묵적 drop보다 나은 이유도 여기서 보인다 — 정리 지점이 닫는 중괄호로 눈에
보이고, `Fs.close`가 effects 절에 나타나 검사된다.
### D4. capability 메서드가 수신자를 소비할 수 없다 — 고침
`close`가 핸들을 소비해야 하는데 `own`은 파라미터에만 붙었다. capability
메서드에는 수신자를 적는 자리가 없었다.
처음에는 우회했다 — 핸들의 메서드가 아니라 `Fs`의 메서드로 두고 핸들을
인자로 받는 형태다. "읽기에 나쁘지 않고 오히려 권한이 필요하다는 게
보인다"고 적었는데, **D9이 그 판단을 뒤집었다.** 정리에 바깥 권한이
따라붙으면 `with`가 자원만 보고 무엇을 부를지 알 수 없다.
**넣은 것**: `own self`를 capability 메서드의 첫 파라미터로.
```cool
pub capability WriteFile {
fn write(s: String) effects {WriteFile.write} -> Result[Unit, IoError]
fn close(own self) effects {WriteFile.close} -> Result[Unit, IoError]
}
```
빌리는 것이 기본이므로 **빌림에는 적을 말이 없다.** `self`만 적는 문장은
존재하지 않고, 그것을 쓰면 문법이 아니라 뜻으로 거절한다:
```
self는 own self로만 적습니다 (수신자를 빌리는 것이 기본이라 적을 말이 없습니다)
```
일반 `fn`에는 이 자리가 문법에 없다. 좌인수분해로 `"own"`을 먼저 먹고
그다음을 보므로 LL(1)이 유지된다 (진짜 충돌 0건).
**부수 발견 — 권한이 좁아진 채로 전달된다.** 1판에서는 파일에 쓰게 하려면
`Fs`를 통째로 줘야 했다. 이제 핸들만 넘기면 그 파일에 쓸 권한만 넘어간다.
정리를 자원 쪽으로 옮긴 것이 권한 약화(attenuation)를 공짜로 만들었다 —
설계할 때 노린 것이 아니라 나중에 보인 것이다.
**부수 발견 — 중첩 수신자에 구멍이 있었다.** move 검사의 `callee_info`
수신자가 이름일 때만 시그니처를 찾았다. `h.lease.release()`는 못 찾아
조용히 빌림이 됐고, `own self`가 생기는 순간 그것은 자원을 두 번 소비할 수
있다는 뜻이 된다. 같은 커밋에서 막았다 (D11의 부분 move 검사로 떨어진다).
**부수 발견 — 진단이 부정확했다.** 메서드 이름만으로 아무 capability에서나
시그니처를 찾았기 때문에, 이름이 겹치면 엉뚱한 시그니처를 읽고 "빌린 값이라
소비할 수 없습니다"를 잘못 찍었다. 수신자의 선언된 타입을 알면 그 capability
안에서만 찾도록 고쳤다.
**2판으로 다시 쓴 결과** (같은 일을 하는 코드):
- `dogfoods/std-draft/fs.cool``WriteFile`이 메서드를 갖는다. 1판에서는 하나도 없었다
- `atomic-update/careful.cool``abandon``fs`를 닫는 데 쓰지 않는다
- `db-pool/careful.cool``release_then(db, pool, l, r)``release_then(l, r)`
- `lru-cache/resources.cool``drain(db, pool, p)``drain(p)`
**도우미의 시그니처가 정리의 진짜 의존을 말하게 됐다.** 그것이 D9이 요구한
것이고, `with`의 전제 조건이다.
### D5. 함수 타입에 `own`이 없다 — 열림, 그리고 이건 구멍이다
```cool
pub fn apply(f: fn(own Handle) -> Handle, own h: Handle) -> Handle
^^^ 타입이(가) 필요합니다 — own 발견
```
문법이 `fn_type = "fn" "(" type_list ")"``own`이 못 들어간다. 그래서
**소유권을 가져가는 클로저를 타입으로 표현할 수 없다.**
결과로 이것이 통과한다:
```cool
List.fold(xs, h, fn(acc, n) { step(acc, n) }) // step은 own을 받는다
```
`fold`의 시그니처는 `fn(acc, a) -> acc`이고 무표기는 빌림인데, 클로저
리터럴의 파라미터에는 소유권 표시가 없어 move 검사기가 소유한 값처럼
취급한다. **타입은 "빌린다"고 말하는데 리터럴은 "가져간다"처럼 행동한다.**
기본 검사는 멀쩡하다 — 빌린 값 옮기기도 이중 소비도 정확히 잡는다.
**고차 경계에서만 뚫린다.** Atomic Updater는 선형 코드라 안 걸렸지만,
Mini Shell(파이프라인을 따라 FD를 나름)과 DB Pool(lease를 fold로 다룸)은
정면으로 걸린다.
→ 그 둘을 쓰기 전에 결정해야 한다.
### D5 후속 — 고침, 그리고 std의 실수 하나가 딸려 나왔다
`own`을 함수 타입과 클로저 파라미터에 넣었다:
```cool
fn(own Handle) -> Handle // 타입에 적을 수 있다
List.fold(xs, h, fn(own acc, n) {...}) // 리터럴에도 적는다
```
클로저 파라미터의 소유권은 **리터럴이 스스로 적는다.** 타입은 기대 타입에서
읽어오지만 소유권은 읽어오지 않는다 — move 검사는 타입 검사와 별도 순회라
타입을 모르고, 소유권은 타입보다 결과가 크기 때문이다.
`unify`가 정확히 일치를 요구한다. 빌리는 클로저를 소유 자리에 넘기는 것은
안전하지만 그 반대는 아니고, 방향을 다루려면 부분 타입이 필요한데 없다.
**딸려 나온 것**: `std/list.cool``fold`가 틀려 있었다.
```cool
f: fn(acc, a) -> acc // 전 — 빌림
f: fn(own acc, a) -> acc // 후 — 누적자는 매 단계 소비되고 새것으로 바뀐다
```
빌림으로 적혀 있어서 **affine 값을 fold로 실어나를 수 없었다.** 그런데 그
사실이 드러나지 않았던 이유가 바로 이 구멍이었다 — 클로저 파라미터를 무조건
소유로 봤으니 아무 오류도 안 났다. **구멍이 자기가 숨긴 버그를 덮고 있었다.**
#### 남은 한계 — 제네릭을 통과해 보지 못한다
move 검사는 타입이 없어 `fold``acc`가 호출 지점에서 무엇으로 묶이는지
모른다. 그래서 클로저 파라미터에 표기가 없고 기대 타입이 제네릭 변수면
affinity를 판정하지 못한다. 지금은 소유권 표기 불일치로 잡히지만, 표기가
양쪽 다 없으면 통과한다.
근본 해법은 move 검사가 타입을 보는 것이고, 그건 두 순회를 합치는 일이다.
v0에서는 하지 않는다.
#### 대가 하나 — own이 흔해진다
`own`은 흔하지 않은 쪽에 붙는 표기인데, `fold`가 항상 요구하면 흔해진다.
`Config` 같은 copyable 누적자에도 `own`을 적게 된다. 정확히 일치를 요구한
결과이고, 부분 타입을 넣으면 사라진다. **표기의 신호가 약해지는지 지켜본다.**
### D6. 문자에 접근할 방법이 없다 — 열림
`String``split`, `trim`, `starts_with`, `contains`뿐이다. 인덱싱도
`chars`도 없어 어휘 분석을 쓸 수 없다. Atomic Updater에는 파싱이 없어 안
걸렸지만 Mini Shell은 여기서 막힌다.
되돌리기 싼 std 문제다.
### D7. effect-free 테스트로는 이 프로그램을 하나도 테스트할 수 없다 — 예상됨
`update`는 전부 effect다. 테스트는 capability를 받지 않으므로 부를 수 없다.
설계대로이고, 대가가 이제 실물로 보인다.
가짜 capability를 만들 수단이 없는 한 이 층은 테스트 밖에 있다.
※ 가짜 capability를 허용해도 보안 성질은 안 깨질 것으로 보인다 — 클로저로
만든 가짜 `Fs`는 진짜 권한을 갖지 않는다. 실제로 불편해진 뒤에 판단한다.
### 소유권 검사가 실제로 잡는 것 (확인)
| | |
|---|---|
| 두 번 닫기 | **잡힘**`f은(는) 이미 move되었습니다 (4:22에서 소비)` |
| 닫은 뒤 쓰기 | **잡힘** — 같은 진단 |
| 빌린 핸들을 반환 | **잡힘**`빌린 값이라 반환할 수 없습니다` |
| 소유한 핸들을 반환 | 통과 — 옳다. `create`가 그렇게 생겼다 |
| 조기 반환으로 누수 | **통과** — v0 정책("오용 금지, 누수 허용") |
마지막 줄이 D3이고, `with`가 닫으려는 자리다.
---
## 6. DB Connection Pool (2026-08-30)
목적: 자원 모델의 **두 번째 데이터 점**. 파일에 없던 셋을 압박한다 —
자원이 두 층(풀/lease), 반납이 파괴가 아님, 획득이 실패할 수 있음.
산출물: `std-draft/db.cool` 50줄, `db-pool/naive.cool` 25줄,
`db-pool/careful.cool` 58줄.
### D8. 인자 안에서 소비와 사용을 섞을 수 없다
```cool
release_then(db, pool, l, db.query(l, sql))
^ ^
소비 사용
```
```
l은(는) 이미 move되었습니다 (13:41에서 소비)
```
move 검사가 인자를 왼쪽부터 걷기 때문이다. **실제 평가 순서와 무관하다**
사람은 "질의가 먼저 돌고 그다음 반납"이라고 읽지만 검사기는 그렇게 안 본다.
우회는 쉽다. 먼저 `let`으로 묶고 넘긴다. 다만 **왜 안 되는지가 코드에서
안 보이므로** 진단 메시지가 위치만 말하고 이유를 말하지 않는다.
### 두 사례 비교 — 이것이 `with` 결정의 근거다
| | naive | careful | 배수 | match 중첩 | 정리 도우미 |
|---|---|---|---|---|---|
| Atomic Updater | 18 | 58 | **3.2** | **4단** | 2개 |
| DB Pool | 25 | 58 | **2.3** | **1단** | 2개 |
**공통 (2/2)**
1. **자원을 들면 `?`를 한 번도 못 쓴다.** 예외 없이 그렇다
2. **"정리하고 결과를 그대로 실어나르는 도우미"를 양쪽이 각자 발명했다**
(`abandon`/`discard`, `release_then`/`close_then`). 같은 패턴이 두 번
독립적으로 나온 것은 그것이 진짜 추상이라는 뜻이다
3. **누수는 조용히 통과한다** — 검사기가 아무 말도 안 한다
**다른 점 — 그리고 이게 중요하다**
파일은 4단 중첩이고 풀은 1단이다. 이유는 **정리가 단계마다 다른가**이다.
- 파일: 닫기 전이면 `close + remove`, 닫은 뒤면 `remove`만 → 단계마다 다름
- 풀: 성공이든 실패든 언제나 `release` → 하나로 접힘
**고통이 자원 개수가 아니라 정리의 균일성에 비례한다.** `with`의 값도
그만큼 달라진다.
### D9. `with`가 성립하려면 D4를 먼저 풀어야 한다
풀 사례가 `with`의 설계 제약 하나를 드러냈다.
```cool
with l = db.acquire(pool)? { ... } // 나갈 때 무엇을 부르나?
```
반납은 `db.release(pool, l)`이다 — **풀과 권한이 필요하다.** 그런데 `with`
자동으로 부르려면 **자원만으로 정리를 표현할 수 있어야 한다.** 즉:
```cool
resource Lease {
release close(own self) effects {Db.release} -> Result[Unit, DbError]
}
```
`own self`가 필요하다. 그런데 **capability 메서드는 수신자를 소비할 수
없다(D4)**. `own`은 파라미터에만 붙는다.
파일 쪽도 같다 — `Fs.close(own f)``Fs` 권한을 요구하므로 자원만으로는
정리가 안 된다.
**따라서 순서가 정해진다: D4 → resource/with.** 자원이 자기 정리를 스스로
선언할 수 있어야 `with`가 성립한다. 런타임이 lease 안에 풀을 넣어 두면
되므로 구현상의 문제는 아니지만, **언어에 표현할 자리가 없다.**
→ D4를 고쳤다. `Lease.release(own self)`에는 풀도 `Db`도 없다. 전제가
충족됐다.
### 잘 된 것 — 설계가 버틴 부분
- **두 층 자원이 자연스럽게 중첩된다.** 풀이 바깥, lease가 안쪽
- **반납 ≠ 파괴가 시그니처로 표현된다.** `release(p: Pool, own l: Lease)`
- **획득 실패가 `Result`로 자연스럽다.** `Exhausted`가 호출자에게 선택을 준다
- `List[String]``List`는 내장 타입이라 import가 필요 없다. 모듈 `List`
가져오는 것은 그 타입의 함수를 쓸 때뿐이다 — 미사용 import lint가 정확히
이것을 지적했다
---
## LRU 캐시 (2026-08-30)
목적: 앞의 둘이 못 건드린 데를 친다. 파일과 풀은 정리 지점이 **렉시컬**이었다.
LRU는 아니다 — **축출은 삽입의 부작용으로 예측할 수 없는 때에 일어난다.**
산출물: `lru-cache/values.cool` 82줄, `lru-cache/resources.cool` 73줄.
### D10. Option/Result 패턴이 타입을 통째로 잃고 있었다 — 고침. 큰 것이었다
```cool
pub fn probe3(r: Result[Int, String]) -> String {
match r {
Ok(v) => v, // Int를 String 자리에 둔다
Err(e) => e,
}
}
```
**통과했다.** 원인: typecheck의 `env.ctors`에 내장 생성자가 없었다. 그래서
`None`은 생성자가 아니라 **`None`이라는 이름의 변수 바인딩**이 되고,
`Ok(v)``v``TUnknown`이 되어 무엇과도 맞았다.
즉 **이 언어의 핵심 오류 처리 수단인 Option/Result의 패턴 매칭이 타입을 전혀
검사하지 않고 있었다.** 소진성 검사는 자기 `is_ctor`에 내장을 갖고 있어서
이 사실을 덮고 있었다 — 빠진 경우는 잡으면서 타입은 안 봤다.
내장 열거형을 등록해서 고쳤다. 기존 코드는 하나도 안 깨졌다.
이 버그가 여태 안 보인 이유가 씁쓸하다. `match r { Ok(v) => Int.show(v) }`
같은 코드는 v가 TUnknown이어도 통과하므로 **아무도 이상함을 못 느낀다.**
틀린 코드를 써봐야 드러난다.
### D11. struct 필드와 affine 값 — 그리고 여기서 보안 구멍이 나왔다
처음 이렇게 적었다: "필드 접근은 빌림이라 affine 값을 꺼낼 수 없다."
**규칙으로는 맞았는데 구현이 그것을 강제하지 않고 있었다.**
나중에 "다 고쳤나"를 확인하려고 전부 다시 돌려보다 드러났다:
```cool
pub capability Pay { fn charge(n: Int) effects {Pay.charge} }
pub struct Wrapper { pay: Pay }
pub fn consume(own p: Pay) effects {Pay.charge}
pub fn duplicate(w: Wrapper) effects {Pay.charge} {
consume(w.pay)
consume(w.pay) // 같은 capability를 두 번 소비한다
}
```
**통과했다.** 그것도 `w`가 **빌린 값**인데도. 즉 **보안 정리 (ii)("safe
code에서 capability는 복제·위조되지 않는다")가 깨져 있었다.** 이 세션에서
찾은 것 중 가장 심각하다.
원인: `E_field`가 빌린 값(`v_use = true`)을 돌려주는데, **`Move` 문맥에서
그것을 검사하는 곳이 `E_ident` 분기에만 있었다.** 필드 접근은 그 분기를
지나가지 않는다.
고쳤다. `E_field`가 Move 자리에 놓이고 필드가 affine이면 오류다:
```
pay 필드는 affine이라 다른 함수에 넘길 수 없습니다
(v0에는 부분 move가 없습니다 — 꺼내려면 열거형으로 감싸십시오)
```
필드의 affinity를 알려면 바인딩의 선언 타입이 필요해서, move 검사기에
struct 필드 표와 바인딩 타입을 넣었다. **copyable 필드는 막지 않는다**
`w.label`은 통과한다.
그리고 이것이 LRU에서 열거형으로 우회한 것을 사후에 정당화한다. 그때는
"struct로는 안 되고 열거형으로는 된다"가 우연처럼 보였는데, **열거형이
유일한 길인 것이 규칙이었고 struct 쪽이 새고 있었을 뿐이다.**
#### 이 버그가 여태 안 보인 이유
`samples/05_move_errors.cool`은 자원 타입을 **직접** 다룬다. struct에 넣고
필드로 꺼내는 코드가 없었다. 개밥 먹기에서 **자원을 자료구조에 담는** 코드를
처음 쓰면서 드러났다.
#### (원래 기록) 열거형으로만 둘을 함께 돌려줄 수 있다
`put`은 캐시와 축출된 자원을 **함께** 돌려줘야 한다. 튜플이 없으니 struct다.
```cool
pub struct Put { cache: LeaseCache, evicted: Option[Db.Lease] }
...
match p.evicted { Some(l) => release(l), ... }
```
```
l은(는) 빌린 값이라 다른 함수에 넘길 수 없습니다
```
**필드 접근은 빌림이고, struct를 분해하는 패턴이 언어에 없다.** 그래서 struct에
넣은 affine 값은 다시 꺼낼 수 없다.
열거형은 된다 — 패턴이 분해하기 때문이다:
```cool
pub enum Put {
Kept(LeaseCache),
Evicted(LeaseCache, Db.Lease),
}
match p { Evicted(c, l) => release_then(db, pool, l, c), ... }
```
**즉 "둘을 함께 돌려주기"가 열거형으로만 가능하다.** 튜플이 없는 대가가
여기서 두 번째로 나온다(F5에 이어). 그리고 이건 취향 문제가 아니라
**표현 가능성 문제**다.
→ 해법 후보: struct 분해 패턴 + 부분 이동, 또는 튜플. 둘 다 문법 결정이다.
### D12. 축출은 렉시컬이 아니다 — `with`가 못 닿는 첫 자리
`with`는 렉시컬 수명만 다룬다. 축출된 lease는 **어느 블록에도 묶이지 않는다.**
삽입할 때 튀어나오고, 그 시점은 캐시 상태에 달려 있다.
그래서 `resource`가 렉시컬 전용이면 **자원을 담는 컨테이너를 언어로 만들 수
없다.** 그리고 이것이 이미 우리 설계에 나타나 있었다 — `Pool` 자체가 자원
캐시인데, `std-draft/db.cool`에서 **런타임이 주는 불투명한 capability**로
선언했다. 만들 수 없어서 그렇게 한 것이다.
두 갈래다:
- **(a) 자원은 렉시컬 전용** → 자원 컨테이너는 런타임만 만들 수 있다.
풀, 캐시, 레지스트리가 전부 언어 밖이 된다. 단순하고, 지금 상태가 그렇다
- **(b) 자원이 자료구조로 탈출할 수 있다** → `with`만으로 부족하고 결국
선형 타입이 필요하다. v0가 미룬 바로 그것
**(a)를 권한다.** 자원 컨테이너는 드물고, 만드는 쪽은 런타임이며, 쓰는 쪽은
lease를 렉시컬하게 빌린다. 다만 **이 선택이 무엇을 포기하는지 적어둬야
한다** — 사용자가 자기 자원 풀을 언어로 못 만든다.
### 자료구조 자체의 마찰 (값 판)
- **Map이 없어 O(n)이다.** LRU의 요점이 O(1)인데 표현할 수단이 없다
- **튜플이 없어 운반용 struct를 세 번 만들었다** — `Got`, `Put`, `Dropping`
- **`get`이 캐시를 새로 돌려줘야 한다.** 최근성이 바뀌므로. 공유 가변 상태가
없다는 것의 대가이고, 호출자가 캐시를 계속 실어날라야 한다
- **뒤에서 자르는 함수가 std에 없다.** `drop_last``reverse` 두 번으로 썼다
- **fold에 "첫 원소 건너뛰기"가 없어** `Dropping` struct를 또 만들었다
---
## 결정 — resource / with
**넣는다. 다만 D4가 먼저다.**
근거는 위 표다. 데이터 두 점에서 공통으로:
- `?`를 자원과 함께 쓸 수 없다 (2/2)
- 같은 정리 패턴을 각자 발명했다 (2/2)
- 누수가 조용히 통과한다 (2/2)
- 2.3 ~ 3.2배
그리고 `with`가 암묵적 drop보다 나은 이유가 여기서도 확인된다 — 정리 지점이
닫는 중괄호로 보이고, 정리의 effect가 effects 절에 나타나 검사된다.
## Mini Shell (2026-08-30)
목적: 앞의 셋이 못 건드린 모양을 친다.
| | 자원 개수 | 정리 지점 | 한 자원이 |
|---|---|---|---|
| 파일 | 하나 | 렉시컬 | 한 곳으로 |
| 풀 | 두 층 | 렉시컬 | 한 곳으로 |
| LRU | 한 종류, 여럿 | **비렉시컬** | 한 곳으로 |
| **셸** | **여러 종류, 여럿** | 비렉시컬 | **둘로 갈라져 서로 다른 곳으로** |
파이프 하나는 끝이 둘이고 각각 다른 자식에게 간다. 부모는 넘긴 뒤 아무것도
안 남는다. **어느 블록에도 묶이지 않는다.**
산출물: `std-draft/os.cool` 95줄, `mini-shell/parse.cool` 130줄,
`mini-shell/shell.cool` 155줄.
**처음으로 개밥이 실제로 돈다.** 파싱을 자원에서 떼어 놓았기 때문이다 —
`parse.cool`의 내장 test 5개가 `coolc test`로 통과한다. capability를 받지
않으므로 effect-free임이 검사로 증명되고, 그래서 순서에 의존하지 않는다.
### D13. 열거형 생성자가 인자를 소비하지 않았다 — 고침 (보안)
```cool
fn dup(own f: Fd) -> Option[Fd] {
let a = Some(f)
Some(f) // 통과했다
}
```
리스트 리터럴(`[f, f]`)과 struct 리터럴은 처음부터 Move 자리였는데
**생성자만 일반 호출로 취급돼 빠져 있었다.** 사용자 열거형도 내장
`Some`/`Ok`도 마찬가지였다.
**LRU가 affine 값을 열거형으로 실어나르는 우회를 쓰므로, 이 자리가
뚫려 있으면 그 우회 자체가 무의미했다.** 넣는 쪽이 검사되지 않으면
꺼내는 쪽을 아무리 조여도 소용이 없다.
### D14. 소유한 열거형을 두 번 분해할 수 있었다 — 고침 (보안)
```cool
fn dup(own p: Pipe) effects {Fd.close} {
match p { Ends(a, b) => { take(a) take(b) }, }
match p { Ends(a, b) => { take(a) take(b) }, } // 통과했다
}
```
`match`가 scrutinee를 항상 빌렸다. 조각은 owned로 꺼내 놓고 원본은
소비하지 않았으므로 **같은 파이프에서 같은 fd를 두 번 꺼낼 수 있었다.**
이름을 묶는 패턴으로 소유한 affine 값을 분해하면 그 값이 소비되게 했다.
이름을 묶지 않는 패턴(`Ends(_, _)`)은 아무것도 꺼내지 않으므로 그대로 둔다.
**D11·D13과 같은 하나의 규칙이 세 자리에서 새고 있었다**: v0에 부분 move가
없으므로 affine 값이 합성 구조를 드나드는 모든 자리는 Move 자리여야 한다.
struct 필드(D11), 열거형 생성자(D13), 열거형 분해(D14) — 셋 다 뚫려 있었다.
남은 자리를 세어 보면 리스트 리터럴, struct 리터럴, 함수 인자, 반환,
클로저 capture이고 이들은 처음부터 막혀 있었다.
### D15. 점 왼쪽을 검사하지 않았다 — 고침 (D1과 같은 병)
```cool
fn f() -> Bool {
Nonsense.whatever(1) // 통과했다
}
```
`Nonsense`는 어디에도 없다. 그런데 "모르는 것을 틀렸다고 말하지 않는다"는
규칙이 **가져오지도 않은 이름에까지 적용되고 있었다.** 그 규칙은 해소할 수
없는 패키지 import를 위한 것이지, 아무 이름이나 써도 된다는 뜻이 아니다.
내장 타입 이름도 모듈이 아니다. 타입으로 쓰는 데는 import가 필요 없지만
그 타입에 딸린 함수를 쓰려면 가져와야 한다 — prelude가 없다는 규칙에
예외를 두지 않는다.
**samples 셋이 이 구멍에 기대고 있었다** (02, 04, 06). `Code.show`,
`File.close`가 어디에도 정의되지 않은 채 통과하고 있었다. 셋 다 고쳤고,
06은 `own self` 덕에 `File.close(f)``f.close()`가 됐다.
### D16. variant 이름이 모듈 전역이다 — 열림
```cool
pub enum Stdin { Inherit, FromPipe(ReadFd) }
pub enum Stdout { Inherit, ToPipe(WriteFd) } // variant Inherit이 중복입니다
```
`InheritIn` / `InheritOut`이 됐다. **뜻이 아니라 언어의 제약이 고른
이름이다** — 철학 6이 지지 않아야 할 방식으로 진 자리다.
원인은 `match` 팔에 열거형 이름을 적지 않는다는 결정이다 (한 개념 한 방식).
그 결정 자체는 옳아 보이는데 대가가 여기서 나왔다. 고치려면 `Stdin.Inherit`
같은 한정 표기를 식과 패턴 양쪽에 넣어야 하고, 그러면 "한 방식"이 둘이 된다.
**미룬다.** 두 열거형이 같은 뜻의 variant를 갖는 일은 흔하지만, 지금은
데이터 점이 하나뿐이다. Reverse Proxy나 TCP Server에서 또 나오면 그때
결정한다.
### D17. 리스트에 넣은 자원을 꺼낼 수 없다 — 열림, 설계 결정 대기
셸의 자연스러운 모양은 "자식을 전부 띄우고 리스트에 모아 두었다가 한꺼번에
거두기"다. 파이프 버퍼가 차면 막히므로 **띄우기를 다 끝내기 전에 거둘 수
없다.**
넣는 것은 된다 — `[c1, c2]`는 통과하고 `List[Child]`는 전이적으로 affine이다.
**꺼내는 것이 안 된다.** `List.fold`의 시그니처가 `fn(own acc, a)`라 원소 `a`
빌린 값이고, 빌린 자원은 소비할 수 없다.
그래서 재귀가 됐다 — "뒤를 먼저 다 띄우고, 돌아오면서 자기 자식을 거둔다".
읽기에 나쁘지 않고 오히려 파이프라인의 모양과 닮았다. 하지만 이것은 우회지
답이 아니다.
이것이 **D12(축출은 비렉시컬)의 반대편**이다. D12는 자원이 컨테이너에서
예측 못 할 때 나가는 문제였고, 여기는 아예 나갈 방법이 없는 문제다. 둘이
같은 결정을 기다린다: **자원을 담는 컨테이너를 사용자가 만들 수 있는가.**
### D18. `spawn`이 자원을 가져갔는지 시그니처로 알 수 없다 — 열림
```cool
fn spawn(program: String, args: List[String],
own stdin: Stdin, own stdout: Stdout)
effects {Os.spawn} -> Result[Child, OsError]
```
`own`이 있으므로 부르는 쪽에서는 소비된다. 그런데 **실패했을 때 안에서
정리됐는지는 시그니처가 말하지 않는다.** 실제 `posix_spawn`은 실패해도 fd를
닫지 않는다 — 즉 이 시그니처는 진실을 감춘다.
정직하게 쓰면 `Result[Child, SpawnFailed]`이고 `SpawnFailed`가 fd를 돌려줘야
한다. 그러면 실패 처리가 매번 두 겹이 된다.
**이것은 언어의 결함이 아니라 계약의 문제다.** 다만 언어가 `own`을 강제하는
순간 이 질문이 모든 자원 API에서 나온다는 것을 기록해 둔다 — Rust도
`std::process`에서 같은 선택을 했고 fd를 돌려주지 않는다.
### 잘 된 것
- **파싱과 자원을 나눈 것이 옳았다.** 파싱이 순수하므로 내장 test로 그 자리에서
증명된다. 자원이 섞였으면 개밥 먹기 전체가 "쓰기만 한 것"으로 남았다
- **`?`가 파싱에서는 필요 없었다.** fold의 누적자가 `Result`라 실패가 그대로
실려간다 — 조기 탈출 없이도 뒤 조각이 앞의 실패를 덮지 않는다
- **읽는 끝과 쓰는 끝을 타입으로 나눈 것**이 값싸고 정확했다. 읽는 끝에 쓰는
것은 프로그램의 결함이고, 결함은 타입이 막는 편이 낫다
- **`own self` 뒤로 `child.wait()`가 자연스럽다.** 1판 문법이었다면
`os.wait(child)`였고 `Os` 권한을 자식 거두기까지 들고 다녀야 했다
## Process Supervisor (2026-08-30)
목적: 앞의 넷은 자원 모델이었다. 여기는 **실패 위상**이다 — 자식이 죽었을
때 그 사실이 어디까지 가는가, 누가 멈출 수 있는가.
산출물: `std-draft/sup.cool` 60줄, `supervisor/workers.cool` 130줄.
### 확인된 것 — 문서의 주장 셋이 코드가 됐다
**① spawn은 하나의 개념이고 실패가 어디까지 가느냐가 권한으로 갈린다.**
`sc.spawn(f)`는 실패가 부모로 가고 `sup.supervise(sc, f, policy)`는 여기서
멈춘다. 실패를 삼키려면 `Supervisor`를 받았어야 하고 **그것이 시그니처에
보인다.** 문법 변경 없이 성립한다.
**② 재시작 가능한 자식은 copyable 클로저여야 한다 — affinity가 검사한다.**
이것이 이번 개밥의 가장 값싼 확인이다. `supervise`의 body가 무표기 `fn`이므로:
```
affine 값을 capture한 클로저는 fn 자리에 넘길 수 없습니다 (affine fn이어야 합니다)
```
lease를 들고 가는 워커는 `supervise`에 못 간다. **한 번밖에 못 도는 것을
재시작할 수는 없다** — 얼랭이 관례로 지키는 것을 타입이 막는다. 한 번만
돌리는 `once``affine fn`을 받으므로 그런 워커도 받는다.
그래서 재시작 가능한 워커의 모양이 정해진다: **자원을 밖에서 받지 말고
안에서 획득하고 안에서 반납한다.** 풀을 빌려 받을 뿐 lease를 들고 오지
않으므로 클로저가 affine이 아니다.
**③ 격리 경계는 recover가 아니다.** `Death`에 담을 수 있는 것이 메시지뿐이다.
죽은 일꾼이 계산하던 값도, 들고 있던 자원도 넘어오지 않는다. **담을 자리가
있으면 그것이 recover다** — 이 열거형의 모양이 그 원칙의 코드 수준 의미다.
### D19. 가져온 모듈의 struct를 만들 수 없었다 — 고침
```cool
sup.supervise(sc, body, Sup.Policy { retries: 3 })
^ )이(가) 필요합니다 — { 발견
```
`Alias.Name { ... }`가 문법에 없었다. **타입은 쓸 수 있는데 값은 못 만드는
상태**였고, 개밥 넷이 전부 같은 모듈 안에서만 struct를 만들어 한 번도 안
걸렸다.
고치는 과정이 문법 검증 장치의 값을 보여줬다. 처음에 점을 `field_sfx`
`name_or_struct` 양쪽에 두자 **LL(1) 검사가 즉시 진짜 충돌 2건을 냈다**
같은 문장을 두 프로덕션이 읽을 수 있었다. 다음 시도(struct_body를 field_sfx에
붙이기)는 LL(1)은 통과했지만 **문법-파서 대조와 생성 커버리지가 잡았다**:
문법은 `f().x { }`를 만드는데 파서가 거부했다. 최종형은 struct를 만들 수 있는
문맥(`<yes>`)에서만 점을 미리 먹는다. 세 장치가 각각 다른 실수를 잡았다.
### D20. 가져온 열거형의 이름을 한정 없이 쓰면 조용히 통과했다 — 고침 (보안)
두 자리에서 각각 다르게 샜다.
**패턴에서는 바인딩이 됐다.**
```cool
match s {
Done => ..., // Sup.Done이어야 한다. 여기서는 그냥 이름 하나다
GaveUp(d) => ..., // "이 팔은 도달할 수 없습니다" — 틀린 줄을 가리킨다
}
```
규칙 자체는 옳다 ("한정되었으면 언제나 생성자, 아니면 바인딩"). 문제는
**진단이 엉뚱한 줄을 가리킨다**는 것이다. 첫 팔이 전부를 잡는 것이 원인인데
오류는 둘째 팔에 난다. 이제 이렇게 말한다:
```
Done은(는) 가져온 열거형의 variant입니다 — Sup.Done로 적으십시오
(한정 없는 이름은 바인딩입니다)
```
**식에서는 모르는 이름이 됐다. 이쪽이 보안 구멍이다.**
```cool
fn dup(own f: K.Fd) -> K.Box {
let a = Wrap(f)
Wrap(f) // 통과했다
}
```
`Wrap`이 생성자인 줄 모르므로 move 검사의 `is_ctor`가 안 걸리고, 인자가
Move가 아니라 빌림이 된다. **affine 값이 복제된다.** D13(생성자가 인자를
소비하지 않았다)을 고쳤는데, 가져온 생성자는 애초에 생성자로 인식되지도
않고 있었다.
**Mini Shell에 다섯 군데 있었다.** `Ends(r, w)`, `ToPipe(w)`, `FromPipe(r)`,
`InheritIn`, `InheritOut` — 즉 **이 개밥이 시험하려던 파이프 처리 전체가
검사되지 않고 있었다.** 고친 뒤 확인하니 이제 진짜로 잡는다:
```
인자: Os.ReadFd이(가) 필요한데 Os.WriteFd입니다
w은(는) 이미 move되었습니다 (81:81에서 소비)
```
**이것이 개밥 먹기가 통과했다는 사실을 믿으면 안 되는 이유다.** 합격 기준 ②
("모듈이 실제로 해소될 것")는 모듈 수준의 공허함만 막는다. **이름 하나가
조용히 불투명해지는 것은 못 막는다.** D1 → D15 → D20이 같은 병의 세 단계였다:
모듈 전체 → 점 왼쪽 → 점을 안 찍은 이름.
### D21. spawn한 태스크의 값이 갈 데가 없다 — 열림
```cool
work.spawn(fn() { announce(log, run_worker(...)) })
```
일꾼이 어떻게 끝났는지 부모가 알 수 없다. 클로저의 반환값이 갈 데가 없기
때문이다 — `Future``Task[a]`도 없고, `scope`의 join은 값을 만들지 않는다.
그래서 보고를 값이 아니라 **Log capability**로 한다. 얼랭도 그렇게 하므로
틀린 모양은 아니고, 오히려 "결과는 effect로 나간다"가 감독과 잘 맞는다.
다만 **취소와 backpressure에 가면 이것으로 안 될 가능성이 높다** — 되돌려
받을 값이 있어야 흐름을 조절할 수 있다.
Reverse Proxy(#4) 전에 결정해야 한다.
---
---
## 테스트 하네스의 결함 (2026-08-30)
개밥 먹기가 아니라 저장소 자체에서 나온 것이므로 따로 적는다.
`test/test_coollang.ml`의 요약과 `exit 1`이 **파일 184줄**에 있었다. OCaml의
최상위 `let ()`은 파일 순서대로 돌므로 **그 뒤에 쓴 1500줄의 검사는 FAIL을
찍고도 종료 코드가 0이었다.**
**실패한 테스트를 커밋할 수 있었다.** 빠른 검증 루프가 목적인 저장소에서
검증이 조용히 통과하는 것이 가장 나쁜 결함이다. 요약을 파일 끝으로 옮겼다.
발견 경위가 이 저장소의 방법론을 그대로 보여준다 — 새 테스트를 넣고
"일부러 틀리게 만들어 FAIL이 나는지" 확인하다가 나왔다. 통과를 믿지 않고
실패를 확인하는 습관이 아니었으면 안 보였다.
---
**순서** (2026-08-30 갱신 — 1·3 완료)
1. ~~**D4** — 수신자를 소비하는 메서드~~ → 고침. `own self`
2. **`resource` 종류와 `with`** — 아래 "with를 미루는 이유" 참고
3. ~~Mini Shell~~ → 완료. D13–D18이 여기서 나왔다
4. ~~Process Supervisor~~ → 완료. D19–D21. 문서의 주장 셋이 코드가 됐다
5. 다음은 Reverse Proxy(#4). 그 전에 D21(태스크의 값)을 결정해야 한다
**LRU가 더한 제약**: 자원은 렉시컬 전용으로 간다. 자원을 담는 컨테이너는
런타임만 만들 수 있다 — 축출처럼 비렉시컬한 정리는 `with`가 못 닿기 때문이다.
사용자가 자기 자원 풀을 언어로 못 만드는 것이 그 대가다.
---
## `with`를 미루는 이유 (2026-08-30 결정)
네 개밥을 다 쓰고 나서 내린 결정이다. 셋을 정하고 하나를 미룬다.
### 1. `resource`라는 새 종류는 넣지 않는다 — 확정
capability가 이미 그것이다. affine이고, 위조되지 않고, 런타임만 만든다.
`resource`가 더하는 사실은 "정리 메서드가 있다" 하나뿐인데, 그것은 `own self`
말한다. 종류를 하나 더 만들면 **95%가 같은 두 종류**가 생기고 그것이 "한 개념
한 방식"이 금지하는 모양이다.
### 2. 누수는 lint로 보인다 — 넣음
D3의 답을 절반 냈다. `?``return`이 있는 자리에서, 소유하고 있고 아직 아무
데도 넘기지 않은 affine 값이 있으면 말한다.
```
?이(가) 여기서 빠져나가면 f이(가) 정리되지 않습니다 (?는 뒤의 정리를 건너뜁니다)
```
**측정**: 저장소 전체에서 네 곳. `atomic-update/naive` 2, `db-pool/naive` 2.
**오탐 0건** — careful 판 둘과 samples 전부가 조용하다. naive/careful 쌍이
정확히 나뉜다.
타입 층의 정책은 그대로다 — 오용 금지, 누수 허용. lint는 막지 않고 말한다.
### 3. `with`는 미룬다 — 설계가 갈라져 있고, 데이터가 아직 부족하다
두 갈래가 있고 둘 다 그럴듯하다.
**갈래 A — 자동 반납.** 블록을 나갈 때 런타임이 정리 메서드를 부른다
(Python `with`, C# `using`). `?`가 그대로 쓸 수 있게 되고 `careful.cool`
58줄이 `naive.cool` 18줄로 접힌다. 대가: **선언되지 않은 effect가 수행된다.**
이 언어에서 effect는 시그니처에 다 적혀야 하는데, 안 보이는 호출이 생긴다.
그리고 정리 메서드가 둘인 자원(트랜잭션의 commit/abort)을 다루지 못한다.
**갈래 B — 소비 강제.** 블록이 끝나기 전에 모든 길에서 그 값을 소비해야
하고, 아니면 오류다. 아무것도 자동으로 부르지 않는다. 숨은 effect가 없고
commit/abort가 자연스럽다. 대가: **마찰이 그대로 남는다.** 컴파일러가
"여기서 안 닫았다"고 말해 줄 뿐, 중첩된 match를 손으로 쓰는 일은 똑같다.
**미루는 이유는 Mini Shell이다.** 셸이 보여준 모양에는 `with`가 닿지 않는다.
파이프 하나는 끝이 둘이고 각각 다른 자식에게 간다. 부모에게는 아무것도 안
남고, 어느 블록에도 묶이지 않는다. 즉 **가장 어려운 자원 코드가 렉시컬하지
않다.** 렉시컬 도구를 먼저 넣으면 그 도구가 닿지 않는 자리를 나중에 다시
설계해야 하고, 그때는 이미 두 방식이 공존한다.
`own self`가 D9이 요구한 것을 이미 줬으므로 급하지 않다. 정리는 자원만으로
표현되고, 도우미의 시그니처가 진짜 의존을 말한다.
**결정을 다시 여는 조건**: Process Supervisor(#3)나 Reverse Proxy(#4)에서
렉시컬한 자원 코드가 다수를 차지하면 갈래 A·B 중 하나를 고른다. 셸처럼
비렉시컬한 것이 또 나오면 `with`가 아니라 컨테이너 문제(D17)를 먼저 푼다.
**미루는 것**: crash 시 정리 여부. `with`를 넣어도 이 질문은 열려 있다 —
정상 종료·`return`·`?`에서는 돌고 crash에서는 안 도는 것이 지금의 잠정
답이지만, 감독(supervision)이 들어오면 다시 봐야 한다.
+83
View File
@@ -0,0 +1,83 @@
# dogfoods — 실제 유스케이스로 언어를 압박한다
`samples/`가 검사기를 시험한다면 여기는 **언어가 실제 문제를 표현할 수 있는지**를
시험한다. 성격이 다르므로 폴더를 나눈다.
## 방법
프로그램을 **쓰되 돌리지 않는다.** `std-draft/`에 런타임이 구현한다고 가정한
선언을 두고, 그것에 대고 실제 프로그램을 쓴다.
"가상"이 아니다. `std/*.cool`이 원래 본문 없는 선언이므로 **실행만 빼고 전부
진짜로 검사된다**:
```
lexer ✓ parser ✓ 이름 해소 ✓ 타입 ✓ effect ✓
capability ✓ move/affinity ✓ 소진성 ✓ interface hash ✓
run ✗
```
종이 스케치가 아니라 컴파일러가 검증한 설계다.
## 합격 기준
```
① coolc check exit 0
② 모듈이 실제로 해소될 것
```
②가 없으면 ①이 공허하다. import가 조용히 해소되지 않으면 그 모듈의 이름이
전부 불투명해지고, 검사기는 "모르는 것을 틀렸다고 말하지 않는다"는 원칙에 따라
무엇이든 통과시킨다. **실제로 첫 시도에서 이 일이 일어났다** (D1 참고).
## 규율
`std-draft`의 선언은 독립적으로 정당화되어야 한다.
- 지금 런타임이 가진 권한으로 구현 가능할 것
- effect를 전부 선언할 것
- **실패 양상을 선언하는 자리에서 정할 것** — 무엇이 `Result`이고 무엇이
`crash`인지
- 없는 언어 기능에 기대지 말 것. 필요하다는 것이 드러나면 **그것이 발견이지
지름길이 아니다**
프로그램이 예뻐 보이도록 API를 발명하면 아무것도 배우지 못한다.
## 한계
이 기준이 증명하는 것은 **표현 가능성**이지 의미론의 정확성이 아니다.
`check`가 통과한다고 그 프로그램이 옳게 도는 것은 아니다 — 돌려본 적이 없다.
**Mini Shell에서 그 한계가 반쯤 깨졌다.** 파싱을 자원에서 떼어 놓았더니
`mini-shell/parse.cool`은 실제로 돈다 — 내장 test 5개가 `coolc test`
통과한다. 자원을 안 쓰는 부분은 돌릴 수 있다는 뜻이고, 그것은 개밥을
어떻게 나눠 써야 하는지를 말해 준다.
## 세트
| | | 압박하는 곳 | 상태 |
|---|---|---|---|
| 1 | **Mini Shell** | **여러 자원, 하나가 둘로 갈라짐** | **완료** |
| 2 | **Atomic File Updater** | **자원 수명, 실패 경로** | **완료** |
| 3 | **Process Supervisor** | **실패 위상** | **완료** |
| 4 | Reverse Proxy | 취소, backpressure | 대기 |
| 5 | TCP Server | 규모의 API 모양 | 대기 |
| 6 | DB Pool | lease, 자원 정리 | **완료** |
| + | **LRU Cache** | **비렉시컬 정리 (축출)** | **완료** |
| 7 | Resource Exhaustion | 실패 의미론의 최악 조건 | **미룸** — 런타임 속성이라 안 돌리면 알 수 없다. 언어 층 질문("무엇이 Result이고 무엇이 crash인가")은 std-draft 규율로 흡수했다 |
순서는 자원 모델(2, 6, 1) → 동시성·취소(3, 4, 5)다. 2번을 먼저 둔 이유는
**동시성을 안 섞고 자원 모델만 시험할 수 있는 유일한 것**이기 때문이다.
자원 모델 쪽 넷이 끝났다. 넷이 각각 다른 모양이었다:
| | 자원 개수 | 정리 지점 | 한 자원이 |
|---|---|---|---|
| 파일 | 하나 | 렉시컬 | 한 곳으로 |
| 풀 | 두 층 | 렉시컬 | 한 곳으로 |
| LRU | 한 종류, 여럿 | 비렉시컬 | 한 곳으로 |
| 셸 | 여러 종류, 여럿 | 비렉시컬 | **둘로 갈라져 서로 다른 곳으로** |
이 넷이 `with`를 미루기로 한 근거다 (FINDINGS의 "`with`를 미루는 이유").
발견은 `FINDINGS.md`에 쌓고, 확정된 결정은 `docs/thesis.md`로 올린다.
+62
View File
@@ -0,0 +1,62 @@
// 제대로 정리하는 버전.
//
// naive.cool과 같은 일을 하지만 모든 실패 경로에서 핸들을 닫고 임시 파일을
// 지운다. 두 파일의 차이가 이 개밥 먹기의 산출물이다.
//
// ※ 2판. `own self` 뒤에 다시 썼다. 도우미들이 더 이상 fs를 들고 다니지
// 않는다 — 닫는 데 필요한 것이 핸들 자신뿐이기 때문이다.
import "../std-draft/fs" as Fs
import "cool.dev/std/string" as String
pub fn update(fs: Fs.Fs, path: String, contents: String)
effects {Fs.create, WriteFile.write, WriteFile.sync, WriteFile.close,
Fs.rename, Fs.remove}
-> Result[Unit, Fs.IoError] {
let tmp = String.concat(path, ".tmp")
match fs.create(tmp) {
Err(e) => Err(e),
Ok(f) => finish(fs, f, tmp, path, contents),
}
}
// 핸들을 얻은 뒤. 여기서부터 모든 실패 경로가 f를 닫고 tmp를 지워야 한다.
// ? 를 쓸 수 없다 — 조기 반환이 정리를 건너뛰기 때문이다.
pub fn finish(
fs: Fs.Fs,
own f: Fs.WriteFile,
tmp: String,
path: String,
contents: String,
) effects {WriteFile.write, WriteFile.sync, WriteFile.close, Fs.rename, Fs.remove}
-> Result[Unit, Fs.IoError] {
match f.write(contents) {
Err(e) => abandon(fs, f, tmp, e),
Ok(_) => match f.sync() {
Err(e) => abandon(fs, f, tmp, e),
Ok(_) => match f.close() {
Err(e) => discard(fs, tmp, e),
Ok(_) => match fs.rename(tmp, path) {
Err(e) => discard(fs, tmp, e),
Ok(_) => Ok(unit),
},
},
},
}
}
// 핸들을 아직 들고 있는 실패. 닫고 지운다.
// 정리 중의 실패는 일부러 버린다 — 원래 오류가 더 중요하다.
// let _ = 가 버린다는 사실을 코드에 보이게 한다.
pub fn abandon(fs: Fs.Fs, own f: Fs.WriteFile, tmp: String, e: Fs.IoError)
effects {WriteFile.close, Fs.remove} -> Result[Unit, Fs.IoError] {
let _ = f.close()
discard(fs, tmp, e)
}
// 핸들은 이미 없다. 임시 파일만 지운다.
pub fn discard(fs: Fs.Fs, tmp: String, e: Fs.IoError)
effects {Fs.remove} -> Result[Unit, Fs.IoError] {
let _ = fs.remove(tmp)
Err(e)
}
+20
View File
@@ -0,0 +1,20 @@
// 순진한 버전 — ?로 짧게 쓴 것.
//
// 처음 썼을 때 이 파일의 목적은 "통과한다는 사실을 보여주는 것"이었다.
// 이제 lint가 두 자리를 가리킨다 — write와 sync의 ?다. 그 둘에서 빠져나가면
// f가 닫히지 않는다. 타입 층에서는 여전히 오류가 아니다 (누수 허용).
import "../std-draft/fs" as Fs
import "cool.dev/std/string" as String
pub fn update(fs: Fs.Fs, path: String, contents: String)
effects {Fs.create, WriteFile.write, WriteFile.sync, WriteFile.close, Fs.rename}
-> Result[Unit, Fs.IoError] {
let tmp = String.concat(path, ".tmp")
let f = fs.create(tmp)?
f.write(contents)?
f.sync()?
f.close()?
fs.rename(tmp, path)?
Ok(unit)
}
+58
View File
@@ -0,0 +1,58 @@
// 제대로 반납하는 버전.
//
// Atomic Updater와 비교하는 것이 목적이다. 자원이 두 층이고, 반납이
// 파괴가 아니며, 획득이 실패할 수 있다 — 파일에는 셋 다 없었다.
//
// ※ 2판. `own self` 뒤에 다시 썼다. 1판과의 차이는 아래 주석에 적는다.
import "../std-draft/db" as Db
// 한 요청. 성공하든 실패하든 lease를 반납한다.
pub fn load_user(pool: Db.Pool, sql: String)
effects {Pool.acquire, Lease.query, Lease.release}
-> Result[List[String], Db.DbError] {
match pool.acquire() {
Err(e) => Err(e),
// 1판에서는 l을 소비하는 인자와 l을 쓰는 인자를 한 호출에 섞을 수
// 없어(D8) 순서를 손으로 풀어야 했다. 이제 질의는 수신자를 빌리고
// 반납은 수신자를 소비하므로 둘이 같은 인자 목록에 만날 일이 없다.
Ok(l) => {
let r = l.query(sql)
release_then(l, r)
},
}
}
// 반납하고 원래 결과를 그대로 돌려준다.
// 반납 실패는 일부러 버린다 — 원래 결과가 더 중요하다.
//
// 1판은 (db, pool, l, r) 넷을 받았다. 이제 둘이다 — 반납에 필요한 것이
// lease 자신뿐이기 때문이다. 도우미의 시그니처가 정리의 진짜 의존을 말한다.
pub fn release_then(
own l: Db.Lease,
r: Result[List[String], Db.DbError],
) effects {Lease.release} -> Result[List[String], Db.DbError] {
let _ = l.release()
r
}
// 프로그램 하나. 풀을 열고 쓰고 닫는다.
pub fn run(db: Db.Db, url: String, sql: String)
effects {Db.pool, Pool.acquire, Pool.close, Lease.query, Lease.release}
-> Result[List[String], Db.DbError] {
match db.pool(url, 8) {
Err(e) => Err(e),
Ok(pool) => {
let rows = load_user(pool, sql)
close_then(pool, rows)
},
}
}
pub fn close_then(
own pool: Db.Pool,
r: Result[List[String], Db.DbError],
) effects {Pool.close} -> Result[List[String], Db.DbError] {
let _ = pool.close()
r
}
+29
View File
@@ -0,0 +1,29 @@
// 순진한 버전 — ?로 짧게 쓴 것.
//
// Atomic Updater와 같은 병이 나오는지 본다. 다만 자원이 두 층이다 —
// 풀과 lease. 그리고 반납은 파괴가 아니다.
//
// lint가 두 층을 각각 가리킨다: query의 ?에서 lease가, load_user의 ?에서
// 풀이 샌다. 층이 늘면 새는 자리도 그만큼 는다.
import "../std-draft/db" as Db
// 한 요청. lease를 빌리고, 질의하고, 반납한다.
pub fn load_user(pool: Db.Pool, sql: String)
effects {Pool.acquire, Lease.query, Lease.release}
-> Result[List[String], Db.DbError] {
let l = pool.acquire()?
let rows = l.query(sql)? // 실패하면 lease가 반납되지 않는다
l.release()?
Ok(rows)
}
// 프로그램 하나. 풀을 열고 쓰고 닫는다.
pub fn run(db: Db.Db, url: String, sql: String)
effects {Db.pool, Pool.acquire, Pool.close, Lease.query, Lease.release}
-> Result[List[String], Db.DbError] {
let pool = db.pool(url, 8)?
let rows = load_user(pool, sql)? // 실패하면 풀도 안 닫힌다
pool.close()?
Ok(rows)
}
+69
View File
@@ -0,0 +1,69 @@
// LRU 캐시 — 자원 판.
//
// 앞의 두 개밥(파일, 풀)은 정리 지점이 렉시컬이었다. 여기는 아니다.
// 축출은 삽입의 부작용으로, 예측할 수 없는 때에 일어난다.
//
// 이것이 with 결정 전에 알아야 할 자리다 — with는 렉시컬 수명만 다루는데
// 축출된 자원은 어느 블록에도 묶이지 않는다.
import "cool.dev/std/list" as List
import "../std-draft/db" as Db
pub struct Held {
key: String,
lease: Db.Lease,
}
// Lease를 필드로 가지므로 전이적으로 affine이다 — 선언하지 않아도 유도된다.
pub struct LeaseCache {
cap: Int,
held: List[Held],
}
// put은 캐시와 "떨어져 나온 자원"을 함께 돌려줘야 한다. 튜플이 없다.
//
// struct로 쓰면 막힌다 — 필드 접근은 빌림이라 affine 값을 꺼낼 수 없고,
// struct를 분해하는 패턴이 언어에 없다. 열거형은 패턴이 분해하므로 꺼낼 수
// 있다. 그래서 "둘을 함께 돌려주기"가 열거형으로만 가능하다.
pub enum Put {
Kept(LeaseCache),
Evicted(LeaseCache, Db.Lease),
}
pub fn empty(cap: Int) -> LeaseCache {
LeaseCache { cap: cap, held: [] }
}
// 넣는다. 넘치면 가장 오래된 것이 떨어져 나온다.
//
// 떨어져 나온 lease는 호출자가 반납해야 한다. 언어는 그것을 강제하지 못한다 —
// Put.evicted를 무시하고 버려도 아무 말이 없다. 누수 허용이므로.
pub fn put(own c: LeaseCache, key: String, own l: Db.Lease) -> Put
// 호출자가 해야 하는 일. 이 함수를 안 부르면 조용히 샌다.
pub fn drain(own p: Put) effects {Lease.release} -> LeaseCache {
match p {
Kept(c) => c,
Evicted(c, l) => release_then(l, c),
}
}
// ※ 2판. `own self` 뒤로 db도 pool도 필요 없다 — 반납은 lease 자신이 안다.
pub fn release_then(own l: Db.Lease, own c: LeaseCache)
effects {Lease.release} -> LeaseCache {
let _ = l.release()
c
}
// 쓰는 쪽. 매 삽입마다 축출을 받아 처리해야 한다.
pub fn add(pool: Db.Pool, own c: LeaseCache, key: String)
effects {Pool.acquire, Lease.release} -> Result[LeaseCache, Db.DbError] {
match pool.acquire() {
Err(e) => Err(e),
Ok(l) => Ok(drain(put(c, key, l))),
}
}
pub fn size(c: LeaseCache) -> Int {
List.len(c.held)
}
+82
View File
@@ -0,0 +1,82 @@
// LRU 캐시 — 값 판.
//
// 먼저 자원 없이 써서 자료구조 자체의 마찰을 본다. 자원 판은 resources.cool.
import "cool.dev/std/list" as List
// ※ Option[Int]의 Option은 내장 타입이라 import가 필요 없다.
pub copyable struct Entry {
key: String,
value: Int,
}
// 최근에 쓴 것이 앞. Map이 없어 리스트를 훑는다 — O(n)이다.
// LRU의 요점이 O(1)인데 그것을 표현할 수단이 언어에 없다.
pub copyable struct Cache {
cap: Int,
entries: List[Entry],
}
pub fn empty(cap: Int) -> Cache {
Cache { cap: cap, entries: [] }
}
// get이 최근성을 바꾸므로 캐시를 새로 돌려줘야 한다.
// 그런데 값도 같이 돌려줘야 한다 — 튜플이 없어 struct를 하나 더 만든다.
pub copyable struct Got {
cache: Cache,
value: Option[Int],
}
pub fn get(c: Cache, key: String) -> Got {
let hit = List.first(List.filter(c.entries, fn(e) { e.key == key }))
match hit {
None => Got { cache: c, value: None },
Some(e) => Got {
cache: Cache { cap: c.cap, entries: touch(c.entries, key, e) },
value: Some(e.value),
},
}
}
// 찾은 항목을 앞으로 옮긴다.
pub fn touch(entries: List[Entry], key: String, e: Entry) -> List[Entry] {
List.concat([e], List.filter(entries, fn(x) { !(x.key == key) }))
}
pub fn put(c: Cache, key: String, value: Int) -> Cache {
let without = List.filter(c.entries, fn(x) { !(x.key == key) })
let added = List.concat([Entry { key: key, value: value }], without)
Cache { cap: c.cap, entries: evict(added, c.cap) }
}
// 넘치면 뒤에서 떨어뜨린다. 값이라 그냥 사라진다 — 자원이면 이야기가 다르다.
pub fn evict(entries: List[Entry], cap: Int) -> List[Entry] {
if List.len(entries) <= cap {
entries
} else {
evict(drop_last(entries), cap)
}
}
// 마지막 하나를 뺀 리스트. 뒤에서 자르는 함수가 std에 없다.
pub fn drop_last(entries: List[Entry]) -> List[Entry] {
List.reverse(drop_first(List.reverse(entries)))
}
pub fn drop_first(entries: List[Entry]) -> List[Entry] {
List.fold(entries, Dropping { first: true, kept: [] }, fn(own d, e) {
if d.first {
Dropping { first: false, kept: d.kept }
} else {
Dropping { first: false, kept: List.push(d.kept, e) }
}
}).kept
}
// fold에 "첫 원소 건너뛰기"가 없어 운반용 struct를 또 만든다.
pub copyable struct Dropping {
first: Bool,
kept: List[Entry],
}
+121
View File
@@ -0,0 +1,121 @@
// Mini Shell — 줄을 파이프라인으로 읽는 부분.
//
// 이 파일에는 자원이 하나도 없다. 일부러 나눴다 — 파싱은 순수하므로
// 내장 test로 그 자리에서 증명할 수 있고, 자원이 섞이면 그러지 못한다.
// 테스트는 capability를 받지 않으므로 effect-free임이 검사로 증명된다.
import "cool.dev/std/list" as List
import "cool.dev/std/string" as String
import "cool.dev/std/option" as Option
import "cool.dev/std/result" as Result
import "cool.dev/std/test" as Test
// 명령 하나. program과 args를 나눠 둔다 —
// argv[0]에 이름을 겹쳐 두는 관례는 둘 중 뭐가 진짜인지 매번 묻게 만든다.
pub copyable struct Command {
program: String,
args: List[String],
}
// 파이프로 이어진 명령들. 비어 있을 수 있다 (빈 줄).
pub copyable struct Pipeline {
stages: List[Command],
}
// 무엇이 잘못됐는지. 셸은 오류를 사람에게 보여주고 계속 돈다 —
// 죽지 않는다. 그래서 crash가 아니라 Result다.
pub enum ParseError {
// "ls | | wc" — 가운데가 비었다
EmptyStage,
// "|" 로 시작하거나 끝났다
DanglingPipe,
}
// 공백으로 쪼개고 빈 조각을 버린다.
// 연속된 공백이 빈 조각을 만들기 때문에 filter가 필요하다.
pub fn words(s: String) -> List[String] {
List.filter(String.split(String.trim(s), " "), fn(w) { !String.is_empty(w) })
}
// 조각 하나를 명령으로. 조각이 비면 EmptyStage다.
pub fn stage(s: String) -> Result[Command, ParseError] {
let ws = words(s)
match List.first(ws) {
None => Err(EmptyStage),
Some(p) => Ok(Command { program: p, args: List.rest(ws) }),
}
}
// 줄 하나를 파이프라인으로.
//
// 빈 줄은 오류가 아니다 — 사람이 엔터만 친 것이고, 셸은 아무 일도 하지
// 않는다. 그것과 "ls | | wc"는 다른 일이므로 구분한다.
pub fn line(raw: String) -> Result[Pipeline, ParseError] {
let t = String.trim(raw)
if String.is_empty(t) {
Ok(Pipeline { stages: [] })
} else {
stages(String.split(t, "|"))
}
}
// 조각들을 차례로 읽는다. 하나라도 실패하면 전체가 실패한다.
//
// fold로 접는다. 누적자가 Result라 실패가 그대로 실려간다 — 조기 탈출이
// 없어도 뒤 조각이 앞의 실패를 덮지 않는다.
pub fn stages(parts: List[String]) -> Result[Pipeline, ParseError] {
List.fold(parts, Ok(Pipeline { stages: [] }), fn(own acc, part) {
match acc {
Err(e) => Err(e),
Ok(p) => match stage(part) {
Err(e) => Err(e),
Ok(c) => Ok(Pipeline { stages: List.push(p.stages, c) }),
},
}
})
}
// 내장 명령인가. cd와 exit은 자식 프로세스로 띄우면 뜻이 없다 —
// 자식의 디렉터리를 바꿔 봐야 자식이 죽으면 사라진다.
pub fn is_builtin(c: Command) -> Bool {
c.program == "cd" || c.program == "exit"
}
// ------------------------------------------------------------------
// 테스트
// ------------------------------------------------------------------
test "공백은 몇 칸이든 한 번 자른다" {
Test.assert(List.len(words(" ls -l ")) == 2)
Test.assert(Option.unwrap_or(List.first(words(" ls -l ")), "") == "ls")
}
test "빈 줄은 아무 단계도 없는 파이프라인이다" {
match line(" ") {
Err(_) => Test.assert(false),
Ok(p) => Test.assert(List.is_empty(p.stages)),
}
}
test "파이프가 단계를 나눈다" {
match line("ls -l | grep foo | wc -l") {
Err(_) => Test.assert(false),
Ok(p) => Test.assert(List.len(p.stages) == 3),
}
}
test "빈 단계는 오류다" {
Test.assert(!Result.is_ok(line("ls | | wc")))
Test.assert(!Result.is_ok(line("| ls")))
Test.assert(!Result.is_ok(line("ls |")))
}
test "program과 args가 나뉜다" {
match stage("grep -i foo") {
Err(_) => Test.assert(false),
Ok(c) => {
Test.assert(c.program == "grep")
Test.assert(List.len(c.args) == 2)
},
}
}
+151
View File
@@ -0,0 +1,151 @@
// Mini Shell — 자원을 다루는 부분.
//
// 앞의 세 개밥(파일, 풀, LRU)이 못 건드린 데를 친다.
// 파일: 자원 하나, 렉시컬
// 풀: 자원 두 층, 렉시컬
// LRU: 정리 지점이 비렉시컬. 다만 자원은 한 종류
// 여기: **자원이 여럿이고, 하나가 둘로 갈라져 서로 다른 곳으로 간다**
//
// 파이프 하나는 끝이 둘이고 각각 다른 자식에게 간다. 부모는 넘긴 뒤
// 아무것도 안 남는다. 어느 블록에도 묶이지 않는다 — `with`가 닿지 않는
// 모양이 무엇인지 여기서 정확히 보인다.
import "cool.dev/std/list" as List
import "../std-draft/os" as Os
import "./parse" as Parse
// ------------------------------------------------------------------
// 한 줄 실행
// ------------------------------------------------------------------
// 파이프라인 하나를 돌리고 마지막 단계의 종료 코드를 준다.
//
// 빈 파이프라인(사람이 엔터만 침)은 0이다. 아무 일도 안 한 것은 성공이다.
pub fn run(os: Os.Os, p: Parse.Pipeline)
effects {Os.pipe, Os.spawn, Child.wait, ReadFd.close, WriteFd.close}
-> Result[Int, Os.OsError] {
if List.is_empty(p.stages) {
Ok(0)
} else {
from(os, p.stages, Os.InheritIn)
}
}
// 단계 목록을 앞에서부터 실행한다. stdin은 앞 단계가 정해 준다.
//
// 재귀가 필요하다. 자식을 리스트에 모아 두고 나중에 한꺼번에 거두는 쪽이
// 자연스러운데 그렇게 못 한다 — 아래 "리스트에 넣으면 못 꺼낸다" 참고.
// 그래서 "뒤를 먼저 다 띄우고, 돌아오면서 자기 자식을 거둔다"가 된다.
pub fn from(os: Os.Os, stages: List[Parse.Command], own stdin: Os.Stdin)
effects {Os.pipe, Os.spawn, Child.wait, ReadFd.close, WriteFd.close}
-> Result[Int, Os.OsError] {
match List.first(stages) {
// 여기 오면 stdin이 샌다. 언어는 말하지 않는다 (D3).
None => Ok(0),
Some(c) => {
let tail = List.rest(stages)
if List.is_empty(tail) {
last(os, c, stdin)
} else {
middle(os, c, stdin, tail)
}
},
}
}
// 마지막 단계. 출력은 부모 것을 그대로 쓴다.
pub fn last(os: Os.Os, c: Parse.Command, own stdin: Os.Stdin)
effects {Os.spawn, Child.wait} -> Result[Int, Os.OsError] {
match os.spawn(c.program, c.args, stdin, Os.InheritOut) {
Err(e) => Err(e),
Ok(child) => child.wait(),
}
}
// 가운데 단계. 파이프를 만들어 쓰는 끝을 자식에게 주고,
// 읽는 끝을 다음 단계에 넘긴다.
//
// 이 함수가 이 개밥 먹기의 본체다. 자원 셋(stdin, 읽는 끝, 쓰는 끝)이
// 서로 다른 곳으로 가고, 실패하면 셋의 상태가 제각각이다.
pub fn middle(
os: Os.Os,
c: Parse.Command,
own stdin: Os.Stdin,
rest: List[Parse.Command],
) effects {Os.pipe, Os.spawn, Child.wait, ReadFd.close, WriteFd.close}
-> Result[Int, Os.OsError] {
match os.pipe() {
// stdin이 샌다
Err(e) => Err(e),
Ok(p) => match p {
Os.Ends(r, w) => match os.spawn(c.program, c.args, stdin, Os.ToPipe(w)) {
// 여기서는 r이 샌다. w는 spawn이 가져갔는지 아닌지
// 시그니처만 봐서는 알 수 없다 — 아래 발견 참고.
Err(e) => Err(e),
// 자식이 떴다. 부모는 쓰는 끝을 이미 넘겼고, 읽는 끝을
// 다음 단계에 넘긴다. 부모에게는 아무것도 안 남는다.
Ok(child) => reap(child, from(os, rest, Os.FromPipe(r))),
},
},
}
}
// 자식을 거두고 뒤쪽의 결과를 돌려준다.
//
// 뒤쪽 결과가 앞선다 — 파이프라인의 종료 코드는 마지막 단계의 것이다.
// 앞 단계가 죽어도 셸은 마지막 것을 보고한다.
//
// 거두기 실패는 일부러 버린다. 자식이 이미 사라진 것은 우리가 할 일이
// 없고, 뒤쪽 결과가 사람이 알고 싶은 것이다.
pub fn reap(own child: Os.Child, r: Result[Int, Os.OsError])
effects {Child.wait} -> Result[Int, Os.OsError] {
let _ = child.wait()
r
}
// ------------------------------------------------------------------
// 되풀이
// ------------------------------------------------------------------
// 읽고, 읽어서 나눈 것을 돌리고, 다시 읽는다.
//
// 내장 명령은 자식으로 띄우지 않는다. cd를 자식에서 하면 자식의 디렉터리가
// 바뀌고 자식은 곧 죽는다 — 아무 일도 안 일어난 것과 같다.
pub fn once(os: Os.Os, line: String)
effects {Os.pipe, Os.spawn, Os.chdir, Child.wait,
ReadFd.close, WriteFd.close}
-> Result[Int, Os.OsError] {
match Parse.line(line) {
// 파싱 실패는 셸을 죽이지 않는다. 사람이 오타를 낸 것뿐이다.
Err(_) => Ok(2),
Ok(p) => match List.first(p.stages) {
None => Ok(0),
Some(head) => {
if Parse.is_builtin(head) && List.len(p.stages) == 1 {
builtin(os, head)
} else {
run(os, p)
}
},
},
}
}
// 내장 명령. cd만 실제로 뭔가 한다.
//
// exit은 여기서 처리할 수 없다 — 되풀이를 멈추는 일은 부르는 쪽의
// 제어 흐름이지 이 함수의 결과값이 아니다. 종료 코드로 신호를 보낸다.
pub fn builtin(os: Os.Os, c: Parse.Command)
effects {Os.chdir} -> Result[Int, Os.OsError] {
if c.program == "exit" {
Ok(0 - 1)
} else {
match List.first(c.args) {
None => Ok(0),
Some(dir) => match os.chdir(dir) {
Err(e) => Err(e),
Ok(_) => Ok(0),
},
}
}
}
+56
View File
@@ -0,0 +1,56 @@
// 데이터베이스 연결 풀 — 개밥 먹기용 초안.
//
// 세 층이 있고 수명을 관리하는 주체가 각각 다르다.
// Conn 실제 연결. 풀이 소유한다. 사용자는 존재도 모른다
// Pool 연결 묶음. 프로그램 수명. 사용자가 만들고 닫는다
// Lease 빌린 한 자리. 요청 하나 수명. 반드시 반납해야 한다
//
// 사용자가 관리하는 것은 연결의 수명이 아니라 lease의 수명이다.
// 그래서 반납은 "파괴"가 아니다 — 연결은 풀로 돌아간다.
//
// ※ 2판. `own self`가 생긴 뒤 다시 썼다. 전에는 정리가 전부 Db의 메서드라
// 반납 한 번에 풀과 권한을 같이 들고 다녀야 했다. 이제 자원이 자기
// 정리를 스스로 선언한다.
// ※ List[String]의 List는 내장 타입이라 import가 필요 없다. 모듈 List를
// 가져오는 것은 그 타입에 딸린 함수(len, map...)를 쓸 때뿐이다.
pub enum DbError {
// 연결 자체가 안 된다
Unreachable(String),
// 풀이 가득 찼고 기다릴 수 없다. 호출자가 대처할 수 있다 — 재시도든 포기든
Exhausted,
// 질의가 틀렸다. 프로그램의 결함에 가깝지만 런타임 값에서 오므로 Result다
BadQuery(String),
Other(String),
}
// 빌린 한 자리. 반드시 반납해야 한다.
// ※ 언어는 아직 그 "반드시"를 강제하지 못한다 (D3). `with`가 그 자리다.
pub capability Lease {
// 질의한다. 수신자를 빌린다 — 한 lease로 여러 번 질의할 수 있다.
fn query(sql: String) effects {Lease.query} -> Result[List[String], DbError]
// 반납한다. 연결은 풀로 돌아간다 — 파괴가 아니다.
// 수신자를 소비하므로 두 번 반납할 수 없다. 그리고 이 시그니처에는
// 풀도 Db 권한도 없다 — 반납에 필요한 것은 lease 자신뿐이다.
// 그것이 `with`가 성립하기 위한 조건이다 (D9).
fn release(own self) effects {Lease.release} -> Result[Unit, DbError]
}
// 풀. capability이므로 affine이다 — 복제되지 않는다.
pub capability Pool {
// 한 자리를 빌린다. 없으면 Exhausted — 기다리지 않는다.
// 기다리는 형태(timeout)는 취소를 정해야 해서 미룬다.
fn acquire() effects {Pool.acquire} -> Result[Lease, DbError]
// 풀을 닫는다. 안에 있는 연결이 전부 정리된다.
// 아직 빌려나간 lease가 있으면? — 아래 발견 참고.
fn close(own self) effects {Pool.close} -> Result[Unit, DbError]
}
// 데이터베이스에 손댈 권한. 풀을 만들 수 있는 유일한 출처다.
pub capability Db {
// 풀을 연다. size는 최대 동시 연결 수.
fn pool(url: String, size: Int) effects {Db.pool} -> Result[Pool, DbError]
}
+70
View File
@@ -0,0 +1,70 @@
// 파일 시스템 — 개밥 먹기용 초안.
//
// 이 파일은 구현이 아니라 계약이다. 런타임이 구현한다고 가정하고, coollang이
// 이 일을 표현할 수 있는지만 본다. 실행은 안 되지만 타입·effect·capability·
// 소유권 검사는 전부 진짜로 돈다.
//
// 규율: 여기 적는 것은 지금 런타임이 가진 권한으로 구현 가능해야 하고,
// effect를 전부 선언해야 하며, 없는 언어 기능에 기대면 안 된다. 없는 기능이
// 필요하다는 게 드러나면 그것이 발견이지 지름길이 아니다.
// ------------------------------------------------------------------
// 실패는 무엇인가
//
// 여기서 Result와 crash의 선을 긋는다. 기준은 "호출자가 대처할 수 있는가"다.
// 대처할 수 있다 → Result
// 프로그램이 틀렸다 → crash
//
// 그래서 아래는 전부 Result다. 파일이 없는 것도, 권한이 없는 것도, 디스크가
// 가득 찬 것도 프로그램의 결함이 아니다 — 세상의 상태다.
// ------------------------------------------------------------------
pub enum IoError {
NotFound(String),
Denied(String),
Exists(String),
NoSpace(String),
// 나머지. 런타임이 분류하지 못한 것들
Other(String),
}
// 열린 쓰기 핸들.
//
// capability로 선언하는 이유가 둘이다.
// 1. 이것은 실제로 권한이다 — 이 파일에 쓸 수 있는 권한
// 2. capability는 affinity의 뿌리이므로 복제되지 않는다
//
// ※ 2판. `own self`가 생긴 뒤 다시 썼다. 1판에서는 핸들에 메서드가 하나도
// 없었다 — close가 핸들을 소비해야 하는데 수신자를 소비할 방법이 없어서
// 전부 Fs의 메서드로 밀어냈다. 이제 핸들이 자기 메서드를 갖는다.
//
// 그러면서 권한의 모양이 바뀌었다. 쓰기·동기화·닫기에 더 이상 Fs가 필요
// 없다. 핸들만 넘기면 그 파일에 쓸 권한만 넘어가고 파일 시스템 전체 권한은
// 넘어가지 않는다 — 권한이 좁아진 채로 전달된다. 1판에서는 쓰게 하려면
// Fs를 통째로 줘야 했다.
pub capability WriteFile {
// 핸들을 빌린다. 여러 번 쓸 수 있다.
fn write(s: String) effects {WriteFile.write} -> Result[Unit, IoError]
// 디스크까지 내려간다. 이게 없으면 rename이 원자적이어도 내용이 없을 수 있다.
fn sync() effects {WriteFile.sync} -> Result[Unit, IoError]
// 수신자를 소비한다. 두 번 닫을 수 없다 — own self가 그것을 강제한다.
// 이 시그니처에 Fs가 없다는 것이 요점이다. 정리에 필요한 것이 자원
// 자신뿐이어야 `with`가 자원만 보고 무엇을 부를지 알 수 있다 (D9).
fn close(own self) effects {WriteFile.close} -> Result[Unit, IoError]
}
// 파일 시스템에 손댈 권한. 이름으로 하는 일이 여기 남는다 —
// 경로는 파일 시스템 전체에 대한 이름이므로 전체 권한이 필요하다.
pub capability Fs {
// 새로 만든다. 이미 있으면 자른다.
fn create(path: String) effects {Fs.create} -> Result[WriteFile, IoError]
// 같은 파일 시스템 안에서 원자적이다.
fn rename(from: String, to: String) effects {Fs.rename} -> Result[Unit, IoError]
fn remove(path: String) effects {Fs.remove} -> Result[Unit, IoError]
fn read(path: String) effects {Fs.read} -> Result[String, IoError]
}
+86
View File
@@ -0,0 +1,86 @@
// 프로세스와 파이프 — 개밥 먹기용 초안.
//
// 이 파일은 구현이 아니라 계약이다. Mini Shell을 쓰기 위해 필요한 최소한만
// 적는다. 규율은 fs.cool과 같다 — 없는 언어 기능에 기대면 그것이 발견이다.
//
// 여기서 처음 나오는 것: **자원의 수명이 렉시컬이 아니다.**
// 파이프 하나는 끝이 둘이고, 각각 다른 자식에게 간다. 부모는 넘긴 뒤
// 자기 몫을 닫아야 한다. 어느 블록에도 묶이지 않는다.
pub enum OsError {
NotFound(String),
Denied(String),
// fd가 동났다 / 프로세스가 동났다. 세상의 상태지 프로그램의 결함이 아니다
Exhausted(String),
Other(String),
}
// 열린 파일 서술자. 읽는 쪽과 쓰는 쪽을 타입으로 나눈다 —
// 읽는 끝에 쓰는 것은 프로그램의 결함이고, 결함은 타입이 막는 것이 낫다.
pub capability ReadFd {
fn close(own self) effects {ReadFd.close} -> Result[Unit, OsError]
}
pub capability WriteFd {
fn close(own self) effects {WriteFd.close} -> Result[Unit, OsError]
}
// 파이프. 만들면 끝이 둘 생긴다.
//
// struct로 담으면 필드에서 꺼낼 수 없다 (부분 move가 없다). 열거형으로
// 감싸면 패턴이 분해하므로 꺼낼 수 있다. 그래서 "둘을 함께 돌려주기"가
// 열거형으로만 된다 — LRU에서 나온 것과 같은 자리다 (D11).
pub enum Pipe {
Ends(ReadFd, WriteFd),
}
// 살아 있는 자식 프로세스. 반드시 거둬야 한다 — 안 거두면 좀비가 된다.
pub capability Child {
// 끝날 때까지 기다리고 종료 코드를 준다. 수신자를 소비한다 —
// 한 번 거둔 자식을 다시 거둘 수 없다.
fn wait(own self) effects {Child.wait} -> Result[Int, OsError]
}
// 자식에게 물려줄 표준 입출력. 무엇을 물려줄지는 세 가지뿐이다.
//
// 물려주는 fd는 소비된다 — 자식에게 넘어가고 부모는 더 못 쓴다.
// 그것이 own이 시그니처에 있는 이유다.
//
// ※ 발견: variant 이름이 모듈 전역이라 Stdin.Inherit와 Stdout.Inherit가
// 충돌한다. 그래서 InheritIn / InheritOut이 됐다 — 뜻이 아니라 언어의
// 제약이 고른 이름이다. 철학 6이 지지 않아야 할 방식으로 진 자리다.
pub enum Stdin {
// 부모 것을 그대로
InheritIn,
// 파이프의 읽는 끝
FromPipe(ReadFd),
}
pub enum Stdout {
InheritOut,
ToPipe(WriteFd),
}
// 프로세스를 만들 권한.
pub capability Os {
fn pipe() effects {Os.pipe} -> Result[Pipe, OsError]
// 프로그램을 띄운다. argv[0]은 프로그램 이름이 아니라 인자다 —
// 이름은 따로 받는다. 관례로 겹쳐 두면 둘 중 뭐가 진짜인지 매번 묻게 된다.
fn spawn(
program: String,
args: List[String],
own stdin: Stdin,
own stdout: Stdout,
) effects {Os.spawn} -> Result[Child, OsError]
// 현재 디렉터리를 바꾼다. 프로세스 전역 상태이므로 권한이 필요하다.
fn chdir(path: String) effects {Os.chdir} -> Result[Unit, OsError]
fn cwd() effects {Os.cwd} -> String
}
// 줄 단위 입력. 끝나면 None.
pub capability Stdin_ {
fn read_line() effects {Stdin_.read_line} -> Result[Option[String], OsError]
}
+56
View File
@@ -0,0 +1,56 @@
// 감독 — 개밥 먹기용 초안.
//
// 문서의 주장을 코드로 옮겨 본다: "spawn은 하나의 개념이고 실패가 어디까지
// 가느냐가 권한으로 갈린다." 그 말이 실제 시그니처가 되는지가 이 파일의 목적이다.
//
// 규율은 앞의 셋과 같다. 없는 언어 기능에 기대면 그것이 발견이다.
// 죽었다는 사실. 값이 아니라 사실만 넘어온다.
//
// 격리 경계는 recover가 아니다 — 실패한 계산이 만든 값은 경계를 넘지
// 못하고, 경계가 얻는 것은 죽었다는 것과 메시지뿐이다. 그래서 이 열거형에
// 자식의 결과를 담는 자리가 없다. 담을 자리가 있으면 그것이 recover다.
pub enum Death {
Crashed(String),
}
// 왜 멈췄는가. 감독자가 사람에게 보고할 것.
pub enum Stopped {
// 스스로 끝났다
Done,
// 죽었고 다시 안 띄운다
GaveUp(Death),
}
// 재시작 정책. 얼랭의 OTP가 라이브러리인 것과 같은 이유로 값이다 —
// 언어가 정할 일이 아니다.
pub copyable struct Policy {
// 몇 번까지 다시 띄우는가. 0이면 한 번 죽으면 끝이다.
retries: Int,
}
// 감독할 권한.
//
// TaskScope와 따로 있는 것이 요점이다. sc.spawn(f)는 실패가 부모로
// 전파되고, sup.supervise(sc, f)는 여기서 멈춘다. **실패를 삼키려면
// Supervisor를 받았어야 하고, 그것이 시그니처에 보인다.**
pub capability Supervisor {
// 죽으면 다시 띄운다.
//
// body가 무표기 fn인 것이 이 시그니처의 전부다. 무표기 fn은 여러 번
// 부를 수 있고, affine 값을 capture한 클로저는 여기 못 온다 —
// 한 번밖에 못 도는 것을 재시작할 수는 없기 때문이다.
// 얼랭이 관례로 지키는 것을 affinity가 검사한다.
fn supervise[e: effects](
sc: TaskScope,
body: fn() effects e,
policy: Policy,
) effects {Supervisor.supervise} | e -> Stopped
// 한 번만 돌린다. 죽으면 그것으로 끝이고 사실만 돌려준다.
// 재시작이 없으므로 자원을 들고 가는 클로저도 된다.
fn once[e: effects](
sc: TaskScope,
body: affine fn() effects e,
) effects {Supervisor.once} | e -> Stopped
}
+117
View File
@@ -0,0 +1,117 @@
// Process Supervisor — 죽는 일꾼을 감독한다.
//
// 앞의 넷은 자원 모델이었다. 여기는 **실패 위상**이다 — 자식이 죽었을 때
// 그 사실이 어디까지 가는가, 누가 멈출 수 있는가.
//
// 압박하려는 주장 셋:
// 1. spawn은 하나의 개념이고 실패가 어디까지 가느냐가 권한으로 갈린다
// 2. 재시작 가능한 자식은 copyable 클로저여야 한다 — affinity가 검사한다
// 3. 격리 경계는 recover가 아니다 — 죽었다는 사실만 넘어온다
import "cool.dev/std/list" as List
import "cool.dev/std/string" as String
import "cool.dev/std/int" as Int
import "../std-draft/sup" as Sup
import "../std-draft/db" as Db
// 로그를 쓸 권한. 감독자가 사람에게 보고하는 유일한 통로다 —
// 아래 "spawn은 값을 돌려주지 않는다" 참고.
pub capability Log {
fn write(s: String) effects {Log.write}
}
// ------------------------------------------------------------------
// 1. 재시작 가능한 일꾼
// ------------------------------------------------------------------
// 일감 하나를 처리한다. 자원을 **안에서** 얻고 안에서 반납한다.
//
// 이것이 재시작 가능성의 조건이다. 풀을 빌려 받을 뿐 lease를 들고 오지
// 않으므로, 이 함수를 감싼 클로저는 affine이 아니다. 죽어도 다음 번에
// 다시 획득하면 되고, 죽은 판의 lease는 감독자가 아니라 풀이 거둔다.
pub fn handle(pool: Db.Pool, log: Log, sql: String)
effects {Pool.acquire, Lease.query, Lease.release, Log.write} {
match pool.acquire() {
Err(_) => log.write("자리를 못 얻었다"),
Ok(l) => report(log, l, sql),
}
}
pub fn report(log: Log, own l: Db.Lease, sql: String)
effects {Lease.query, Lease.release, Log.write} {
let rows = l.query(sql)
let _ = l.release()
match rows {
Err(_) => log.write("질의가 실패했다"),
Ok(rs) => log.write(String.concat("행 ", Int.show(List.len(rs)))),
}
}
// 감독 아래 돌린다. body가 무표기 fn이므로 여러 번 돌 수 있다.
//
// pool과 log는 빌린 capability다. 빌린 값을 capture한 클로저는 use이고,
// use는 소유를 가져가는 자리로 못 간다 — supervise의 body는 빌림이므로 간다.
pub fn run_worker(sup: Sup.Supervisor, sc: TaskScope, pool: Db.Pool, log: Log, sql: String)
effects {Supervisor.supervise, Pool.acquire, Lease.query,
Lease.release, Log.write} -> Sup.Stopped {
sup.supervise(sc, fn() { handle(pool, log, sql) }, Sup.Policy { retries: 3 })
}
// ------------------------------------------------------------------
// 2. 재시작할 수 없는 일꾼
// ------------------------------------------------------------------
// lease를 밖에서 받아 들고 간다. 그래서 이 클로저는 affine이고,
// **supervise에 넘길 수 없다.** 한 번밖에 못 도는 것을 재시작할 수는 없다.
//
// 언어가 그 사실을 시그니처에서 막는다. 얼랭이 관례로 지키는 것을
// affinity가 검사한다 — 이 파일이 그것을 실제로 확인하는 자리다.
pub fn run_once(sup: Sup.Supervisor, sc: TaskScope, log: Log, own l: Db.Lease, sql: String)
effects {Supervisor.once, Lease.query, Lease.release, Log.write}
-> Sup.Stopped {
sup.once(sc, fn() { report(log, l, sql) })
}
// ------------------------------------------------------------------
// 3. 감독자가 보고한다
// ------------------------------------------------------------------
// 왜 멈췄는지 사람에게 알린다.
//
// Death에서 꺼낼 수 있는 것이 메시지뿐이다. 죽은 일꾼이 계산하던 값도,
// 들고 있던 자원도 여기 없다 — 격리 경계가 recover가 아니라는 것의
// 코드 수준 의미다. 담을 자리가 있으면 그것이 recover다.
pub fn announce(log: Log, s: Sup.Stopped) effects {Log.write} {
match s {
Sup.Done => log.write("끝났다"),
Sup.GaveUp(d) => match d {
Sup.Crashed(msg) => log.write(String.concat("포기: ", msg)),
},
}
}
// ------------------------------------------------------------------
// 4. 전부 엮기
// ------------------------------------------------------------------
// 일감마다 일꾼 하나. 블록을 나갈 때 전원이 join된다.
//
// ※ 발견: 여기서 각 일꾼이 어떻게 끝났는지 알 수 없다. spawn한 클로저의
// 반환값이 갈 데가 없기 때문이다. 그래서 보고를 값이 아니라 Log로 한다.
pub fn supervise_all(
sup: Sup.Supervisor,
root: TaskScope,
pool: Db.Pool,
log: Log,
jobs: List[String],
) effects {TaskScope.spawn, Supervisor.supervise, Pool.acquire,
Lease.query, Lease.release, Log.write} {
scope work = root {
List.each(jobs, fn(sql) {
work.spawn(fn() {
announce(log, run_worker(sup, work, pool, log, sql))
})
})
}
log.write("전원 종료")
}
+51 -9
View File
@@ -21,7 +21,8 @@ type ty =
} }
| T_fn of { | T_fn of {
affine : bool; affine : bool;
params : ty list; (* 파라미터마다 소유권 표시. 무표기는 빌림 *)
params : fn_param_ty list;
eff : eff_atom option; eff : eff_atom option;
ret : ty option; ret : ty option;
pos : pos; pos : pos;
@@ -29,6 +30,7 @@ type ty =
(* 제네릭 인자는 타입 또는 effect다. 맨 이름은 둘 다일 수 있으므로 (* 제네릭 인자는 타입 또는 effect다. 맨 이름은 둘 다일 수 있으므로
파서는 타입으로 읽고 이름 해소가 판정한다. *) 파서는 타입으로 읽고 이름 해소가 판정한다. *)
and fn_param_ty = { pt_own : bool; pt_ty : ty }
and targ = TA_ty of ty | TA_eff of eff_atom and targ = TA_ty of ty | TA_eff of eff_atom
type pattern = type pattern =
@@ -63,7 +65,14 @@ type expr =
| E_lit of lit * pos | E_lit of lit * pos
| E_ident of string * pos | E_ident of string * pos
| E_list of expr list * pos | E_list of expr list * pos
| E_struct of { name : string; fields : (string * expr) list; pos : pos } (* modl은 가져온 모듈의 별칭이다. 이것이 없으면 다른 모듈의 struct를
만들 수 없다 — 타입은 쓸 수 있는데 값은 못 만드는 상태가 된다. *)
| E_struct of {
modl : string option;
name : string;
fields : (string * expr) list;
pos : pos;
}
| E_closure of closure | E_closure of closure
| E_if of { cond : expr; then_ : block; else_ : expr option; pos : pos } | E_if of { cond : expr; then_ : block; else_ : expr option; pos : pos }
| E_match of { scrutinee : expr; arms : arm list; pos : pos } | E_match of { scrutinee : expr; arms : arm list; pos : pos }
@@ -74,16 +83,21 @@ type expr =
| E_inst of { callee : expr; args : targ list; pos : pos } | E_inst of { callee : expr; args : targ list; pos : pos }
| E_try of { inner : expr; pos : pos } | E_try of { inner : expr; pos : pos }
| E_unary of { op : unop; operand : expr; pos : pos } | E_unary of { op : unop; operand : expr; pos : pos }
(* 복구 불가능한 실패. 타입은 Never — 돌아오지 않으므로 어떤 자리에도 놓인다 *)
| E_crash of { msg : expr; pos : pos }
| E_binary of { op : binop; lhs : expr; rhs : expr; pos : pos } | E_binary of { op : binop; lhs : expr; rhs : expr; pos : pos }
and closure = { and closure = {
cl_params : (string * ty option) list; (* (own, 이름, 타입). 타입은 기대 타입에서 읽어오지만 소유권은 리터럴이
스스로 적는다 — move 검사가 타입을 모르기 때문이다 *)
cl_params : cl_param list;
cl_eff : eff_atom option; cl_eff : eff_atom option;
cl_ret : ty option; cl_ret : ty option;
cl_body : block; cl_body : block;
cl_pos : pos; cl_pos : pos;
} }
and cl_param = { cp_own : bool; cp_name : string; cp_ty : ty option }
and arm = { arm_pat : pattern; arm_body : expr; arm_pos : pos } and arm = { arm_pat : pattern; arm_body : expr; arm_pos : pos }
and block = { stmts : stmt list; block_pos : pos } and block = { stmts : stmt list; block_pos : pos }
@@ -112,6 +126,14 @@ type param = {
type fn_decl = { type fn_decl = {
fn_name : string; fn_name : string;
fn_gen : gen_param list; fn_gen : gen_param list;
(* 수신자를 소비하는가. capability 메서드에만 있고, 소스에는 `own self`로
적는다. 빌리는 것이 기본이므로 빌림에는 적을 말이 없다 — 뜻이 있는
쪽에만 표기가 있다.
이것이 없으면 자원이 자기 정리를 스스로 선언할 수 없고, 정리에 항상
바깥 권한(Fs, Db)이 따라붙는다. 그러면 `with`가 자원만 보고 무엇을
불러야 할지 알 수 없다 (dogfoods/FINDINGS D9). *)
fn_self : bool;
fn_params : param list; fn_params : param list;
fn_eff : eff_result option; fn_eff : eff_result option;
fn_ret : ty option; fn_ret : ty option;
@@ -148,6 +170,9 @@ type item =
pos : pos; pos : pos;
} }
| I_const of { pub : bool; name : string; ty : ty; value : expr; pos : pos } | I_const of { pub : bool; name : string; ty : ty; value : expr; pos : pos }
(* 테스트. 파라미터가 없으므로 capability를 받을 수 없고, 그래서
effect-free임이 증명된다 — 관례가 아니라 검사다 *)
| I_test of { name : string; body : block; pos : pos }
type modul = { items : item list } type modul = { items : item list }
@@ -189,7 +214,11 @@ let rec buf_ty b = function
Buffer.add_char b ')') Buffer.add_char b ')')
| T_fn { affine; params; eff; ret; _ } -> | T_fn { affine; params; eff; ret; _ } ->
Buffer.add_string b (if affine then "(affine-fn (" else "(fn ("); Buffer.add_string b (if affine then "(affine-fn (" else "(fn (");
buf_list b (buf_ty b) " " params; buf_list b
(fun (p : fn_param_ty) ->
if p.pt_own then Buffer.add_string b "own ";
buf_ty b p.pt_ty)
" " params;
Buffer.add_char b ')'; Buffer.add_char b ')';
(match eff with (match eff with
| None -> () | None -> ()
@@ -244,6 +273,10 @@ let binop_name = function
let rec buf_expr b = function let rec buf_expr b = function
| E_lit (l, _) -> buf_lit b l | E_lit (l, _) -> buf_lit b l
| E_crash { msg; _ } ->
Buffer.add_string b "(crash ";
buf_expr b msg;
Buffer.add_char b ')'
| E_ident (n, _) -> Buffer.add_string b n | E_ident (n, _) -> Buffer.add_string b n
| E_list (xs, _) -> | E_list (xs, _) ->
Buffer.add_string b "(list"; Buffer.add_string b "(list";
@@ -253,8 +286,9 @@ let rec buf_expr b = function
buf_expr b e) buf_expr b e)
xs; xs;
Buffer.add_char b ')' Buffer.add_char b ')'
| E_struct { name; fields; _ } -> | E_struct { modl; name; fields; _ } ->
Buffer.add_string b ("(struct " ^ name); Buffer.add_string b
("(struct " ^ (match modl with Some a -> a ^ "." | None -> "") ^ name);
List.iter List.iter
(fun (n, e) -> (fun (n, e) ->
Buffer.add_string b (" (" ^ n ^ " "); Buffer.add_string b (" (" ^ n ^ " ");
@@ -265,9 +299,10 @@ let rec buf_expr b = function
| E_closure c -> | E_closure c ->
Buffer.add_string b "(closure ("; Buffer.add_string b "(closure (";
buf_list b buf_list b
(fun (n, t) -> (fun (p : cl_param) ->
Buffer.add_string b n; if p.cp_own then Buffer.add_string b "own ";
match t with Buffer.add_string b p.cp_name;
match p.cp_ty with
| None -> () | None -> ()
| Some t -> | Some t ->
Buffer.add_char b ':'; Buffer.add_char b ':';
@@ -391,6 +426,9 @@ let buf_gen b gen =
let buf_fn b (d : fn_decl) = let buf_fn b (d : fn_decl) =
Buffer.add_string b ("(fn " ^ d.fn_name); Buffer.add_string b ("(fn " ^ d.fn_name);
buf_gen b d.fn_gen; buf_gen b d.fn_gen;
(* 해시에 들어간다. 수신자를 소비하는지가 바뀌면 호출하는 쪽의 move 검사
결과가 바뀐다 — 표면이다 *)
if d.fn_self then Buffer.add_string b " own-self";
Buffer.add_string b " ("; Buffer.add_string b " (";
buf_list b buf_list b
(fun p -> (fun p ->
@@ -468,6 +506,10 @@ let buf_item b = function
Buffer.add_char b ' '; Buffer.add_char b ' ';
buf_expr b value; buf_expr b value;
Buffer.add_char b ')' Buffer.add_char b ')'
| I_test { name; body; _ } ->
Buffer.add_string b ("(test \"" ^ name ^ "\" ");
buf_block b body;
Buffer.add_char b ')'
let show_item item = let show_item item =
let b = Buffer.create 256 in let b = Buffer.create 256 in
+652
View File
@@ -0,0 +1,652 @@
(* EBNF 읽기.
docs/grammar.ebnf를 데이터로 읽어들인다. 여기서부터 문법은 사람이 읽는
문서가 아니라 기계가 소비하는 소스가 된다.
이 파일이 존재하는 이유: 설명서와 파서가 따로 있으면 어긋난다. 실제로
어긋났고, 어긋난 줄 아무도 몰랐다. 문법을 읽을 수 있게 되면 파서와
기계적으로 대조할 수 있고, 나아가 파서를 여기서 뽑아낼 수 있다.
표기는 grammar.ebnf 머리에 적힌 그대로다:
= 정의 | 선택 [ ] 선택적 { } 반복
( ) 묶음 " " 단말 (* *) 주석 - 제외 *)
type expr =
| Ref of string (* 다른 프로덕션 또는 토큰 이름 *)
| RefArg of string * string (* 매개변수 프로덕션 참조: primary<ok> *)
| Term of string (* "fn" 같은 리터럴 단말 *)
| Seq of expr list
| Alt of expr list
| Opt of expr
| Rep of expr
| Except of expr * expr (* char - '"' *)
(* 매개변수 프로덕션. expr_ns를 표현하려면 필요하다 — if/match의 머리에서만
struct 리터럴이 금지되는데, 그 제약은 식 문법 전체를 타고 내려간다.
매개변수가 없으면 여덟 개 프로덕션을 통째로 복제해야 하고, 그러면 사람이
읽는 문서로서의 값이 사라진다. *)
type rule = { name : string; params : string list; body : expr; line : int }
type t = rule list
type error = { line : int; msg : string }
exception Error of error
(* ------------------------------------------------------------------ *)
(* 어휘 *)
(* ------------------------------------------------------------------ *)
type tok =
| T_ident of string
| T_str of string
| T_eq
| T_semi
| T_comma
| T_bar
| T_lbracket
| T_rbracket
| T_lbrace
| T_rbrace
| T_lparen
| T_rparen
| T_minus
| T_lt
| T_gt
| T_eof
let tokenize (src : string) : (tok * int) array =
let n = String.length src in
let out = ref [] in
let line = ref 1 in
let i = ref 0 in
let emit t = out := (t, !line) :: !out in
while !i < n do
let c = src.[!i] in
if c = '\n' then (
incr line;
incr i)
else if c = ' ' || c = '\t' || c = '\r' then incr i
else if c = '(' && !i + 1 < n && src.[!i + 1] = '*' then begin
(* 주석. 중첩을 허용한다 — 문법 파일에 설명이 길게 들어간다 *)
let depth = ref 0 in
let fin = ref false in
while (not !fin) && !i < n do
if !i + 1 < n && src.[!i] = '(' && src.[!i + 1] = '*' then (
incr depth;
i := !i + 2)
else if !i + 1 < n && src.[!i] = '*' && src.[!i + 1] = ')' then (
decr depth;
i := !i + 2;
if !depth = 0 then fin := true)
else (
if src.[!i] = '\n' then incr line;
incr i)
done;
if not !fin then raise (Error { line = !line; msg = "주석이 닫히지 않았습니다" })
end
else if c = '"' || c = '\'' then begin
let quote = c in
let start = !i + 1 in
incr i;
while !i < n && src.[!i] <> quote do
if src.[!i] = '\n' then
raise (Error { line = !line; msg = "단말이 닫히지 않았습니다" });
incr i
done;
if !i >= n then raise (Error { line = !line; msg = "단말이 닫히지 않았습니다" });
emit (T_str (String.sub src start (!i - start)));
incr i
end
else if (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || c = '_' then begin
let start = !i in
while
!i < n
&&
let d = src.[!i] in
(d >= 'a' && d <= 'z')
|| (d >= 'A' && d <= 'Z')
|| (d >= '0' && d <= '9')
|| d = '_'
do
incr i
done;
emit (T_ident (String.sub src start (!i - start)))
end
else begin
let single t =
emit t;
incr i
in
match c with
| '=' -> single T_eq
| ';' -> single T_semi
| ',' -> single T_comma
| '|' -> single T_bar
| '[' -> single T_lbracket
| ']' -> single T_rbracket
| '{' -> single T_lbrace
| '}' -> single T_rbrace
| '(' -> single T_lparen
| ')' -> single T_rparen
| '-' -> single T_minus
| '<' -> single T_lt
| '>' -> single T_gt
| _ ->
raise (Error { line = !line; msg = Printf.sprintf "알 수 없는 문자 %c" c })
end
done;
emit T_eof;
Array.of_list (List.rev !out)
(* ------------------------------------------------------------------ *)
(* 구문 *)
(* ------------------------------------------------------------------ *)
type state = { toks : (tok * int) array; mutable p : int }
let cur st = fst st.toks.(st.p)
let line st = snd st.toks.(st.p)
let adv st = if st.p < Array.length st.toks - 1 then st.p <- st.p + 1
let fail st msg = raise (Error { line = line st; msg })
let eat st t what =
if cur st = t then adv st else fail st (Printf.sprintf "%s이(가) 필요합니다" what)
(* alt := seq { "|" seq } *)
let rec parse_alt st =
let first = parse_seq st in
if cur st <> T_bar then first
else begin
let acc = ref [ first ] in
while cur st = T_bar do
adv st;
acc := parse_seq st :: !acc
done;
Alt (List.rev !acc)
end
(* seq := factor { "," factor } *)
and parse_seq st =
let first = parse_factor st in
if cur st <> T_comma then first
else begin
let acc = ref [ first ] in
while cur st = T_comma do
adv st;
acc := parse_factor st :: !acc
done;
Seq (List.rev !acc)
end
(* factor := primary { "-" primary } — 제외는 여러 번 올 수 있다 *)
and parse_factor st =
let a = ref (parse_primary st) in
while cur st = T_minus do
adv st;
a := Except (!a, parse_primary st)
done;
!a
and parse_primary st =
match cur st with
| T_ident n ->
adv st;
if cur st = T_lt then begin
adv st;
let a =
match cur st with
| T_ident a ->
adv st;
a
| _ -> fail st "매개변수 이름"
in
eat st T_gt ">";
RefArg (n, a)
end
else Ref n
| T_str s ->
adv st;
Term s
| T_lbracket ->
adv st;
let e = parse_alt st in
eat st T_rbracket "]";
Opt e
| T_lbrace ->
adv st;
let e = parse_alt st in
eat st T_rbrace "}";
Rep e
| T_lparen ->
adv st;
let e = parse_alt st in
eat st T_rparen ")";
e
| _ -> fail st "이름, 단말, 또는 묶음"
let parse (src : string) : t =
let st = { toks = tokenize src; p = 0 } in
let rules = ref [] in
while cur st <> T_eof do
let ln = line st in
let name =
match cur st with
| T_ident n ->
adv st;
n
| _ -> fail st "프로덕션 이름"
in
let params =
if cur st = T_lt then begin
adv st;
let acc = ref [] in
let rec loop () =
(match cur st with
| T_ident p ->
adv st;
acc := p :: !acc
| _ -> fail st "매개변수 이름");
if cur st = T_comma then (
adv st;
loop ())
in
loop ();
eat st T_gt ">";
List.rev !acc
end
else []
in
eat st T_eq "=";
let body = parse_alt st in
eat st T_semi ";";
rules := { name; params; body; line = ln } :: !rules
done;
List.rev !rules
let parse_result src =
match parse src with r -> Ok r | exception Error e -> Error e
(* ------------------------------------------------------------------ *)
(* 조회 *)
(* ------------------------------------------------------------------ *)
let find (g : t) name = List.find_opt (fun r -> r.name = name) g
(* 정의되지 않은 채 참조된 이름. 토큰 이름일 수도 있고 빠뜨린 프로덕션일
수도 있으므로 판정하지 않고 목록만 준다. *)
let undefined (g : t) : string list =
let defined = List.map (fun r -> r.name) g in
let seen = Hashtbl.create 32 in
let rec walk = function
| Ref n -> if not (List.mem n defined) then Hashtbl.replace seen n ()
| RefArg (n, _) ->
if not (List.mem n defined) then Hashtbl.replace seen n ()
| Term _ -> ()
| Seq xs | Alt xs -> List.iter walk xs
| Opt e | Rep e -> walk e
| Except (a, b) ->
walk a;
walk b
in
List.iter (fun r -> walk r.body) g;
Hashtbl.fold (fun k () acc -> k :: acc) seen [] |> List.sort compare
(* 어디서도 참조되지 않는 프로덕션. 시작 기호는 제외한다. *)
let unreachable (g : t) ~(start : string) : string list =
let used = Hashtbl.create 32 in
let rec walk = function
| Ref n -> Hashtbl.replace used n ()
(* 인자로 넘어간 이름도 쓰인 것이다: brace_list<field>의 field *)
| RefArg (n, a) ->
Hashtbl.replace used n ();
Hashtbl.replace used a ()
| Term _ -> ()
| Seq xs | Alt xs -> List.iter walk xs
| Opt e | Rep e -> walk e
| Except (a, b) ->
walk a;
walk b
in
List.iter (fun r -> walk r.body) g;
List.filter_map
(fun r ->
if r.name = start || Hashtbl.mem used r.name then None else Some r.name)
g
let rec show_expr = function
| Ref n -> n
| RefArg (n, a) -> n ^ "<" ^ a ^ ">"
| Term s -> "\"" ^ s ^ "\""
| Seq xs -> String.concat " , " (List.map show_expr xs)
| Alt xs -> String.concat " | " (List.map show_paren xs)
| Opt e -> "[ " ^ show_expr e ^ " ]"
| Rep e -> "{ " ^ show_expr e ^ " }"
| Except (a, b) -> show_paren a ^ " - " ^ show_paren b
and show_paren e =
match e with Alt _ | Seq _ -> "( " ^ show_expr e ^ " )" | _ -> show_expr e
let show_rule r =
let ps =
if r.params = [] then "" else "<" ^ String.concat ", " r.params ^ ">"
in
r.name ^ ps ^ " = " ^ show_expr r.body ^ " ;"
(* ------------------------------------------------------------------ *)
(* 단일화 *)
(* *)
(* 매개변수 프로덕션을 실제로 쓰인 인자별로 펼친다. primary<ok>와 *)
(* primary<no>가 각각 하나의 평범한 프로덕션이 되고, 그 뒤 분석은 매개변수를 *)
(* 몰라도 된다. 문서는 짧게 유지하고 기계는 펼친 것을 본다. *)
(* ------------------------------------------------------------------ *)
let mangle n a = n ^ "<" ^ a ^ ">"
let expand (g : t) : t =
(* 인자를 머리에 박아 특수화한 규칙이 있으면 그것을 먼저 쓴다.
ident_or_struct<yes>와 ident_or_struct<no>처럼 인자에 따라 몸통이
달라지는 자리를 위한 것이다. 없으면 일반 규칙에 인자를 대입한다. *)
let by_name_arg n a =
match List.find_opt (fun r -> r.name = n && r.params = [ a ]) g with
| Some r -> Some r
| None -> List.find_opt (fun r -> r.name = n && r.params <> []) g
in
let out = Hashtbl.create 64 in
let queue = ref [] in
(* 인자를 실제 값으로 바꾸며 몸통을 복사한다 *)
let rec subst (env : (string * string) list) e =
match e with
| Term _ -> e
(* 매개변수 이름이 그대로 참조된 자리도 인자로 바꾼다: list<item>의 item *)
| Ref n -> ( match List.assoc_opt n env with Some v -> Ref v | None -> e)
| RefArg (n, a) -> (
let a = match List.assoc_opt a env with Some v -> v | None -> a in
match by_name_arg n a with
| Some r when r.params <> [] ->
let key = (n, a) in
if (not (Hashtbl.mem out (mangle n a))) && not (List.mem key !queue)
then queue := key :: !queue;
Ref (mangle n a)
| _ -> Ref n)
| Seq xs -> Seq (List.map (subst env) xs)
| Alt xs -> Alt (List.map (subst env) xs)
| Opt x -> Opt (subst env x)
| Rep x -> Rep (subst env x)
| Except (x, y) -> Except (subst env x, subst env y)
in
(* 매개변수 없는 규칙부터 *)
List.iter
(fun r ->
if r.params = [] then
Hashtbl.replace out r.name { r with body = subst [] r.body })
g;
while !queue <> [] do
let n, a = List.hd !queue in
queue := List.tl !queue;
let key = mangle n a in
if not (Hashtbl.mem out key) then
match by_name_arg n a with
| None -> ()
| Some r ->
let env = match r.params with p :: _ -> [ (p, a) ] | [] -> [] in
Hashtbl.replace out key
{ name = key; params = []; body = subst env r.body; line = r.line }
done;
(* 원본 순서를 최대한 유지한다 — 문서와 대조하기 쉽게 *)
let ordered =
List.concat_map
(fun r ->
if r.params = [] then
match Hashtbl.find_opt out r.name with Some x -> [ x ] | None -> []
else
Hashtbl.fold
(fun k v acc ->
if
String.length k > String.length r.name
&& String.sub k 0 (String.length r.name + 1) = r.name ^ "<"
then v :: acc
else acc)
out []
|> List.sort (fun a b -> compare a.name b.name))
g
in
ordered
(* ------------------------------------------------------------------ *)
(* nullable과 FIRST *)
(* *)
(* 여기서부터가 "다음 한 토큰만 보고 결정할 수 있는가"를 기계가 판정하는 *)
(* 근거다. 문법 첫머리의 LL(1) 주장은 지금까지 사람의 말이었다. *)
(* ------------------------------------------------------------------ *)
(* 단말 하나의 이름. 리터럴은 그 철자, 토큰 부류는 그 이름. *)
module SS = Set.Make (String)
type analysis = {
rules : t;
tokens : SS.t; (* 단말로 취급할 Ref 이름 (ident, NEWLINE 등) *)
nullable : (string, bool) Hashtbl.t;
first : (string, SS.t) Hashtbl.t;
}
let is_token a n = SS.mem n a.tokens || find a.rules n = None
let rec nullable_expr a = function
| Term _ -> false
| Ref n ->
if is_token a n then false else Hashtbl.find_opt a.nullable n = Some true
| RefArg (n, x) -> nullable_expr a (Ref (mangle n x))
| Seq xs -> List.for_all (nullable_expr a) xs
| Alt xs -> List.exists (nullable_expr a) xs
| Opt _ | Rep _ -> true
| Except (x, _) -> nullable_expr a x
let rec first_expr a = function
| Term s -> SS.singleton s
| Ref n -> (
if is_token a n then SS.singleton n
else
match Hashtbl.find_opt a.first n with Some s -> s | None -> SS.empty)
| RefArg (n, x) -> first_expr a (Ref (mangle n x))
| Alt xs ->
List.fold_left (fun acc x -> SS.union acc (first_expr a x)) SS.empty xs
| Opt x | Rep x -> first_expr a x
| Except (x, _) -> first_expr a x
| Seq xs ->
let rec go acc = function
| [] -> acc
| x :: rest ->
let acc = SS.union acc (first_expr a x) in
if nullable_expr a x then go acc rest else acc
in
go SS.empty xs
(* 변화가 없을 때까지 돈다. 문법은 작으므로 단순한 고정점으로 충분하다. *)
let analyze ?(tokens = []) (g : t) : analysis =
let a =
{
rules = g;
tokens = SS.of_list tokens;
nullable = Hashtbl.create 64;
first = Hashtbl.create 64;
}
in
List.iter (fun r -> Hashtbl.replace a.nullable r.name false) g;
List.iter (fun r -> Hashtbl.replace a.first r.name SS.empty) g;
let changed = ref true in
while !changed do
changed := false;
List.iter
(fun r ->
let nu = nullable_expr a r.body in
if nu && Hashtbl.find_opt a.nullable r.name <> Some true then (
Hashtbl.replace a.nullable r.name true;
changed := true);
let f = first_expr a r.body in
let old =
match Hashtbl.find_opt a.first r.name with
| Some s -> s
| None -> SS.empty
in
if not (SS.equal f old) then (
Hashtbl.replace a.first r.name (SS.union old f);
changed := true))
g
done;
a
let first a name =
match Hashtbl.find_opt a.first name with Some s -> s | None -> SS.empty
let nullable a name = Hashtbl.find_opt a.nullable name = Some true
(* ------------------------------------------------------------------ *)
(* FOLLOW와 LL(1) 충돌 *)
(* ------------------------------------------------------------------ *)
(* 이어지는 자리를 (올 수 있는 단말들, 규칙 끝에 닿을 수 있는가)로 나른다.
끝에 닿을 수 있으면 그 규칙의 FOLLOW가 더해진다. *)
type follow_env = {
a : analysis;
fol : (string, SS.t) Hashtbl.t;
mutable deps : (string * string) list;
(* (n, owner): follow n ⊇ follow owner *)
}
let get_fol e n =
match Hashtbl.find_opt e.fol n with Some s -> s | None -> SS.empty
let rec collect e owner expr (cont : SS.t) (cont_end : bool) =
match expr with
| Term _ -> ()
| RefArg (n, x) -> collect e owner (Ref (mangle n x)) cont cont_end
| Ref n ->
if not (is_token e.a n) then begin
Hashtbl.replace e.fol n (SS.union (get_fol e n) cont);
if cont_end && not (List.mem (n, owner) e.deps) then
e.deps <- (n, owner) :: e.deps
end
| Alt xs -> List.iter (fun x -> collect e owner x cont cont_end) xs
| Opt x -> collect e owner x cont cont_end
(* 반복은 자기 자신이 뒤따를 수 있다 *)
| Rep x -> collect e owner x (SS.union cont (first_expr e.a x)) cont_end
| Except (x, _) -> collect e owner x cont cont_end
| Seq xs ->
let acc_first = ref cont and acc_end = ref cont_end in
List.iter
(fun x ->
collect e owner x !acc_first !acc_end;
let f = first_expr e.a x in
if nullable_expr e.a x then acc_first := SS.union f !acc_first
else (
acc_first := f;
acc_end := false))
(List.rev xs)
let follows ?(tokens = []) (g : t) : (string, SS.t) Hashtbl.t =
let a = analyze ~tokens g in
let e = { a; fol = Hashtbl.create 64; deps = [] } in
List.iter (fun r -> collect e r.name r.body SS.empty true) g;
(* 규칙 끝에 닿는 참조는 그 규칙의 FOLLOW를 물려받는다. 고정점. *)
let changed = ref true in
while !changed do
changed := false;
List.iter
(fun (n, owner) ->
let merged = SS.union (get_fol e n) (get_fol e owner) in
if not (SS.equal merged (get_fol e n)) then (
Hashtbl.replace e.fol n merged;
changed := true))
e.deps
done;
e.fol
type conflict = {
c_rule : string;
c_line : int;
c_kind : string; (* "선택" | "선택적" | "반복" *)
c_tokens : string list; (* 겹치는 단말 *)
c_detail : string;
(* greedy 규칙으로 해소되는가. [ X ]와 { X }가 "최대한 먹는다"로 정의되면,
겹치는 토큰이 흡수 대상뿐일 때 결정이 갈린다. 어느 쪽으로 읽든 같은
것을 뜻하는 자리에서만 쓸 수 있는 해소다 — 진짜 중의성을 덮지 않도록
greedy 토큰 목록은 문법이 명시한다. *)
c_greedy : bool;
}
(* 같은 단말로 시작하는 대안이 둘 이상이면 한 토큰으로 결정할 수 없다. *)
let conflicts ?(tokens = []) ?(greedy = []) (g : t) : conflict list =
let a = analyze ~tokens g in
let fol = follows ~tokens g in
let out = ref [] in
let add r kind toks detail =
if toks <> [] then
out :=
{
c_rule = r.name;
c_line = r.line;
c_kind = kind;
c_tokens = toks;
c_detail = detail;
(* 선택/반복만 greedy로 해소된다. 대안(Alt) 충돌은 못 덮는다 *)
c_greedy =
kind <> "선택" && List.for_all (fun t -> List.mem t greedy) toks;
}
:: !out
in
let rec walk r expr (cont : SS.t) (cont_end : bool) =
let cont_full =
if cont_end then
SS.union cont
(match Hashtbl.find_opt fol r.name with
| Some s -> s
| None -> SS.empty)
else cont
in
match expr with
| Term _ | Ref _ | RefArg _ -> ()
| Except (x, _) -> walk r x cont cont_end
| Alt xs ->
let n = List.length xs in
for i = 0 to n - 1 do
for j = i + 1 to n - 1 do
let fi = first_expr a (List.nth xs i)
and fj = first_expr a (List.nth xs j) in
let inter = SS.inter fi fj in
if not (SS.is_empty inter) then
add r "선택" (SS.elements inter)
(Printf.sprintf "%d번째와 %d번째 대안이 같은 토큰으로 시작합니다: %s / %s" (i + 1)
(j + 1)
(show_expr (List.nth xs i))
(show_expr (List.nth xs j)))
done
done;
let nulls = List.filter (nullable_expr a) xs in
if List.length nulls > 1 then
add r "선택" [ "(빈 것)" ] "비어도 되는 대안이 둘 이상입니다";
List.iter (fun x -> walk r x cont cont_end) xs
| Opt x ->
let inter = SS.inter (first_expr a x) cont_full in
if not (SS.is_empty inter) then
add r "선택적" (SS.elements inter)
(Printf.sprintf "[ %s ]를 넣을지 말지가 다음 토큰으로 갈리지 않습니다" (show_expr x));
walk r x cont cont_end
| Rep x ->
let inter = SS.inter (first_expr a x) cont_full in
if not (SS.is_empty inter) then
add r "반복" (SS.elements inter)
(Printf.sprintf "{ %s }를 더 돌지 말지가 다음 토큰으로 갈리지 않습니다" (show_expr x));
walk r x (SS.union cont (first_expr a x)) cont_end
| Seq xs ->
let acc_first = ref cont and acc_end = ref cont_end in
List.iter
(fun x ->
walk r x !acc_first !acc_end;
let f = first_expr a x in
if nullable_expr a x then acc_first := SS.union f !acc_first
else (
acc_first := f;
acc_end := false))
(List.rev xs)
in
List.iter (fun r -> walk r r.body SS.empty true) g;
List.rev !out
+154
View File
@@ -0,0 +1,154 @@
(* 문법에서 문장을 만든다.
대조는 두 방향이 있다. 저장소의 .cool 파일로 하는 대조는 "사람이 쓴 코드"
만 훑으므로, 문법이 약속했는데 파서가 못 읽는 구석은 아무도 안 밟으면
드러나지 않는다. 여기서는 문법이 허용하는 문장을 직접 만들어 파서에
먹인다 — 파서가 거부하면 둘 중 하나가 틀린 것이다.
텍스트가 아니라 토큰 열을 만든다. 렉서의 줄바꿈 삽입 규칙을 거치면
문법이 허용해도 렉서가 만들 수 없는 문장이 생기는데, 그건 파서의 잘못이
아니다. 검사하려는 것은 문법과 파서 사이지 렉서가 아니다. *)
(* 최소 유도 길이. 깊이가 차면 가장 짧게 끝나는 가지를 고른다 —
이게 없으면 재귀 문법에서 생성이 끝나지 않는다. *)
let min_len (g : Ebnf.t) (tokens : string list) : (string, int) Hashtbl.t =
let tbl = Hashtbl.create 128 in
let inf = 1_000_000 in
List.iter (fun (r : Ebnf.rule) -> Hashtbl.replace tbl r.name inf) g;
let get n =
if
List.mem n tokens
|| not (List.exists (fun (r : Ebnf.rule) -> r.name = n) g)
then 1
else match Hashtbl.find_opt tbl n with Some v -> v | None -> 1
in
let cap a b = if a >= inf || b >= inf then inf else a + b in
let rec cost = function
| Ebnf.Term _ -> 1
| Ebnf.Ref n -> get n
| Ebnf.RefArg (n, x) -> get (Ebnf.mangle n x)
| Ebnf.Seq xs -> List.fold_left (fun a x -> cap a (cost x)) 0 xs
| Ebnf.Alt xs -> List.fold_left (fun a x -> min a (cost x)) inf xs
| Ebnf.Opt _ | Ebnf.Rep _ -> 0
| Ebnf.Except (x, _) -> cost x
in
let changed = ref true in
while !changed do
changed := false;
List.iter
(fun (r : Ebnf.rule) ->
let c = cost r.body in
if c < Hashtbl.find tbl r.name then (
Hashtbl.replace tbl r.name c;
changed := true))
g
done;
tbl
type gen = {
rules : (string, Ebnf.rule) Hashtbl.t;
costs : (string, int) Hashtbl.t;
tokens : string list;
mutable out : Token.kind list;
(* 깊이로 제한한다. 총 확장 횟수로 세면 선언 머리에서 다 써 버려 정작
식과 문에는 도달하지 못한다 — 재미있는 구석이 전부 그 안에 있는데. *)
mutable depth : int;
max_depth : int;
(* 어느 프로덕션을 밟았는가. 퍼저가 무엇을 시험하는지 모르면 통과했다는
말에 값이 없다 — 안 밟은 규칙은 시험되지 않은 규칙이다. *)
visited : (string, unit) Hashtbl.t;
}
let sample_token (n : string) : Token.kind =
match n with
| "ident" -> Token.Ident "x"
| "int_lit" -> Token.Int "1"
| "string_lit" -> Token.Str "s"
| "NEWLINE" -> Token.Newline
| _ -> Token.Ident "x"
(* 단말 철자에서 토큰으로. 모든 토큰을 훑어 show_kind가 같은 것을 찾는다 —
철자 표를 따로 두면 그것도 어긋난다 *)
let token_of_term (s : string) : Token.kind option =
List.find_opt (fun k -> Token.show_kind k = s) Token.all_kinds
let emit g k = g.out <- k :: g.out
let rec cost_of g = function
| Ebnf.Term _ -> 1
| Ebnf.Ref n -> (
if List.mem n g.tokens then 1
else match Hashtbl.find_opt g.costs n with Some v -> v | None -> 1)
| Ebnf.RefArg (n, x) -> cost_of g (Ebnf.Ref (Ebnf.mangle n x))
| Ebnf.Seq xs -> List.fold_left (fun a x -> a + cost_of g x) 0 xs
| Ebnf.Alt xs -> List.fold_left (fun a x -> min a (cost_of g x)) 1_000_000 xs
| Ebnf.Opt _ | Ebnf.Rep _ -> 0
| Ebnf.Except (x, _) -> cost_of g x
let deep g = g.depth >= g.max_depth
let rec gen_expr g (e : Ebnf.expr) =
match e with
| Ebnf.Term s -> (
match token_of_term s with
| Some k -> emit g k
| None -> emit g (Token.Ident "x"))
| Ebnf.Ref n -> (
if List.mem n g.tokens then emit g (sample_token n)
else
match Hashtbl.find_opt g.rules n with
| Some r ->
Hashtbl.replace g.visited n ();
g.depth <- g.depth + 1;
gen_expr g r.Ebnf.body;
g.depth <- g.depth - 1
| None -> emit g (sample_token n))
| Ebnf.RefArg (n, x) -> gen_expr g (Ebnf.Ref (Ebnf.mangle n x))
| Ebnf.Seq xs -> List.iter (gen_expr g) xs
| Ebnf.Alt xs ->
let pick =
if deep g then begin
(* 깊이가 차면 가장 짧게 끝나는 가지. 같은 값이 여럿이면 무작위로
고른다 — 늘 첫 번째를 고르면 뒤쪽 가지가 영영 안 밟힌다 *)
let best =
List.fold_left (fun a x -> min a (cost_of g x)) 1_000_000 xs
in
let cands = List.filter (fun x -> cost_of g x = best) xs in
match cands with
| [] -> None
| _ -> Some (List.nth cands (Random.int (List.length cands)))
end
else Some (List.nth xs (Random.int (List.length xs)))
in
Option.iter (gen_expr g) pick
| Ebnf.Opt x -> if (not (deep g)) && Random.bool () then gen_expr g x
| Ebnf.Rep x ->
if not (deep g) then
(* 얕을수록 더 돌린다. 최상위 { item }이 0번이면 빈 파일이 된다 *)
let n = if g.depth <= 1 then 1 + Random.int 3 else Random.int 3 in
for _ = 1 to n do
gen_expr g x
done
| Ebnf.Except (x, _) -> gen_expr g x
(* start에서 시작하는 문장 하나. 토큰 열을 돌려준다 (Eof 포함) *)
let sentence ?(tokens = []) ?(start = "module") ?(max_depth = 14) ?visited
(g : Ebnf.t) : Token.t list =
let g' = Ebnf.expand g in
let rules = Hashtbl.create 128 in
List.iter (fun (r : Ebnf.rule) -> Hashtbl.replace rules r.name r) g';
let st =
{
rules;
costs = min_len g' tokens;
tokens;
out = [];
depth = 0;
max_depth;
visited = (match visited with Some v -> v | None -> Hashtbl.create 8);
}
in
gen_expr st (Ebnf.Ref start);
let pos = Token.{ line = 1; col = 1 } in
List.rev_map (fun k -> Token.{ kind = k; pos }) st.out |> fun xs ->
List.rev (Token.{ kind = Token.Eof; pos } :: List.rev xs)
+9 -1
View File
@@ -25,6 +25,9 @@ let is_exported = function
| I_capability { pub; _ } -> pub | I_capability { pub; _ } -> pub
| I_const { pub; _ } -> pub | I_const { pub; _ } -> pub
| I_reexport _ -> true | I_reexport _ -> true
(* 테스트는 표면이 아니다. 테스트를 고쳤다고 downstream이 재검사되면
안 된다 — 함수 본문과 같은 이유다 *)
| I_test _ -> false
| I_import _ -> false | I_import _ -> false
let strip = function let strip = function
@@ -38,6 +41,7 @@ let item_name = function
| I_capability { name; _ } -> name | I_capability { name; _ } -> name
| I_const { name; _ } -> name | I_const { name; _ } -> name
| I_reexport { name; _ } -> name | I_reexport { name; _ } -> name
| I_test { name; _ } -> name
| I_import { alias; _ } -> alias | I_import { alias; _ } -> alias
(* reexport는 이름이 아니라 해소된 정의 본문이 hash에 들어간다. (* reexport는 이름이 아니라 해소된 정의 본문이 hash에 들어간다.
@@ -104,7 +108,11 @@ let rec q_ty alias defined gen (t : ty) : ty =
T_fn T_fn
{ {
affine; affine;
params = List.map (q_ty alias defined gen) params; params =
List.map
(fun (p : fn_param_ty) ->
{ p with pt_ty = q_ty alias defined gen p.pt_ty })
params;
eff; eff;
ret = Option.map (q_ty alias defined gen) ret; ret = Option.map (q_ty alias defined gen) ret;
pos; pos;
+222 -8
View File
@@ -81,8 +81,46 @@ let bind (env : env) n v : env =
(* 런타임이 제공하는 것 *) (* 런타임이 제공하는 것 *)
(* ------------------------------------------------------------------ *) (* ------------------------------------------------------------------ *)
(* 문자열 도우미. 언어에 인덱싱 연산자가 없으므로 이 일은 런타임 몫이다. *)
let find_sub hay needle =
let n = String.length needle and h = String.length hay in
let rec go i =
if i + n > h then None
else if String.sub hay i n = needle then Some i
else go (i + 1)
in
if n = 0 then Some 0 else go 0
let split_on s sep =
if sep = "" then [ s ]
else
let n = String.length sep in
let rec go s acc =
match find_sub s sep with
| None -> List.rev (s :: acc)
| Some i ->
go
(String.sub s (i + n) (String.length s - i - n))
(String.sub s 0 i :: acc)
in
go s []
let out = Buffer.create 1024 let out = Buffer.create 1024
(* 프로그램 인자. 런타임이 들고 있다가 Args capability를 통해서만 준다 —
전역 변수로 아무 데서나 읽을 수 있으면 그것이 ambient authority다. *)
let argv : string list ref = ref []
let read_whole path =
let ic = open_in_bin path in
let n = in_channel_length ic in
let s = really_input_string ic n in
close_in ic;
s
(* IO 오류는 문자열로 돌려준다. 런타임이 사용자 정의 enum을 만들 수는 없고,
만들 수 있게 하면 런타임이 프로그램의 타입을 알아야 한다. v0의 선은
여기다 — Result[a, String]. *)
let root_capability name : value option = let root_capability name : value option =
match name with match name with
| "Console" -> | "Console" ->
@@ -96,20 +134,124 @@ let root_capability name : value option =
Buffer.add_char out '\n'; Buffer.add_char out '\n';
VUnit ); VUnit );
] )) ] ))
| "File" ->
Some
(VCap
( "File",
[
( "read",
fun args ->
match args with
| [ VStr path ] -> (
try VEnum ("Result", "Ok", [ VStr (read_whole path) ])
with Sys_error m -> VEnum ("Result", "Err", [ VStr m ]))
| _ -> VEnum ("Result", "Err", [ VStr "read: 경로가 필요합니다" ]) );
] ))
| "Args" ->
Some
(VCap
( "Args",
[ ("all", fun _ -> VList (List.map (fun s -> VStr s) !argv)) ] ))
| "TaskScope" -> | "TaskScope" ->
(* 루트 스코프. 구조적 동시성의 뿌리도 런타임이 준다 — 프로그램이 (* 루트 스코프. 구조적 동시성의 뿌리도 런타임이 준다 — 프로그램이
스스로 만들 수 있으면 부모 없는 작업이 생긴다. *) 스스로 만들 수 있으면 부모 없는 작업이 생긴다. *)
Some (VScope "root") Some (VScope "root")
| _ -> None | _ -> None
(* 런타임이 구현한 이름 전부. std/*.cool의 선언과 이 목록이 어긋나면 검사는
통과하고 실행이 죽는다 — 그 간극을 테스트가 막는다 (docs/friction.md F7).
v1에서 std를 coollang으로 구현하면 이 목록 자체가 사라진다. *)
let implemented =
[
"string.len";
"string.is_empty";
"string.concat";
"string.split";
"string.join";
"string.trim";
"string.starts_with";
"string.contains";
"int.show";
"int.abs";
"int.parse";
"bool.show";
"list.len";
"list.is_empty";
"list.first";
"list.nth";
"list.rest";
"list.push";
"list.concat";
"list.reverse";
"list.enumerate";
"list.each";
"list.map";
"list.filter";
"list.fold";
"option.is_some";
"option.map";
"option.unwrap_or";
"option.ok_or";
"result.is_ok";
"result.map";
"result.map_err";
"result.unwrap_or";
]
let builtin pos name (args : value list) : value = let builtin pos name (args : value list) : value =
match (name, args) with match (name, args) with
| "string.concat", [ VStr a; VStr b ] -> VStr (a ^ b) | "string.concat", [ VStr a; VStr b ] -> VStr (a ^ b)
| "string.len", [ VStr a ] -> VInt (String.length a) | "string.len", [ VStr a ] -> VInt (String.length a)
| "string.is_empty", [ VStr a ] -> VBool (a = "")
| "string.split", [ VStr s; VStr sep ] ->
VList (List.map (fun x -> VStr x) (split_on s sep))
| "string.trim", [ VStr s ] -> VStr (String.trim s)
| "string.starts_with", [ VStr s; VStr p ] ->
VBool
(String.length s >= String.length p
&& String.sub s 0 (String.length p) = p)
| "string.contains", [ VStr s; VStr n ] -> VBool (find_sub s n <> None)
| "int.show", [ VInt n ] -> VStr (string_of_int n) | "int.show", [ VInt n ] -> VStr (string_of_int n)
| "int.abs", [ VInt n ] -> VInt (abs n) | "int.abs", [ VInt n ] -> VInt (abs n)
| "int.parse", [ VStr s ] -> (
match int_of_string_opt (String.trim s) with
| Some n -> VEnum ("Result", "Ok", [ VInt n ])
| None -> VEnum ("Result", "Err", [ VStr (s ^ "은(는) 정수가 아닙니다") ]))
| "bool.show", [ VBool b ] -> VStr (if b then "true" else "false") | "bool.show", [ VBool b ] -> VStr (if b then "true" else "false")
| "list.len", [ VList xs ] -> VInt (List.length xs) | "list.len", [ VList xs ] -> VInt (List.length xs)
| "list.is_empty", [ VList xs ] -> VBool (xs = [])
| "list.push", [ VList xs; x ] -> VList (xs @ [ x ])
| "list.concat", [ VList xs; VList ys ] -> VList (xs @ ys)
| "list.reverse", [ VList xs ] -> VList (List.rev xs)
| "list.rest", [ VList xs ] -> (
match xs with [] -> VList [] | _ :: r -> VList r)
| "list.first", [ VList xs ] -> (
match xs with
| [] -> VEnum ("Option", "None", [])
| x :: _ -> VEnum ("Option", "Some", [ x ]))
| "list.enumerate", [ VList xs ] ->
VList
(List.mapi
(fun i x ->
VStruct ("List.Indexed", [ ("i", ref (VInt i)); ("value", ref x) ]))
xs)
| "list.nth", [ VList xs; VInt i ] -> (
match List.nth_opt xs i with
| Some x -> VEnum ("Option", "Some", [ x ])
| None -> VEnum ("Option", "None", []))
| "string.join", [ VStr sep; VList parts ] ->
VStr
(String.concat sep
(List.map (function VStr s -> s | v -> show v) parts))
| "option.is_some", [ VEnum ("Option", v, _) ] -> VBool (v = "Some")
| "option.unwrap_or", [ VEnum ("Option", "Some", [ x ]); _ ] -> x
| "option.unwrap_or", [ _; fallback ] -> fallback
| "option.ok_or", [ VEnum ("Option", "Some", [ x ]); _ ] ->
VEnum ("Result", "Ok", [ x ])
| "option.ok_or", [ _; e ] -> VEnum ("Result", "Err", [ e ])
| "result.is_ok", [ VEnum ("Result", v, _) ] -> VBool (v = "Ok")
| "result.unwrap_or", [ VEnum ("Result", "Ok", [ x ]); _ ] -> x
| "result.unwrap_or", [ _; fallback ] -> fallback
| _ -> | _ ->
fail pos (Printf.sprintf "%s은(는) 런타임이 제공하지 않습니다 (표준 라이브러리가 아직 없습니다)" name) fail pos (Printf.sprintf "%s은(는) 런타임이 제공하지 않습니다 (표준 라이브러리가 아직 없습니다)" name)
@@ -181,6 +323,8 @@ let rec eval st (env : env) (e : Ir.t) : value =
| [ f ] -> apply st pos f [] | [ f ] -> apply st pos f []
| _ -> fail pos "spawn은 함수 하나를 받습니다") | _ -> fail pos "spawn은 함수 하나를 받습니다")
| other -> fail pos (Printf.sprintf "%s에는 필드가 없습니다" (show other))) | other -> fail pos (Printf.sprintf "%s에는 필드가 없습니다" (show other)))
| Ir.I_crash (msg, pos) -> (
match eval st env msg with VStr s -> fail pos s | v -> fail pos (show v))
| Ir.I_unary (op, e, pos) -> ( | Ir.I_unary (op, e, pos) -> (
match (op, eval st env e) with match (op, eval st env e) with
| Ast.U_not, VBool b -> VBool (not b) | Ast.U_not, VBool b -> VBool (not b)
@@ -228,6 +372,38 @@ and apply st pos f args =
match args with match args with
| [ VList xs; f ] -> VList (List.map (fun x -> apply st pos f [ x ]) xs) | [ VList xs; f ] -> VList (List.map (fun x -> apply st pos f [ x ]) xs)
| _ -> fail pos "list.map은 리스트와 함수를 받습니다") | _ -> fail pos "list.map은 리스트와 함수를 받습니다")
| VBuiltin "list.filter" -> (
match args with
| [ VList xs; f ] ->
VList
(List.filter
(fun x ->
match apply st pos f [ x ] with VBool b -> b | _ -> false)
xs)
| _ -> fail pos "list.filter는 리스트와 함수를 받습니다")
| VBuiltin "option.map" -> (
match args with
| [ VEnum ("Option", "Some", [ x ]); f ] ->
VEnum ("Option", "Some", [ apply st pos f [ x ] ])
| [ o; _ ] -> o
| _ -> fail pos "option.map은 Option과 함수를 받습니다")
| VBuiltin "result.map" -> (
match args with
| [ VEnum ("Result", "Ok", [ x ]); f ] ->
VEnum ("Result", "Ok", [ apply st pos f [ x ] ])
| [ r; _ ] -> r
| _ -> fail pos "result.map은 Result와 함수를 받습니다")
| VBuiltin "result.map_err" -> (
match args with
| [ VEnum ("Result", "Err", [ e ]); f ] ->
VEnum ("Result", "Err", [ apply st pos f [ e ] ])
| [ r; _ ] -> r
| _ -> fail pos "result.map_err는 Result와 함수를 받습니다")
| VBuiltin "list.fold" -> (
match args with
| [ VList xs; init; f ] ->
List.fold_left (fun acc x -> apply st pos f [ acc; x ]) init xs
| _ -> fail pos "list.fold는 리스트, 초기값, 함수를 받습니다")
| VBuiltin n -> builtin pos n args | VBuiltin n -> builtin pos n args
| VNative f -> f args | VNative f -> f args
| other -> fail pos (Printf.sprintf "%s은(는) 부를 수 없습니다" (show other)) | other -> fail pos (Printf.sprintf "%s은(는) 부를 수 없습니다" (show other))
@@ -313,13 +489,17 @@ and eval_binary st env op a b pos =
(* main이 선언한 capability만 런타임이 넘긴다. 선언하지 않은 권한은 (* main이 선언한 capability만 런타임이 넘긴다. 선언하지 않은 권한은
프로그램 안에 존재하지 않는다. *) 프로그램 안에 존재하지 않는다. *)
let run (prog : Ir.program) (entry : string) (* 실패해도 그때까지 나온 출력을 함께 돌려준다.
(main_params : (string * string) list) : (string, Token.pos * string) result print는 실제로 일어난 effect다. 일어난 일을 안 보여주면 "어디까지 갔나"를
= 알 수 없고, 그게 실패했을 때 가장 먼저 보고 싶은 것이다. *)
let run ?(args = []) (prog : Ir.program) (entry : string)
(main_params : (string * string) list) :
string * (Token.pos * string) option =
Buffer.clear out; Buffer.clear out;
argv := args;
let st = { prog } in let st = { prog } in
match Hashtbl.find_opt prog.Ir.fns (entry ^ "#main") with match Hashtbl.find_opt prog.Ir.fns (entry ^ "#main") with
| None -> Error (Token.{ line = 0; col = 0 }, "main 함수가 없습니다") | None -> ("", Some (Token.{ line = 0; col = 0 }, "main 함수가 없습니다"))
| Some fn -> ( | Some fn -> (
let args = let args =
List.map List.map
@@ -331,12 +511,46 @@ let run (prog : Ir.program) (entry : string)
main_params main_params
in in
match List.find_opt Result.is_error args with match List.find_opt Result.is_error args with
| Some (Error m) -> Error (Token.{ line = 0; col = 0 }, m) | Some (Error m) -> ("", Some (Token.{ line = 0; col = 0 }, m))
| _ -> ( | _ -> (
let args = List.map Result.get_ok args in let args = List.map Result.get_ok args in
try try
ignore (apply st Token.{ line = 0; col = 0 } (VFn fn) args); ignore (apply st Token.{ line = 0; col = 0 } (VFn fn) args);
Ok (Buffer.contents out) (Buffer.contents out, None)
with with
| Fail (pos, msg) -> Error (pos, msg) | Fail (pos, msg) -> (Buffer.contents out, Some (pos, msg))
| Return_exc _ -> Ok (Buffer.contents out))) | Return_exc _ -> (Buffer.contents out, None)))
(* ------------------------------------------------------------------ *)
(* 테스트 러너 *)
(* *)
(* 격리는 런타임의 일이지 언어의 일이 아니다. 언어에는 recover가 없고, *)
(* 러너는 죽은 것을 되살리는 것이 아니라 죽었다는 사실을 관찰하고 다음 *)
(* 으로 간다 — 프로세스 경계에 가깝다. *)
(* *)
(* 테스트가 effect-free임은 검사기가 이미 보장한다. 그래서 순서에 *)
(* 의존하지 않고, 어떤 순서로 돌려도 같다. *)
(* ------------------------------------------------------------------ *)
type test_result = {
t_module : string;
t_name : string;
t_pos : Token.pos;
t_failure : (Token.pos * string) option;
}
let run_tests ?(filter = "") (prog : Ir.program) : test_result list =
let st = { prog } in
List.rev prog.Ir.tests
|> List.filter (fun (_, name, _, _) ->
filter = "" || find_sub name filter <> None)
|> List.map (fun (m, name, pos, body) ->
let failure =
try
ignore (eval st [ [] ] body);
None
with
| Fail (p, msg) -> Some (p, msg)
| Return_exc _ -> None
in
{ t_module = m; t_name = name; t_pos = pos; t_failure = failure })
+16 -4
View File
@@ -45,6 +45,7 @@ type t =
| I_seq of stmt list * t (* 블록: 문 나열 + 꼬리 값 *) | I_seq of stmt list * t (* 블록: 문 나열 + 꼬리 값 *)
| I_call of { callee : t; args : t list; pos : pos } | I_call of { callee : t; args : t list; pos : pos }
| I_field of { obj : t; name : string; pos : pos } | I_field of { obj : t; name : string; pos : pos }
| I_crash of t * pos
| I_unary of Ast.unop * t * pos | I_unary of Ast.unop * t * pos
| I_binary of Ast.binop * t * t * pos | I_binary of Ast.binop * t * t * pos
@@ -60,6 +61,8 @@ type fn = { fn_name : string; fn_params : string list; fn_body : t }
type program = { type program = {
fns : (string, fn) Hashtbl.t; fns : (string, fn) Hashtbl.t;
(* 테스트. (모듈 경로, 이름, 위치, 본문) *)
mutable tests : (string * string * pos * t) list;
(* variant 이름 -> (enum 이름, 인자 개수) *) (* variant 이름 -> (enum 이름, 인자 개수) *)
ctors : (string, string * int) Hashtbl.t; ctors : (string, string * int) Hashtbl.t;
caps : (string, string list) Hashtbl.t; (* capability -> 메서드 이름 *) caps : (string, string list) Hashtbl.t; (* capability -> 메서드 이름 *)
@@ -183,19 +186,21 @@ let rec lower_pat c (p : Ast.pattern) : pat =
let rec lower c (e : Ast.expr) : t = let rec lower c (e : Ast.expr) : t =
match e with match e with
| Ast.E_crash { msg; pos } -> I_crash (lower c msg, pos)
| Ast.E_lit (l, _) -> I_lit l | Ast.E_lit (l, _) -> I_lit l
| Ast.E_ident (n, pos) -> I_ref (classify c n, pos) | Ast.E_ident (n, pos) -> I_ref (classify c n, pos)
| Ast.E_list (xs, _) -> I_list (List.map (lower c) xs) | Ast.E_list (xs, _) -> I_list (List.map (lower c) xs)
| Ast.E_struct { name; fields; _ } -> | Ast.E_struct { modl; name; fields; _ } ->
I_make (name, List.map (fun (n, e) -> (n, lower c e)) fields) let key = match modl with Some a -> a ^ "." ^ name | None -> name in
I_make (key, List.map (fun (n, e) -> (n, lower c e)) fields)
| Ast.E_closure cl -> | Ast.E_closure cl ->
lpush c; lpush c;
List.iter (fun (n, _) -> lbind c n) cl.cl_params; List.iter (fun (p : Ast.cl_param) -> lbind c p.cp_name) cl.cl_params;
let body = lower_block c cl.cl_body in let body = lower_block c cl.cl_body in
lpop c; lpop c;
I_closure I_closure
{ {
c_params = List.map fst cl.cl_params; c_params = List.map (fun (p : Ast.cl_param) -> p.cp_name) cl.cl_params;
c_body = body; c_body = body;
c_pos = cl.cl_pos; c_pos = cl.cl_pos;
} }
@@ -310,6 +315,7 @@ let of_program (mods : modinfo list) : program =
let prog = let prog =
{ {
fns = Hashtbl.create 64; fns = Hashtbl.create 64;
tests = [];
ctors = Hashtbl.create 64; ctors = Hashtbl.create 64;
caps = Hashtbl.create 16; caps = Hashtbl.create 16;
consts = Hashtbl.create 16; consts = Hashtbl.create 16;
@@ -377,6 +383,12 @@ let of_program (mods : modinfo list) : program =
| Ast.I_const { name; value; _ } -> | Ast.I_const { name; value; _ } ->
c.locals <- []; c.locals <- [];
Hashtbl.replace prog.consts (key name) (lower c value) Hashtbl.replace prog.consts (key name) (lower c value)
| Ast.I_test { name; body; pos } ->
c.locals <- [];
lpush c;
let b = lower_block c body in
lpop c;
prog.tests <- (mi.m_path, name, pos, b) :: prog.tests
| _ -> ()) | _ -> ())
mi.m_ast.items) mi.m_ast.items)
mods; mods;
+60
View File
@@ -0,0 +1,60 @@
(* 문법 문서의 어휘 절을 코드에서 생성한다.
키워드 표와 "줄을 끝낼 수 있는 토큰" 목록은 지금까지 token.ml과
grammar.ebnf 양쪽에 손으로 적혀 있었다. 그런 목록은 어긋난다 —
실제로 이 프로젝트의 문법 문서 전체가 그렇게 어긋났다.
여기서 생성하고 테스트가 대조하므로, 이제 토큰을 추가하면 문서가 낡거나
빌드가 깨진다. 사람이 두 곳을 맞출 의무가 없다. *)
let display (k : Token.kind) =
match k with
| Token.Ident _ -> Some "ident"
| Token.Int _ -> Some "int_lit"
| Token.Str _ -> Some "string_lit"
| Token.Newline | Token.Eof -> None
| _ -> Some ("\"" ^ Token.show_kind k ^ "\"")
let is_keyword (k : Token.kind) = Token.keyword (Token.show_kind k) = Some k
let keywords () =
List.filter_map
(fun k ->
if is_keyword k then Some ("\"" ^ Token.show_kind k ^ "\"") else None)
Token.all_kinds
let statement_enders () =
List.filter_map
(fun k -> if Token.can_end_statement k then display k else None)
Token.all_kinds
let begin_mark = "(* 여기부터 lib/token.ml에서 생성됩니다 — 손으로 고치지 마십시오 *)"
let end_mark = "(* 생성 끝 *)"
(* 한 줄이 길어지지 않게 접는다. 구분자는 줄 끝에 남겨 이어짐이 보이게 한다 *)
let wrap ~indent items =
let rec go acc line = function
| [] -> List.rev (line :: acc)
| x :: rest ->
if line = indent then go acc (line ^ x) rest
else if String.length line + String.length x + 3 > 72 then
go ((line ^ " |") :: acc) (indent ^ x) rest
else go acc (line ^ " | " ^ x) rest
in
String.concat "\n" (go [] indent items)
let render () =
String.concat "\n"
[
begin_mark;
" * 줄을 끝낼 수 있는 토큰. 줄의 마지막 토큰이 이 중 하나이면 그 줄 끝에";
" * NEWLINE이 삽입된다. Token.can_end_statement가 원본이다.";
" *";
wrap ~indent:" * " (statement_enders ());
" *";
" * 키워드. 이름으로 쓸 수 없다. Token.keyword가 원본이다.";
" *";
wrap ~indent:" * " (keywords ());
" " ^ end_mark;
]
+246 -43
View File
@@ -13,7 +13,9 @@
open Ast open Ast
type error = { pos : Token.pos; msg : string } (* lint는 오류가 아니다. 코드는 뜻이 통하고 검사도 계속되어야 한다 —
Resolve와 같은 구분이다 *)
type error = { pos : Token.pos; msg : string; lint : bool }
(* 값이 무엇인가: 소유한 affine 값인가, 빌린 값인가. 둘은 직교한다. *) (* 값이 무엇인가: 소유한 affine 값인가, 빌린 값인가. 둘은 직교한다. *)
type vinfo = { v_affine : bool; v_use : bool } type vinfo = { v_affine : bool; v_use : bool }
@@ -27,13 +29,23 @@ type binding = {
b_use : bool; b_use : bool;
b_mut : bool; b_mut : bool;
b_depth : int; b_depth : int;
(* 선언된 타입. 필드가 affine인지 알려면 필요하다 — 모르면 필드에서
꺼내는 것을 막을 수 없고, 그러면 capability가 struct를 통해 복제된다. *)
b_ty : ty option;
} }
type fninfo = { f_params : param list; f_ret : Ast.ty option } type fninfo = { f_self : bool; f_params : param list; f_ret : Ast.ty option }
type state = { type state = {
aff : (string, bool) Hashtbl.t; aff : (string, bool) Hashtbl.t;
(* struct 이름 -> 필드 이름과 타입 *)
fields : (string, (string * ty) list) Hashtbl.t;
fns : (string, fninfo) Hashtbl.t; fns : (string, fninfo) Hashtbl.t;
(* 열거형 생성자 이름. 생성자는 인자를 소유한다 — 값이 variant 안으로
들어가기 때문이다. 이것이 없으면 Some(f)를 두 번 써서 affine 값을
복제할 수 있다 (보안 정리 ii). 리스트 리터럴은 처음부터 막혀 있었고
struct 리터럴도 그랬는데, 생성자만 호출로 취급돼 빠져 있었다. *)
ctors : (string, unit) Hashtbl.t;
meths : (string, (string * fninfo) list) Hashtbl.t; meths : (string, (string * fninfo) list) Hashtbl.t;
mutable scopes : binding list list; mutable scopes : binding list list;
moved : (int, Token.pos) Hashtbl.t; moved : (int, Token.pos) Hashtbl.t;
@@ -41,10 +53,15 @@ type state = {
mutable depth : int; mutable depth : int;
(* 클로저 프레임: (프레임 깊이, 잡아온 바깥 바인딩). 중첩 클로저를 위해 스택 *) (* 클로저 프레임: (프레임 깊이, 잡아온 바깥 바인딩). 중첩 클로저를 위해 스택 *)
mutable frames : (int * binding list ref) list; mutable frames : (int * binding list ref) list;
(* 클로저 인자를 걸을 때 기대 파라미터 타입. move 검사는 타입 검사와 별도
순회라 타입을 모르는데, 클로저 파라미터의 affinity를 알아야 빌림 여부를
판정할 수 있다. 호출 대상의 시그니처에서 읽어와 여기 잠깐 둔다. *)
mutable cl_expect : fn_param_ty list option;
mutable errors : error list; mutable errors : error list;
} }
let err st pos msg = st.errors <- { pos; msg } :: st.errors let err st pos msg = st.errors <- { pos; msg; lint = false } :: st.errors
let lint st pos msg = st.errors <- { pos; msg; lint = true } :: st.errors
(* ------------------------------------------------------------------ *) (* ------------------------------------------------------------------ *)
(* affinity 유도 *) (* affinity 유도 *)
@@ -74,7 +91,10 @@ let derive_affinity st (items : item list) =
(fun it -> (fun it ->
match it with match it with
| I_capability { name; _ } -> Hashtbl.replace st.aff name true | I_capability { name; _ } -> Hashtbl.replace st.aff name true
| I_struct { name; _ } -> Hashtbl.replace st.aff name false | I_struct { name; fields; _ } ->
Hashtbl.replace st.aff name false;
Hashtbl.replace st.fields name
(List.map (fun f -> (f.f_name, f.f_ty)) fields)
| I_enum { name; _ } -> Hashtbl.replace st.aff name false | I_enum { name; _ } -> Hashtbl.replace st.aff name false
| _ -> ()) | _ -> ())
items; items;
@@ -122,7 +142,7 @@ let derive_affinity st (items : item list) =
let push st = st.scopes <- [] :: st.scopes let push st = st.scopes <- [] :: st.scopes
let pop st = match st.scopes with _ :: r -> st.scopes <- r | [] -> () let pop st = match st.scopes with _ :: r -> st.scopes <- r | [] -> ()
let add st name ~affine ~use ~mut_ = let add st name ~affine ~use ~mut_ ?ty () =
st.next_id <- st.next_id + 1; st.next_id <- st.next_id + 1;
let b = let b =
{ {
@@ -132,6 +152,7 @@ let add st name ~affine ~use ~mut_ =
b_use = use; b_use = use;
b_mut = mut_; b_mut = mut_;
b_depth = st.depth; b_depth = st.depth;
b_ty = ty;
} }
in in
(match st.scopes with (match st.scopes with
@@ -184,9 +205,50 @@ let merge st snaps =
type ctx = Borrow | Move of string (* 어디로 옮겨가는지 — 진단에 쓴다 *) type ctx = Borrow | Move of string (* 어디로 옮겨가는지 — 진단에 쓴다 *)
(* 필드의 선언된 타입. 바인딩의 타입을 알아야 찾을 수 있다 — 모르면 None이고,
그때는 객체가 affine인지로 보수적으로 판정한다. *)
let field_ty st (obj : expr) (name : string) : ty option =
match obj with
| E_ident (n, _) -> (
match find st n with
| Some b -> (
match b.b_ty with
| Some (T_named { name = tn; _ }) -> (
match Hashtbl.find_opt st.fields tn with
| Some fs -> List.assoc_opt name fs
| None -> None)
| _ -> None)
| None -> None)
| _ -> None
(* 지금 살아 있는, 소유한 affine 바인딩. 소유했다(use가 아니다)는 것은
정리할 책임이 이 함수에 있다는 뜻이고, moved가 아니라는 것은 아직
아무 데도 넘기지 않았다는 뜻이다. *)
let live_owned st =
List.concat_map
(fun scope ->
List.filter
(fun b -> b.b_affine && (not b.b_use) && not (Hashtbl.mem st.moved b.b_id))
scope)
st.scopes
(* 조기 탈출 지점에서 새는 것을 말한다. 이름순으로 하나의 메시지에 모은다 —
자원 셋을 들고 있으면 같은 자리에 경고가 셋 뜨는 것이 아니라 하나 뜬다. *)
let live_leaks st pos what =
match List.sort compare (List.map (fun b -> b.b_name) (live_owned st)) with
| [] -> ()
| names ->
lint st pos
(Printf.sprintf "%s이(가) 여기서 빠져나가면 %s이(가) 정리되지 않습니다 (%s는 뒤의 정리를 건너뜁니다)"
what (String.concat ", " names) what)
let rec walk st (ctx : ctx) (e : expr) : vinfo = let rec walk st (ctx : ctx) (e : expr) : vinfo =
match e with match e with
| E_lit _ -> v_copy | E_lit _ -> v_copy
(* crash는 돌아오지 않지만 메시지 식은 계산된다. 메시지는 빌려 쓴다 *)
| E_crash { msg; _ } ->
ignore (walk st Borrow msg);
v_copy
| E_ident (n, pos) -> ( | E_ident (n, pos) -> (
match find st n with match find st n with
| None -> v_copy | None -> v_copy
@@ -210,11 +272,12 @@ let rec walk st (ctx : ctx) (e : expr) : vinfo =
| E_list (xs, _) -> | E_list (xs, _) ->
let infos = List.map (walk st (Move "컨테이너에 넣을 수")) xs in let infos = List.map (walk st (Move "컨테이너에 넣을 수")) xs in
{ v_affine = List.exists (fun i -> i.v_affine) infos; v_use = false } { v_affine = List.exists (fun i -> i.v_affine) infos; v_use = false }
| E_struct { name; fields; _ } -> | E_struct { modl; name; fields; _ } ->
List.iter (fun (_, e) -> ignore (walk st (Move "struct에 저장할 수") e)) fields; List.iter (fun (_, e) -> ignore (walk st (Move "struct에 저장할 수") e)) fields;
let key = match modl with Some a -> a ^ "." ^ name | None -> name in
{ {
v_affine = v_affine =
(match Hashtbl.find_opt st.aff name with (match Hashtbl.find_opt st.aff key with
| Some b -> b | Some b -> b
| None -> false); | None -> false);
v_use = false; v_use = false;
@@ -243,6 +306,25 @@ let rec walk st (ctx : ctx) (e : expr) : vinfo =
{ v_affine = t1.v_affine || t2.v_affine; v_use = t1.v_use || t2.v_use } { v_affine = t1.v_affine || t2.v_affine; v_use = t1.v_use || t2.v_use }
| E_match { scrutinee; arms; _ } -> | E_match { scrutinee; arms; _ } ->
let sinfo = walk st Borrow scrutinee in let sinfo = walk st Borrow scrutinee in
(* 소유한 affine 값을 이름을 묶는 패턴으로 분해하면 그 값 자체가
소비된다. 조각이 밖으로 나가기 때문이다 — 그러지 않으면 같은
파이프를 두 번 분해해 같은 fd를 두 번 꺼낼 수 있다 (보안 정리 ii).
빌린 값은 여기서 건드리지 않는다. 빌린 scrutinee의 조각은 use로
묶이므로 꺼내려는 순간 각자의 자리에서 걸리고, 그쪽 진단이 더
정확하다. 필드에서 분해하는 것도 마찬가지로 E_field가 잡는다. *)
(if
sinfo.v_affine && (not sinfo.v_use)
&& List.exists (fun a -> pat_binds a.arm_pat) arms
then
match scrutinee with
| E_ident (n, spos) -> (
match find st n with
| Some b when b.b_affine && not b.b_use ->
if not (Hashtbl.mem st.moved b.b_id) then
Hashtbl.replace st.moved b.b_id spos
| _ -> ())
| _ -> ());
let before = snapshot st in let before = snapshot st in
let results = let results =
List.map List.map
@@ -264,7 +346,7 @@ let rec walk st (ctx : ctx) (e : expr) : vinfo =
ignore (walk st Borrow (E_ident (parent, Token.{ line = 0; col = 0 }))); ignore (walk st Borrow (E_ident (parent, Token.{ line = 0; col = 0 })));
push st; push st;
(* 자식 TaskScope는 second-class다 — 블록 밖으로 나갈 수 없다 *) (* 자식 TaskScope는 second-class다 — 블록 밖으로 나갈 수 없다 *)
ignore (add st name ~affine:true ~use:true ~mut_:false); ignore (add st name ~affine:true ~use:true ~mut_:false ());
let r = walk_block st ctx body in let r = walk_block st ctx body in
pop st; pop st;
r r
@@ -274,24 +356,55 @@ let rec walk st (ctx : ctx) (e : expr) : vinfo =
pop st; pop st;
r r
| E_call { callee; args; pos } -> walk_call st callee args pos | E_call { callee; args; pos } -> walk_call st callee args pos
| E_field { obj; _ } -> | E_field { obj; name; pos } ->
(* v0에 부분 move는 없다. 필드 접근은 빌림이고 결과도 빌린 값이다. *) (* v0에 부분 move는 없다. 필드 접근은 빌림이고 결과도 빌린 값이다.
그러므로 Move 자리에 놓으면 오류다 — 이 검사가 없으면 capability를
struct에 넣고 필드를 두 번 읽어 복제할 수 있다 (보안 정리 ii). *)
let o = walk st Borrow obj in let o = walk st Borrow obj in
if o.v_affine || o.v_use then { v_affine = true; v_use = true } let fa =
else v_copy match field_ty st obj name with
| Some t -> ty_affine st t
| None -> o.v_affine || o.v_use
in
(match ctx with
| Borrow -> ()
| Move where ->
if fa then
err st pos
(Printf.sprintf
"%s 필드는 affine이라 %s 없습니다 (v0에는 부분 move가 없습니다 — 꺼내려면 열거형으로 \
감싸십시오)"
name where));
if fa then { v_affine = true; v_use = true } else v_copy
| E_inst { callee; _ } -> walk st Borrow callee | E_inst { callee; _ } -> walk st Borrow callee
| E_try { inner; _ } -> walk st ctx inner | E_try { inner; pos } ->
let r = walk st ctx inner in
(* ?는 여기서 함수를 빠져나갈 수 있다. 그 길에는 뒤에 오는 정리
코드가 없다. 지금 살아 있는, 소유한 affine 값은 그 길에서 샌다.
v0 정책은 "오용 금지, 누수 허용"이므로 이것은 오류가 아니라
lint다 — 검사를 막지 않고 사실만 말한다. 누수를 언어가 강제로
막을지는 `with`가 결정할 문제이고, 그 결정에는 "실제 코드에서
얼마나 자주 새는가"라는 측정이 필요하다. 이 lint가 그 측정이다. *)
live_leaks st pos "?";
r
| E_unary { operand; _ } -> walk st Borrow operand | E_unary { operand; _ } -> walk st Borrow operand
| E_binary { lhs; rhs; _ } -> | E_binary { lhs; rhs; _ } ->
ignore (walk st Borrow lhs); ignore (walk st Borrow lhs);
ignore (walk st Borrow rhs); ignore (walk st Borrow rhs);
v_copy v_copy
(* 이름을 묶는 자리가 하나라도 있는가. 있으면 분해로 값이 밖으로 나간다 *)
and pat_binds = function
| P_bind _ -> true
| P_ctor { args; _ } -> List.exists pat_binds args
| P_wild _ | P_lit _ -> false
and bind_pattern st (info : vinfo) p = and bind_pattern st (info : vinfo) p =
match p with match p with
| P_wild _ | P_lit _ -> () | P_wild _ | P_lit _ -> ()
| P_bind (n, _) -> | P_bind (n, _) ->
ignore (add st n ~affine:info.v_affine ~use:info.v_use ~mut_:false) ignore (add st n ~affine:info.v_affine ~use:info.v_use ~mut_:false ())
| P_ctor { args; _ } -> List.iter (bind_pattern st info) args | P_ctor { args; _ } -> List.iter (bind_pattern st info) args
and walk_block st ctx (b : block) : vinfo = and walk_block st ctx (b : block) : vinfo =
@@ -310,15 +423,16 @@ and walk_stmt st = function
let rec bind p = let rec bind p =
match p with match p with
| P_bind (n, _) -> | P_bind (n, _) ->
ignore (add st n ~affine:info.v_affine ~use:info.v_use ~mut_) ignore (add st n ~affine:info.v_affine ~use:info.v_use ~mut_ ())
| P_ctor { args; _ } -> List.iter bind args | P_ctor { args; _ } -> List.iter bind args
| _ -> () | _ -> ()
in in
bind pat bind pat
| S_return { value; _ } -> ( | S_return { value; pos } ->
match value with (match value with
| None -> () | None -> ()
| Some e -> ignore (walk st (Move "반환할 수") e)) | Some e -> ignore (walk st (Move "반환할 수") e));
live_leaks st pos "return"
| S_assign { place; value; _ } -> | S_assign { place; value; _ } ->
ignore (walk st (Move "대입할 수") value); ignore (walk st (Move "대입할 수") value);
ignore (walk st Borrow place) ignore (walk st Borrow place)
@@ -330,10 +444,24 @@ and walk_closure st (c : closure) : vinfo =
let acc = ref [] in let acc = ref [] in
st.frames <- (st.depth, acc) :: st.frames; st.frames <- (st.depth, acc) :: st.frames;
push st; push st;
List.iter let expect = st.cl_expect in
(fun (n, ann) -> st.cl_expect <- None;
let affine = match ann with Some t -> ty_affine st t | None -> false in List.iteri
ignore (add st n ~affine ~use:false ~mut_:false)) (fun i (p : cl_param) ->
let affine =
match p.cp_ty with
| Some t -> ty_affine st t
| None -> (
(* 표기가 없으면 호출 대상의 시그니처에서 읽어온다 *)
match Option.bind expect (fun ps -> List.nth_opt ps i) with
| Some pt -> ty_affine st pt.pt_ty
| None -> false)
in
(* 함수 파라미터와 같은 규칙 — 무표기는 빌림이다. 전에는 클로저
파라미터를 무조건 소유로 봤고, 그래서 고차 경계에서 소유권 검사가
뚫렸다 (dogfoods/FINDINGS D5). *)
let use_ = affine && not p.cp_own in
ignore (add st p.cp_name ~affine ~use:use_ ~mut_:false ?ty:p.cp_ty ()))
c.cl_params; c.cl_params;
ignore (walk_block st (Move "반환할 수") c.cl_body); ignore (walk_block st (Move "반환할 수") c.cl_body);
pop st; pop st;
@@ -358,9 +486,35 @@ and walk_closure st (c : closure) : vinfo =
captured; captured;
{ v_affine = !affine; v_use = !use_ } { v_affine = !affine; v_use = !use_ }
and is_ctor st callee =
match callee with
| E_ident (n, _) when find st n = None -> Hashtbl.mem st.ctors n
(* Alias.Variant — 가져온 모듈의 생성자 *)
| E_field { obj = E_ident (o, _); name; _ } when find st o = None ->
Hashtbl.mem st.ctors name || Hashtbl.mem st.ctors (o ^ "." ^ name)
| _ -> false
and walk_call st callee args pos = and walk_call st callee args pos =
let info = callee_info st callee in let info = callee_info st callee in
ignore (walk st Borrow callee); if info = None && is_ctor st callee then begin
(* 생성자는 인자를 소유한다. 값이 variant 안으로 들어가기 때문이다 —
리스트 리터럴, struct 리터럴과 같은 규칙이다.
affinity는 넣은 것에서 전이된다. 선언된 타입이 아니라 실제로 넣은
값을 보는 것은 E_list와 같다. *)
let infos = List.map (walk st (Move "열거형에 넣을 수")) args in
{ v_affine = List.exists (fun i -> i.v_affine) infos; v_use = false }
end
else walk_plain_call st callee args pos info
and walk_plain_call st callee args pos info =
(* 수신자를 소비하는 메서드면 수신자가 Move 자리다. 이것이 없으면
f.close()를 두 번 부를 수 있다 — 그리고 자원이 자기 정리를 스스로
선언한다는 말 자체가 성립하지 않는다. *)
(match (callee, info) with
| E_field { obj; name; _ }, Some { f_self = true; _ } ->
ignore (walk st (Move (Printf.sprintf "%s()로 소비할 수" name)) obj)
| _ -> ignore (walk st Borrow callee));
let params = match info with Some f -> f.f_params | None -> [] in let params = match info with Some f -> f.f_params | None -> [] in
List.iteri List.iteri
(fun i a -> (fun i a ->
@@ -368,6 +522,9 @@ and walk_call st callee args pos =
let own = match p with Some p -> p.p_own | None -> false in let own = match p with Some p -> p.p_own | None -> false in
let pty = Option.map (fun p -> p.p_ty) p in let pty = Option.map (fun p -> p.p_ty) p in
let ctx = if own then Move "다른 함수에 넘길 수" else Borrow in let ctx = if own then Move "다른 함수에 넘길 수" else Borrow in
(match (a, pty) with
| E_closure _, Some (T_fn { params; _ }) -> st.cl_expect <- Some params
| _ -> st.cl_expect <- None);
let got = walk st ctx a in let got = walk st ctx a in
(* callable affinity: affine 클로저를 fn 자리에 넘길 수 없다 *) (* callable affinity: affine 클로저를 fn 자리에 넘길 수 없다 *)
match pty with match pty with
@@ -385,24 +542,42 @@ and callee_info st callee =
| E_ident (n, _) when find st n = None -> Hashtbl.find_opt st.fns n | E_ident (n, _) when find st n = None -> Hashtbl.find_opt st.fns n
| E_inst { callee = E_ident (n, _); _ } when find st n = None -> | E_inst { callee = E_ident (n, _); _ } when find st n = None ->
Hashtbl.find_opt st.fns n Hashtbl.find_opt st.fns n
| E_field { obj = E_ident (o, _); name; _ } -> ( (* 모듈 별칭을 통한 호출: 가져온 함수는 "Alias.f" 키로 들어와 있다 *)
match find st o with | E_field { obj = E_ident (o, _); name; _ } when find st o = None ->
| Some _ -> ( Hashtbl.find_opt st.fns (o ^ "." ^ name)
(* 값의 메서드: 타입을 모르면 넘어간다. capability 메서드는 아래에서 *) (* 값의 메서드. 수신자가 중첩된 필드여도(w.pay.close()) 여기로 온다 —
match 예전에는 obj가 이름일 때만 봤고, 그래서 중첩 수신자는 시그니처를 못
Hashtbl.fold 찾아 조용히 빌림이 됐다. *)
(fun _ methods acc -> | E_field { obj; name; _ } -> meth_info st obj name
match acc with
| Some _ -> acc
| None -> List.assoc_opt name methods)
st.meths None
with
| Some f -> Some f
| None -> None)
(* 모듈 별칭을 통한 호출: 가져온 함수는 "Alias.f" 키로 들어와 있다 *)
| None -> Hashtbl.find_opt st.fns (o ^ "." ^ name))
| _ -> None | _ -> None
(* 수신자의 메서드 시그니처를 찾는다.
수신자의 선언된 타입을 알면 그 capability 안에서만 찾는다. 모르면 이름만
보고 아무 capability에서나 찾는다 — 이름이 겹치면 엉뚱한 시그니처를 읽고
진단이 부정확해질 수 있다. 통과가 느슨해지지는 않는다 (타입 검사가 수신자
타입을 이미 확인한다). *)
and meth_info st obj name =
let by_name () =
Hashtbl.fold
(fun _ methods acc ->
match acc with Some _ -> acc | None -> List.assoc_opt name methods)
st.meths None
in
let ty_name : ty option =
match obj with
| E_ident (n, _) -> Option.bind (find st n) (fun b -> b.b_ty)
| E_field { obj; name; _ } -> field_ty st obj name
| _ -> None
in
match ty_name with
| Some (T_named { modl; name = tn; _ }) -> (
let key = match modl with Some a -> a ^ "." ^ tn | None -> tn in
match Hashtbl.find_opt st.meths key with
| Some methods -> List.assoc_opt name methods
| None -> by_name ())
| _ -> by_name ()
(* ------------------------------------------------------------------ *) (* ------------------------------------------------------------------ *)
(* 선언 *) (* 선언 *)
(* ------------------------------------------------------------------ *) (* ------------------------------------------------------------------ *)
@@ -421,7 +596,7 @@ let check_fn st (d : fn_decl) =
let affine = ty_affine st p.p_ty in let affine = ty_affine st p.p_ty in
(* 무표기 = 빌림. own만이 소유 이전이다. *) (* 무표기 = 빌림. own만이 소유 이전이다. *)
let use_ = affine && not p.p_own in let use_ = affine && not p.p_own in
ignore (add st p.p_name ~affine ~use:use_ ~mut_:p.p_mut)) ignore (add st p.p_name ~affine ~use:use_ ~mut_:p.p_mut ~ty:p.p_ty ()))
d.fn_params; d.fn_params;
let r = walk_block st (Move "반환할 수") body in let r = walk_block st (Move "반환할 수") body in
(match d.fn_ret with (match d.fn_ret with
@@ -437,33 +612,61 @@ let check ?(imports : item list = []) (m : modul) : error list =
let st = let st =
{ {
aff = Hashtbl.create 16; aff = Hashtbl.create 16;
fields = Hashtbl.create 16;
fns = Hashtbl.create 16; fns = Hashtbl.create 16;
ctors = Hashtbl.create 16;
meths = Hashtbl.create 16; meths = Hashtbl.create 16;
scopes = []; scopes = [];
moved = Hashtbl.create 16; moved = Hashtbl.create 16;
next_id = 0; next_id = 0;
depth = 0; depth = 0;
frames = []; frames = [];
cl_expect = None;
errors = []; errors = [];
} }
in in
derive_affinity st (imports @ m.items); derive_affinity st (imports @ m.items);
(* 내장 열거형의 생성자. 이름만 필요하다 *)
List.iter
(fun n -> Hashtbl.replace st.ctors n ())
[ "Some"; "None"; "Ok"; "Err" ];
List.iter
(fun it ->
match it with
| I_enum { variants; _ } ->
List.iter (fun v -> Hashtbl.replace st.ctors v.v_name ()) variants
| _ -> ())
(imports @ m.items);
List.iter List.iter
(fun it -> (fun it ->
match it with match it with
| I_fn { decl; _ } -> | I_fn { decl; _ } ->
Hashtbl.replace st.fns decl.fn_name Hashtbl.replace st.fns decl.fn_name
{ f_params = decl.fn_params; f_ret = decl.fn_ret } { f_self = false; f_params = decl.fn_params; f_ret = decl.fn_ret }
| I_capability { name; methods; _ } -> | I_capability { name; methods; _ } ->
Hashtbl.replace st.meths name Hashtbl.replace st.meths name
(List.map (List.map
(fun d -> (fun d ->
(d.fn_name, { f_params = d.fn_params; f_ret = d.fn_ret })) ( d.fn_name,
{
f_self = d.fn_self;
f_params = d.fn_params;
f_ret = d.fn_ret;
} ))
methods) methods)
| _ -> ()) | _ -> ())
(imports @ m.items); (imports @ m.items);
List.iter List.iter
(fun it -> match it with I_fn { decl; _ } -> check_fn st decl | _ -> ()) (fun it ->
match it with
| I_fn { decl; _ } -> check_fn st decl
(* 테스트도 같은 검사를 받는다 *)
| I_test { body; _ } ->
st.scopes <- [];
push st;
ignore (walk_block st (Move "반환할 수") body);
pop st
| _ -> ())
m.items; m.items;
List.sort List.sort
(fun a b -> (fun a b ->
+97 -38
View File
@@ -145,7 +145,8 @@ and parse_fn_ty st affine p =
if kind st = Token.RParen then [] if kind st = Token.RParen then []
else else
let rec loop acc = let rec loop acc =
let t = parse_ty st in let own = accept st Token.Kw_own in
let t = { pt_own = own; pt_ty = parse_ty st } in
if accept st Token.Comma then if accept st Token.Comma then
if kind st = Token.RParen then List.rev (t :: acc) else loop (t :: acc) if kind st = Token.RParen then List.rev (t :: acc) else loop (t :: acc)
else List.rev (t :: acc) else List.rev (t :: acc)
@@ -398,6 +399,12 @@ and parse_primary st =
| Token.Kw_fn -> parse_closure st | Token.Kw_fn -> parse_closure st
| Token.Kw_if -> parse_if st | Token.Kw_if -> parse_if st
| Token.Kw_match -> parse_match st | Token.Kw_match -> parse_match st
| Token.Kw_crash ->
adv st;
expect st Token.LParen "(";
let msg = with_struct_ok st (fun () -> parse_expr st) in
expect_close st Token.RParen ")";
E_crash { msg; pos = p }
| Token.Kw_scope -> | Token.Kw_scope ->
adv st; adv st;
let name = ident st "새 scope 이름" in let name = ident st "새 scope 이름" in
@@ -405,11 +412,22 @@ and parse_primary st =
let parent = ident st "부모 scope 이름" in let parent = ident st "부모 scope 이름" in
let body = parse_block st in let body = parse_block st in
E_scope { name; parent; body; pos = p } E_scope { name; parent; body; pos = p }
(* name_or_struct<s>. struct를 만들 수 있는 문맥에서만 점을 미리 먹는다 —
Alias.Name { ... }의 "{"를 받을 자리가 여기밖에 없기 때문이다.
<no>에서는 안 먹으므로 if x.y { } 의 "{"는 언제나 블록이다. *)
| Token.Ident n -> | Token.Ident n ->
adv st; adv st;
if (not st.no_struct) && kind st = Token.LBrace then if (not st.no_struct) && kind st = Token.Dot then begin
adv st;
let m = ident st "이름" in
if kind st = Token.LBrace then
let fields = parse_struct_lit_fields st in
E_struct { modl = Some n; name = m; fields; pos = p }
else E_field { obj = E_ident (n, p); name = m; pos = p }
end
else if (not st.no_struct) && kind st = Token.LBrace then
let fields = parse_struct_lit_fields st in let fields = parse_struct_lit_fields st in
E_struct { name = n; fields; pos = p } E_struct { modl = None; name = n; fields; pos = p }
else E_ident (n, p) else E_ident (n, p)
| _ -> err_expect st "" | _ -> err_expect st ""
@@ -443,12 +461,14 @@ and parse_closure st =
if kind st = Token.RParen then [] if kind st = Token.RParen then []
else else
let rec loop acc = let rec loop acc =
let own = accept st Token.Kw_own in
let n = ident st "파라미터 이름" in let n = ident st "파라미터 이름" in
let t = if accept st Token.Colon then Some (parse_ty st) else None in let t = if accept st Token.Colon then Some (parse_ty st) else None in
let cp = { cp_own = own; cp_name = n; cp_ty = t } in
if accept st Token.Comma then if accept st Token.Comma then
if kind st = Token.RParen then List.rev ((n, t) :: acc) if kind st = Token.RParen then List.rev (cp :: acc)
else loop ((n, t) :: acc) else loop (cp :: acc)
else List.rev ((n, t) :: acc) else List.rev (cp :: acc)
in in
loop [] loop []
in in
@@ -564,16 +584,11 @@ and parse_stmt st =
S_return { value; pos = p } S_return { value; pos = p }
| _ -> | _ ->
let e = parse_expr st in let e = parse_expr st in
if accept st Token.Eq then begin if accept st Token.Eq then
let rec is_place = function (* 대입 왼쪽에 무엇이 올 수 있는지는 구문이 아니라 이름 해소가
| E_ident _ -> true 판정한다. 구문으로 가르면 ident 하나로 대입과 식이 갈리지 않아
| E_field { obj; _ } -> is_place obj 문법이 LL(1)이 아니게 된다 (grammar.ebnf의 expr_stmt). *)
| _ -> false S_assign { place = e; value = parse_expr st; pos = p }
in
if not (is_place e) then err st "대입 왼쪽에는 변수나 필드만 올 수 있습니다";
let value = parse_expr st in
S_assign { place = e; value; pos = p }
end
else S_expr e else S_expr e
(* ------------------------------------------------------------------ *) (* ------------------------------------------------------------------ *)
@@ -600,36 +615,69 @@ let parse_gen_params st =
expect_close st Token.RBracket "]"; expect_close st Token.RBracket "]";
gs gs
let parse_params st = (* params = list<param>
if kind st = Token.RParen then [] param = [ "own" ] , plain_param
plain_param = [ "mut" ] , ident , ":" , type
capability 메서드에서만 첫 자리가 다르다 (좌인수분해 — own을 먼저 먹고
그 다음을 보므로 선읽기는 여전히 한 토큰이다):
cap_params = "own" , cap_own_rest | plain_param , list_rest<param>
cap_own_rest = "self" , list_rest<param> | plain_param , list_rest<param>
일반 함수에는 `own self`를 적을 자리가 문법에 없다 — 검사기가 거부하는
것이 아니라 그 문장이 존재하지 않는다.
돌려주는 bool은 수신자를 소비하는가다. self는 fn_params에 넣지 않는다 —
호출 지점에 인자로 나타나지 않으므로 파라미터가 아니다. *)
let parse_params st ~allow_self : bool * param list =
(* [ "mut" ] ident ":" type — own을 이미 먹었을 수 있으므로 밖에서 받는다 *)
let plain_param st ~own ~p =
let mut_ = accept st Token.Kw_mut in
let name = ident st "파라미터 이름" in
expect st Token.Colon ":";
let ty = parse_ty st in
{ p_own = own; p_mut = mut_; p_name = name; p_ty = ty; p_pos = p }
in
(* list_rest<param>: 후행 콤마를 허용한다 *)
let rec rest acc =
if accept st Token.Comma then
if kind st = Token.RParen then List.rev acc
else
let p = pos st in
let own = accept st Token.Kw_own in
(* self는 첫 자리에만 온다. 뒤에 나오면 수신자가 아니다 *)
if kind st = Token.Kw_self then
err st "self는 capability 메서드의 첫 파라미터로만 올 수 있습니다";
rest (plain_param st ~own ~p :: acc)
else List.rev acc
in
if kind st = Token.RParen then (false, [])
else else
let rec loop acc = let p = pos st in
let p = pos st in if accept st Token.Kw_own then
let own = accept st Token.Kw_own in if allow_self && accept st Token.Kw_self then (true, rest [])
let mut_ = accept st Token.Kw_mut in else (
let name = ident st "파라미터 이름" in if kind st = Token.Kw_self then
expect st Token.Colon ":"; err st "self는 capability 메서드에서만 쓸 수 있습니다";
let ty = parse_ty st in (false, rest [ plain_param st ~own:true ~p ]))
let prm = else (
{ p_own = own; p_mut = mut_; p_name = name; p_ty = ty; p_pos = p } (* own 없는 self. 빌리는 것이 기본이라 적을 말이 없다 —
in 같은 뜻에 두 가지 적는 법을 만들지 않는다 (철학 5) *)
if accept st Token.Comma then if kind st = Token.Kw_self then
if kind st = Token.RParen then List.rev (prm :: acc) err st "self는 own self로만 적습니다 (수신자를 빌리는 것이 기본이라 적을 말이 없습니다)";
else loop (prm :: acc) (false, rest [ plain_param st ~own:false ~p ]))
else List.rev (prm :: acc)
in
loop []
(* fn_decl = "fn" ident [gen] "(" [params] ")" {NL} (* fn_decl = "fn" ident [gen] "(" [params] ")" {NL}
[eff_result {NL}] ["->" type {NL}] [block] [eff_result {NL}] ["->" type {NL}] [block]
{ NEWLINE } 흡수 위치는 문법에 적힌 그대로다. *) { NEWLINE } 흡수 위치는 문법에 적힌 그대로다. *)
let parse_fn_decl st ~allow_body = let parse_fn_decl st ~allow_body ~allow_self =
let p = pos st in let p = pos st in
expect st Token.Kw_fn "fn"; expect st Token.Kw_fn "fn";
let name = ident st "함수 이름" in let name = ident st "함수 이름" in
let gen = if kind st = Token.LBracket then parse_gen_params st else [] in let gen = if kind st = Token.LBracket then parse_gen_params st else [] in
expect st Token.LParen "("; expect st Token.LParen "(";
let params = parse_params st in let self_, params = parse_params st ~allow_self in
expect_close st Token.RParen ")"; expect_close st Token.RParen ")";
skip_nl st; skip_nl st;
let eff = let eff =
@@ -652,6 +700,7 @@ let parse_fn_decl st ~allow_body =
{ {
fn_name = name; fn_name = name;
fn_gen = gen; fn_gen = gen;
fn_self = self_;
fn_params = params; fn_params = params;
fn_eff = eff; fn_eff = eff;
fn_ret = ret; fn_ret = ret;
@@ -737,7 +786,7 @@ let parse_capability st ~pub =
let rec loop acc = let rec loop acc =
if kind st = Token.RBrace then List.rev acc if kind st = Token.RBrace then List.rev acc
else else
let m = parse_fn_decl st ~allow_body:false in let m = parse_fn_decl st ~allow_body:false ~allow_self:true in
skip_nl st; skip_nl st;
loop (m :: acc) loop (m :: acc)
in in
@@ -764,10 +813,20 @@ let parse_item st =
adv st; adv st;
let name = ident st "재수출할 이름" in let name = ident st "재수출할 이름" in
I_reexport { name; pos = p } I_reexport { name; pos = p }
| Token.Kw_test ->
adv st;
let name =
match kind st with
| Token.Str s ->
adv st;
s
| _ -> err_expect st "테스트 이름 (문자열)"
in
I_test { name; body = parse_block st; pos = p }
| _ -> ( | _ -> (
let pub = accept st Token.Kw_pub in let pub = accept st Token.Kw_pub in
match kind st with match kind st with
| Token.Kw_fn -> I_fn { pub; decl = parse_fn_decl st ~allow_body:true } | Token.Kw_fn -> I_fn { pub; decl = parse_fn_decl st ~allow_body:true ~allow_self:false }
| Token.Kw_copyable -> | Token.Kw_copyable ->
adv st; adv st;
parse_struct st ~pub ~copyable:true parse_struct st ~pub ~copyable:true
+113
View File
@@ -0,0 +1,113 @@
(* 문법 파일이 직접 읽는 인식기.
손으로 쓴 파서(lib/parser.ml)와 같은 토큰 열을 받아 같은 판정을 내야 한다.
갈리면 둘 중 하나가 틀린 것이고, 그 순간 테스트가 깨진다. 이것이 설명서와
구현이 어긋나지 않게 하는 기계적 장치다.
AST를 만들지 않는다. 받아들이는가만 답한다. 그래서 손 파서의 진단은
그대로 남는다 — 대조는 판정만 한다.
문법이 LL(1)임이 이미 검증되었으므로 여기서 선택은 결정적이다.
다음 토큰이 어느 대안의 FIRST에 있는지만 보면 되고 되돌아가지 않는다. *)
module SS = Set.Make (String)
type error = { pos : Token.pos; expected : string; got : string }
(* 토큰을 문법의 단말 이름으로 옮긴다. 이 대응이 문법과 렉서를 잇는 유일한
지점이다 — 여기가 틀리면 대조 전체가 무의미하다. *)
let terminal_of (k : Token.kind) =
match k with
| Token.Ident _ -> "ident"
| Token.Int _ -> "int_lit"
| Token.Str _ -> "string_lit"
| Token.Newline -> "NEWLINE"
| Token.Eof -> "<eof>"
| k -> Token.show_kind k
type state = {
toks : Token.t array;
mutable i : int;
rules : (string, Ebnf.rule) Hashtbl.t;
a : Ebnf.analysis;
tokens : SS.t;
mutable err : error option;
}
exception Fail
let cur st = st.toks.(st.i)
let term st = terminal_of (cur st).Token.kind
let fail st expected =
(* 가장 멀리 간 실패를 남긴다. 그 자리가 사람이 볼 자리다 *)
let keep =
match st.err with
| None -> true
| Some e ->
(e.pos.Token.line, e.pos.Token.col)
<= ((cur st).Token.pos.Token.line, (cur st).Token.pos.Token.col)
in
if keep then
st.err <- Some { pos = (cur st).Token.pos; expected; got = term st };
raise Fail
let advance st = if st.i < Array.length st.toks - 1 then st.i <- st.i + 1
let is_tok st n = SS.mem n st.tokens || not (Hashtbl.mem st.rules n)
(* 이 식이 지금 토큰으로 시작할 수 있는가 *)
let starts st e = SS.mem (term st) (Ebnf.first_expr st.a e)
let rec run st (e : Ebnf.expr) =
match e with
| Ebnf.Term s -> if term st = s then advance st else fail st ("\"" ^ s ^ "\"")
| Ebnf.Ref n ->
if is_tok st n then if term st = n then advance st else fail st n
else run st (Hashtbl.find st.rules n).Ebnf.body
| Ebnf.RefArg (n, x) -> run st (Ebnf.Ref (Ebnf.mangle n x))
| Ebnf.Seq xs -> List.iter (run st) xs
| Ebnf.Alt xs -> (
match List.find_opt (starts st) xs with
| Some x -> run st x
| None -> (
(* 비어도 되는 대안이 있으면 그것을 고른다 *)
match List.find_opt (Ebnf.nullable_expr st.a) xs with
| Some x -> run st x
| None ->
fail st
(String.concat " 또는 " (SS.elements (Ebnf.first_expr st.a e)))))
(* 선택과 반복은 최대한 먹는다 (문법 표기 규약의 greedy 규칙) *)
| Ebnf.Opt x -> if starts st x then run st x
| Ebnf.Rep x ->
while starts st x do
run st x
done
| Ebnf.Except (x, _) -> run st x
let check ?(tokens = []) ?(start = "module") (g : Ebnf.t) (toks : Token.t list)
: (unit, error) result =
let g = Ebnf.expand g in
let rules = Hashtbl.create 128 in
List.iter (fun (r : Ebnf.rule) -> Hashtbl.replace rules r.name r) g;
let st =
{
toks = Array.of_list toks;
i = 0;
rules;
a = Ebnf.analyze ~tokens g;
tokens = SS.of_list tokens;
err = None;
}
in
match run st (Ebnf.Ref start) with
| () ->
if term st = "<eof>" then Ok ()
else (
(match st.err with
| Some _ -> ()
| None ->
st.err <-
Some
{ pos = (cur st).Token.pos; expected = "파일 끝"; got = term st });
Error (Option.get st.err))
| exception Fail -> Error (Option.get st.err)
+31 -11
View File
@@ -104,7 +104,7 @@ let rec resolve_ty st = function
then external_ref st name pos; then external_ref st name pos;
List.iter (resolve_targ st pos) args List.iter (resolve_targ st pos) args
| T_fn { params; eff; ret; pos; _ } -> | T_fn { params; eff; ret; pos; _ } ->
List.iter (resolve_ty st) params; List.iter (fun (p : fn_param_ty) -> resolve_ty st p.pt_ty) params;
(match eff with None -> () | Some a -> resolve_eff_atom st (a, pos)); (match eff with None -> () | Some a -> resolve_eff_atom st (a, pos));
Option.iter (resolve_ty st) ret Option.iter (resolve_ty st) ret
@@ -154,6 +154,7 @@ let rec resolve_pattern st seen = function
let rec resolve_expr st = function let rec resolve_expr st = function
| E_lit _ -> () | E_lit _ -> ()
| E_crash { msg; _ } -> resolve_expr st msg
| E_ident (n, pos) -> | E_ident (n, pos) ->
if lookup_local st n = None then if lookup_local st n = None then
if Hashtbl.mem st.items n then () if Hashtbl.mem st.items n then ()
@@ -163,15 +164,15 @@ let rec resolve_expr st = function
(* 타입 이름은 그 타입에 딸린 함수의 이름공간이다: String.len *) (* 타입 이름은 그 타입에 딸린 함수의 이름공간이다: String.len *)
else external_ref st n pos else external_ref st n pos
| E_list (xs, _) -> List.iter (resolve_expr st) xs | E_list (xs, _) -> List.iter (resolve_expr st) xs
| E_struct { name; fields; pos } -> | E_struct { modl; name; fields; pos } ->
resolve_ty st (T_named { modl = None; name; args = []; pos }); resolve_ty st (T_named { modl; name; args = []; pos });
List.iter (fun (_, e) -> resolve_expr st e) fields List.iter (fun (_, e) -> resolve_expr st e) fields
| E_closure c -> | E_closure c ->
push st; push st;
List.iter List.iter
(fun (n, t) -> (fun (p : cl_param) ->
Option.iter (resolve_ty st) t; Option.iter (resolve_ty st) p.cp_ty;
bind st c.cl_pos n false) bind st c.cl_pos p.cp_name false)
c.cl_params; c.cl_params;
(match c.cl_eff with (match c.cl_eff with
| None -> () | None -> ()
@@ -211,10 +212,24 @@ let rec resolve_expr st = function
| E_call { callee; args; _ } -> | E_call { callee; args; _ } ->
resolve_expr st callee; resolve_expr st callee;
List.iter (resolve_expr st) args List.iter (resolve_expr st) args
| E_field { obj; _ } -> | E_field { obj; pos; _ } ->
(* Alias.f — 별칭 접근도 사용이다 *) (* Alias.f — 별칭 접근도 사용이다.
왼쪽이 지역 값도, 가져온 모듈도, 이 모듈의 선언도 아니면 그 이름은
그냥 없는 것이다. 바깥 참조로 넘기면 안 된다 — 넘기는 순간 오른쪽이
전부 불투명해지고 그 아래 검사가 통째로 공허해진다 (D1과 같은 병).
내장 타입 이름도 모듈이 아니다. 타입으로 쓰는 데는 import가 필요
없지만 그 타입에 딸린 함수를 쓰려면 가져와야 한다 — prelude가
없다는 규칙에 예외를 두지 않는다. *)
(match obj with (match obj with
| E_ident (o, _) when lookup_local st o = None -> use_alias st o | E_ident (o, _) when lookup_local st o = None ->
if Hashtbl.mem st.items o then use_alias st o
else if List.mem o builtin_types then
error st pos
(Printf.sprintf "%s은(는) 내장 타입이지 모듈이 아닙니다 (딸린 함수를 쓰려면 가져오십시오)" o)
else if not (Hashtbl.mem st.ctors o || List.mem o builtin_values) then
error st pos (Printf.sprintf "%s은(는) 가져온 모듈도 지역 값도 아닙니다" o)
| _ -> ()); | _ -> ());
resolve_expr st obj resolve_expr st obj
| E_inst { callee; args; pos } -> | E_inst { callee; args; pos } ->
@@ -261,7 +276,7 @@ and resolve_stmt st = function
| _ -> None | _ -> None
in in
match root place with match root place with
| None -> () | None -> error st pos "대입 왼쪽에는 변수나 필드만 올 수 있습니다"
| Some n -> ( | Some n -> (
match lookup_local st n with match lookup_local st n with
| Some true -> () | Some true -> ()
@@ -350,7 +365,7 @@ let resolve (m : modul) : info * error list =
variants variants
| I_capability { name; pos; _ } -> declare st pos name K_type | I_capability { name; pos; _ } -> declare st pos name K_type
| I_const { name; pos; _ } -> declare st pos name K_const | I_const { name; pos; _ } -> declare st pos name K_const
| I_reexport _ -> ()) | I_test _ | I_reexport _ -> ())
m.items; m.items;
(* 2차: 본문을 훑는다. *) (* 2차: 본문을 훑는다. *)
List.iter List.iter
@@ -378,6 +393,11 @@ let resolve (m : modul) : info * error list =
resolve_ty st ty; resolve_ty st ty;
push st; push st;
resolve_expr st value; resolve_expr st value;
pop st
(* 테스트는 파라미터 없는 본문이다. 나머지는 함수와 같다 *)
| I_test { body; _ } ->
push st;
resolve_block st body;
pop st) pop st)
m.items; m.items;
(* 미사용 import는 오류다. 취향 문제가 아니라 invalidation 표면 문제다 — (* 미사용 import는 오류다. 취향 문제가 아니라 invalidation 표면 문제다 —
+33 -9
View File
@@ -65,9 +65,18 @@ let create ?(root = ".") ?std () =
패키지 참조다 (cool.dev/std/list). v0에는 패키지 해소가 없으므로 그런 패키지 참조다 (cool.dev/std/list). v0에는 패키지 해소가 없으므로 그런
import는 불투명하게 남는다 — 없다고 말하지 않는다. *) import는 불투명하게 남는다 — 없다고 말하지 않는다. *)
let is_package path = let is_package path =
match String.index_opt path '/' with (* ./ 와 ../ 로 시작하면 상대 경로다. 첫 세그먼트에 점이 있다는 것만 보면
| Some i -> String.contains (String.sub path 0 i) '.' ".."이 패키지로 오인된다 — 그러면 import가 조용히 해소되지 않고, 그
| None -> String.contains path '.' 모듈의 이름이 전부 불투명해져 검사가 통째로 공허해진다. *)
let starts p =
String.length path >= String.length p
&& String.sub path 0 (String.length p) = p
in
if starts "./" || starts "../" then false
else
match String.index_opt path '/' with
| Some i -> String.contains (String.sub path 0 i) '.'
| None -> String.contains path '.'
let std_prefix = "cool.dev/std/" let std_prefix = "cool.dev/std/"
@@ -258,14 +267,15 @@ let main_params (m : Ast.modul) =
| _ -> []) | _ -> [])
m.items m.items
let run st path : (string, error) result = (* 출력과 실패를 함께 돌려준다. 실패해도 그때까지 나온 것은 보여줘야 한다 *)
let run ?(args = []) st path : string * error option =
load st path; load st path;
let errs = errors st in let errs = errors st in
if errs <> [] then Error (List.hd errs) if errs <> [] then ("", Some (List.hd errs))
else else
match find st path with match find st path with
| None -> | None ->
Error { file = path; line = 0; col = 0; message = "모듈을 찾을 수 없습니다" } ("", Some { file = path; line = 0; col = 0; message = "모듈을 찾을 수 없습니다" })
| Some e -> ( | Some e -> (
(* IR은 그래프 전체를 받는다. 별칭이 실행 의미에 남지 않도록. *) (* IR은 그래프 전체를 받는다. 별칭이 실행 의미에 남지 않도록. *)
let mods = let mods =
@@ -275,6 +285,20 @@ let run st path : (string, error) result =
st.modules [] st.modules []
in in
let prog = Ir.of_program mods in let prog = Ir.of_program mods in
match Interp.run prog path (main_params e.ast) with match Interp.run ~args prog path (main_params e.ast) with
| Ok out -> Ok out | out, None -> (out, None)
| Error (pos, msg) -> Error (err_of path pos msg)) | out, Some (pos, msg) -> (out, Some (err_of path pos msg)))
(* 모듈 그래프를 로드하고 그 안의 테스트를 전부 돌린다 *)
let test ?(filter = "") st path : (Interp.test_result list, error list) result =
load st path;
let errs = errors st in
if errs <> [] then Error errs
else
let mods =
Hashtbl.fold
(fun p (d : entry) acc ->
{ Ir.m_path = p; m_ast = d.ast; m_deps = d.imports } :: acc)
st.modules []
in
Ok (Interp.run_tests ~filter (Ir.of_program mods))
+81
View File
@@ -20,6 +20,7 @@ type kind =
| Kw_let | Kw_let
| Kw_mut | Kw_mut
| Kw_own | Kw_own
| Kw_self
| Kw_affine | Kw_affine
| Kw_copyable | Kw_copyable
| Kw_effects | Kw_effects
@@ -28,6 +29,8 @@ type kind =
| Kw_else | Kw_else
| Kw_match | Kw_match
| Kw_scope | Kw_scope
| Kw_crash
| Kw_test
| Kw_true | Kw_true
| Kw_false | Kw_false
(* 구두점 *) (* 구두점 *)
@@ -79,6 +82,7 @@ let keyword = function
| "let" -> Some Kw_let | "let" -> Some Kw_let
| "mut" -> Some Kw_mut | "mut" -> Some Kw_mut
| "own" -> Some Kw_own | "own" -> Some Kw_own
| "self" -> Some Kw_self
| "affine" -> Some Kw_affine | "affine" -> Some Kw_affine
| "copyable" -> Some Kw_copyable | "copyable" -> Some Kw_copyable
| "effects" -> Some Kw_effects | "effects" -> Some Kw_effects
@@ -87,6 +91,8 @@ let keyword = function
| "else" -> Some Kw_else | "else" -> Some Kw_else
| "match" -> Some Kw_match | "match" -> Some Kw_match
| "scope" -> Some Kw_scope | "scope" -> Some Kw_scope
| "crash" -> Some Kw_crash
| "test" -> Some Kw_test
| "true" -> Some Kw_true | "true" -> Some Kw_true
| "false" -> Some Kw_false | "false" -> Some Kw_false
| _ -> None | _ -> None
@@ -107,6 +113,7 @@ let show_kind = function
| Kw_let -> "let" | Kw_let -> "let"
| Kw_mut -> "mut" | Kw_mut -> "mut"
| Kw_own -> "own" | Kw_own -> "own"
| Kw_self -> "self"
| Kw_affine -> "affine" | Kw_affine -> "affine"
| Kw_copyable -> "copyable" | Kw_copyable -> "copyable"
| Kw_effects -> "effects" | Kw_effects -> "effects"
@@ -115,6 +122,8 @@ let show_kind = function
| Kw_else -> "else" | Kw_else -> "else"
| Kw_match -> "match" | Kw_match -> "match"
| Kw_scope -> "scope" | Kw_scope -> "scope"
| Kw_crash -> "crash"
| Kw_test -> "test"
| Kw_true -> "true" | Kw_true -> "true"
| Kw_false -> "false" | Kw_false -> "false"
| LParen -> "(" | LParen -> "("
@@ -152,6 +161,78 @@ let show_kind = function
let show { kind; pos } = let show { kind; pos } =
Printf.sprintf "%d:%d %s" pos.line pos.col (show_kind kind) Printf.sprintf "%d:%d %s" pos.line pos.col (show_kind kind)
(* 모든 토큰 종류를 한 번씩 잇는 사슬. 아래 match가 전체 나열이므로 토큰을
추가하면 컴파일러가 여기를 지적한다 — 목록이 조용히 낡지 않는다.
문법 문서의 어휘 절이 이 나열에서 생성된다. *)
let next_kind = function
| Ident _ -> Some (Int "")
| Int _ -> Some (Str "")
| Str _ -> Some Kw_pub
| Kw_pub -> Some Kw_fn
| Kw_fn -> Some Kw_struct
| Kw_struct -> Some Kw_enum
| Kw_enum -> Some Kw_capability
| Kw_capability -> Some Kw_const
| Kw_const -> Some Kw_import
| Kw_import -> Some Kw_as
| Kw_as -> Some Kw_reexport
| Kw_reexport -> Some Kw_let
| Kw_let -> Some Kw_mut
| Kw_mut -> Some Kw_own
| Kw_own -> Some Kw_self
| Kw_self -> Some Kw_affine
| Kw_affine -> Some Kw_copyable
| Kw_copyable -> Some Kw_effects
| Kw_effects -> Some Kw_return
| Kw_return -> Some Kw_if
| Kw_if -> Some Kw_else
| Kw_else -> Some Kw_match
| Kw_match -> Some Kw_scope
| Kw_scope -> Some Kw_crash
| Kw_crash -> Some Kw_test
| Kw_test -> Some Kw_true
| Kw_true -> Some Kw_false
| Kw_false -> Some LParen
| LParen -> Some RParen
| RParen -> Some LBrace
| LBrace -> Some RBrace
| RBrace -> Some LBracket
| LBracket -> Some RBracket
| RBracket -> Some Comma
| Comma -> Some Colon
| Colon -> Some Dot
| Dot -> Some Arrow
| Arrow -> Some FatArrow
| FatArrow -> Some Question
| Question -> Some Underscore
| Underscore -> Some Eq
| Eq -> Some EqEq
| EqEq -> Some Bang
| Bang -> Some BangEq
| BangEq -> Some Lt
| Lt -> Some Le
| Le -> Some Gt
| Gt -> Some Ge
| Ge -> Some Plus
| Plus -> Some Minus
| Minus -> Some Star
| Star -> Some Slash
| Slash -> Some Percent
| Percent -> Some AmpAmp
| AmpAmp -> Some PipePipe
| PipePipe -> Some Pipe
| Pipe -> Some Newline
| Newline -> Some Eof
| Eof -> None
let all_kinds =
let rec go k acc =
match next_kind k with
| None -> List.rev (k :: acc)
| Some n -> go n (k :: acc)
in
go (Ident "") []
(* 줄 끝에서 문 구분자를 삽입할지 결정한다 (grammar.ebnf 어휘 절). (* 줄 끝에서 문 구분자를 삽입할지 결정한다 (grammar.ebnf 어휘 절).
값으로 끝날 수 있는 토큰 뒤에서만 삽입하므로, 연산자나 여는 괄호로 값으로 끝날 수 있는 토큰 뒤에서만 삽입하므로, 연산자나 여는 괄호로
끝나는 줄은 다음 줄로 이어진다. *) 끝나는 줄은 다음 줄로 이어진다. *)
+189 -27
View File
@@ -14,7 +14,8 @@ type error = { pos : Token.pos; msg : string }
type scheme = { type scheme = {
s_gen : string list; (* 타입 파라미터 *) s_gen : string list; (* 타입 파라미터 *)
s_eff_gen : string list; (* effect 파라미터 *) s_eff_gen : string list; (* effect 파라미터 *)
s_params : T.t list; (* (own, 타입). 무표기는 빌림 *)
s_params : (bool * T.t) list;
s_eff : T.eff; (* 이 함수를 부르면 수행되는 effect *) s_eff : T.eff; (* 이 함수를 부르면 수행되는 effect *)
s_ret : T.t; s_ret : T.t;
} }
@@ -43,6 +44,26 @@ type env = {
let err env pos msg = env.errors <- { pos; msg } :: env.errors let err env pos msg = env.errors <- { pos; msg } :: env.errors
let expr_pos (e : expr) : Token.pos =
match e with
| E_lit (_, p)
| E_ident (_, p)
| E_list (_, p)
| E_struct { pos = p; _ }
| E_if { pos = p; _ }
| E_match { pos = p; _ }
| E_scope { pos = p; _ }
| E_call { pos = p; _ }
| E_field { pos = p; _ }
| E_inst { pos = p; _ }
| E_try { pos = p; _ }
| E_unary { pos = p; _ }
| E_binary { pos = p; _ }
| E_crash { pos = p; _ } ->
p
| E_closure c -> c.cl_pos
| E_block b -> b.block_pos
let mismatch env pos expected got what = let mismatch env pos expected got what =
err env pos err env pos
(Printf.sprintf "%s: %s이(가) 필요한데 %s입니다" what (T.show expected) (T.show got)) (Printf.sprintf "%s: %s이(가) 필요한데 %s입니다" what (T.show expected) (T.show got))
@@ -102,7 +123,10 @@ let rec conv env (gen : string list) (t : Ast.ty) : T.t =
T.TFn T.TFn
{ {
affine; affine;
params = List.map (conv env gen) params; params =
List.map
(fun (p : fn_param_ty) -> (p.pt_own, conv env gen p.pt_ty))
params;
eff = (match eff with None -> [] | Some a -> conv_eff_atom a); eff = (match eff with None -> [] | Some a -> conv_eff_atom a);
ret = (match ret with None -> T.TUnit | Some r -> conv env gen r); ret = (match ret with None -> T.TUnit | Some r -> conv env gen r);
} }
@@ -121,7 +145,7 @@ let scheme_of env (d : fn_decl) : scheme =
{ {
s_gen = gen; s_gen = gen;
s_eff_gen = egen; s_eff_gen = egen;
s_params = List.map (fun p -> conv env gen p.p_ty) d.fn_params; s_params = List.map (fun p -> (p.p_own, conv env gen p.p_ty)) d.fn_params;
s_eff = s_eff =
(match d.fn_eff with None -> [] | Some atoms -> conv_eff_result atoms); (match d.fn_eff with None -> [] | Some atoms -> conv_eff_result atoms);
s_ret = (match d.fn_ret with None -> T.TUnit | Some r -> conv env gen r); s_ret = (match d.fn_ret with None -> T.TUnit | Some r -> conv env gen r);
@@ -131,14 +155,14 @@ let scheme_of env (d : fn_decl) : scheme =
let instantiate (s : scheme) = let instantiate (s : scheme) =
let sub = List.map (fun v -> (v, T.fresh ())) s.s_gen in let sub = List.map (fun v -> (v, T.fresh ())) s.s_gen in
let esub = List.map (fun v -> (v, T.fresh_eff ())) s.s_eff_gen in let esub = List.map (fun v -> (v, T.fresh_eff ())) s.s_eff_gen in
( List.map (T.subst sub esub) s.s_params, ( List.map (fun (o, t) -> (o, T.subst sub esub t)) s.s_params,
T.subst_eff esub s.s_eff, T.subst_eff esub s.s_eff,
T.subst sub esub s.s_ret ) T.subst sub esub s.s_ret )
let instantiate_with (s : scheme) (args : T.t list) = let instantiate_with (s : scheme) (args : T.t list) =
let sub = List.map2 (fun v a -> (v, a)) s.s_gen args in let sub = List.map2 (fun v a -> (v, a)) s.s_gen args in
let esub = List.map (fun v -> (v, T.fresh_eff ())) s.s_eff_gen in let esub = List.map (fun v -> (v, T.fresh_eff ())) s.s_eff_gen in
( List.map (T.subst sub esub) s.s_params, ( List.map (fun (o, t) -> (o, T.subst sub esub t)) s.s_params,
T.subst_eff esub s.s_eff, T.subst_eff esub s.s_eff,
T.subst sub esub s.s_ret ) T.subst sub esub s.s_ret )
@@ -164,10 +188,16 @@ let builtin_ctor = function
let rec infer env (e : expr) : T.t = let rec infer env (e : expr) : T.t =
match e with match e with
(* crash는 돌아오지 않는다. 타입은 Never이고 어떤 자리에도 놓인다 *)
| E_crash { msg; pos } ->
let t = infer env msg in
if not (T.unify t T.TString) then
mismatch env pos T.TString t "crash의 메시지";
T.TNever
| E_lit (L_int _, _) -> T.TInt | E_lit (L_int _, _) -> T.TInt
| E_lit (L_str _, _) -> T.TString | E_lit (L_str _, _) -> T.TString
| E_lit (L_bool _, _) -> T.TBool | E_lit (L_bool _, _) -> T.TBool
| E_ident (n, _) -> ( | E_ident (n, pos) -> (
match lookup env n with match lookup env n with
| Some t -> t | Some t -> t
| None -> ( | None -> (
@@ -185,9 +215,21 @@ let rec infer env (e : expr) : T.t =
| None -> ( | None -> (
match Hashtbl.find_opt env.ctors n with match Hashtbl.find_opt env.ctors n with
| Some enum -> nullary_ctor env enum n | Some enum -> nullary_ctor env enum n
| None -> | None -> (
env.saw_unknown <- true; (* 가져온 열거형의 생성자를 한정 없이 썼는가.
T.TUnknown))))) 모른다고 넘기면 그 자리가 통째로 불투명해지고,
move 검사가 생성자인 줄 몰라 인자를 빌려 버린다 —
즉 affine 값이 복제된다 (보안 정리 ii). *)
match imported_ctor env n with
| Some full ->
err env pos
(Printf.sprintf
"%s은(는) 가져온 열거형의 생성자입니다 — %s로 적으십시오"
n full);
T.TUnknown
| None ->
env.saw_unknown <- true;
T.TUnknown))))))
| E_list (xs, pos) -> | E_list (xs, pos) ->
let elem = T.fresh () in let elem = T.fresh () in
List.iter List.iter
@@ -197,7 +239,9 @@ let rec infer env (e : expr) : T.t =
mismatch env pos elem t "리스트 원소의 타입이 서로 다릅니다") mismatch env pos elem t "리스트 원소의 타입이 서로 다릅니다")
xs; xs;
T.TCon ("List", [ elem ]) T.TCon ("List", [ elem ])
| E_struct { name; fields; pos } -> infer_struct env name fields pos | E_struct { modl; name; fields; pos } ->
let key = match modl with Some a -> a ^ "." ^ name | None -> name in
infer_struct env key fields pos
| E_closure c -> infer_closure env c None | E_closure c -> infer_closure env c None
| E_if { cond; then_; else_; pos } -> ( | E_if { cond; then_; else_; pos } -> (
let c = infer env cond in let c = infer env cond in
@@ -358,20 +402,31 @@ and infer_closure env (c : closure) (expected : T.t option) =
match Option.map T.resolve expected with match Option.map T.resolve expected with
| Some (T.TFn { params; ret; _ }) | Some (T.TFn { params; ret; _ })
when List.length params = List.length c.cl_params -> when List.length params = List.length c.cl_params ->
(List.map Option.some params, Some ret) (List.map (fun (o, t) -> Some (o, t)) params, Some ret)
| _ -> (List.map (fun _ -> None) c.cl_params, None) | _ -> (List.map (fun _ -> None) c.cl_params, None)
in in
push env; push env;
let param_tys = let param_tys =
List.map2 List.map2
(fun (n, ann) exp -> (fun (p : cl_param) exp ->
let t = let t =
match ann with match p.cp_ty with
| Some a -> conv env [] a | Some a -> conv env [] a
| None -> ( match exp with Some t -> t | None -> T.TUnknown) | None -> ( match exp with Some (_, t) -> t | None -> T.TUnknown)
in in
bind env n t; (* 기대 타입이 소유를 말하는데 리터럴이 안 적었으면 오류다.
t) 반대도 오류다 — 빌리는 자리에 소유를 주장하면 빌린 값을 소비한다.
방향을 다루려면 부분 타입이 필요하고 우리에겐 없다. *)
(match exp with
| Some (o, _) when o <> p.cp_own ->
err env c.cl_pos
(Printf.sprintf "클로저 파라미터 %s의 소유권이 기대와 다릅니다 (기대: %s, 적힌 것: %s)"
p.cp_name
(if o then "own" else "빌림")
(if p.cp_own then "own" else "빌림"))
| _ -> ());
bind env p.cp_name t;
(p.cp_own, t))
c.cl_params expected_params c.cl_params expected_params
in in
let declared_ret = Option.map (conv env []) c.cl_ret in let declared_ret = Option.map (conv env []) c.cl_ret in
@@ -420,13 +475,30 @@ and infer_call env callee args pos =
| None -> ( | None -> (
match builtin_ctor n with match builtin_ctor n with
| Some (params, ret) -> | Some (params, ret) ->
Some (T.TFn { affine = false; params; eff = []; ret }) Some
(T.TFn
{
affine = false;
params = List.map (fun t -> (false, t)) params;
eff = [];
ret;
})
| None -> ( | None -> (
match Hashtbl.find_opt env.fns n with match Hashtbl.find_opt env.fns n with
| Some s -> | Some s ->
let params, eff, ret = instantiate s in let params, eff, ret = instantiate s in
Some (T.TFn { affine = false; params; eff; ret }) Some (T.TFn { affine = false; params; eff; ret })
| None -> None))) | None -> (
(* 가져온 열거형의 생성자를 한정 없이 불렀는가.
모른다고 넘기면 move 검사가 생성자인 줄 몰라 인자를
빌리고, affine 값이 복제된다 (보안 정리 ii). *)
match imported_ctor env n with
| Some full ->
err env pos
(Printf.sprintf
"%s은(는) 가져온 열거형의 생성자입니다 — %s로 적으십시오" n full);
None
| None -> None))))
| _ -> ( | _ -> (
match T.resolve (infer env callee) with match T.resolve (infer env callee) with
| T.TFn _ as t -> Some t | T.TFn _ as t -> Some t
@@ -448,7 +520,7 @@ and infer_call env callee args pos =
List.iter (fun a -> ignore (infer env a)) args) List.iter (fun a -> ignore (infer env a)) args)
else else
List.iter2 List.iter2
(fun p a -> (fun (_, p) a ->
let got = let got =
match a with match a with
| E_closure c -> infer_closure env c (Some p) | E_closure c -> infer_closure env c (Some p)
@@ -481,7 +553,7 @@ and ctor_fn env enum name =
let sub = List.map (fun v -> (v, T.fresh ())) gen in let sub = List.map (fun v -> (v, T.fresh ())) gen in
let params = let params =
match List.assoc_opt name variants with match List.assoc_opt name variants with
| Some ts -> List.map (T.subst sub []) ts | Some ts -> List.map (fun t -> (false, T.subst sub [] t)) ts
| None -> [] | None -> []
in in
T.TFn T.TFn
@@ -598,16 +670,63 @@ and check_pattern env (scrutinee : T.t) (p : pattern) =
| L_bool _ -> T.TBool | L_bool _ -> T.TBool
in in
if not (T.unify scrutinee t) then mismatch env pos scrutinee t "패턴의 리터럴" if not (T.unify scrutinee t) then mismatch env pos scrutinee t "패턴의 리터럴"
| P_bind (n, _) -> ( | P_bind (n, pos) -> (
match Hashtbl.find_opt env.ctors n with match Hashtbl.find_opt env.ctors n with
| Some enum -> | Some enum ->
check_ctor env scrutinee enum n [] Token.{ line = 0; col = 0 } check_ctor env scrutinee enum n [] Token.{ line = 0; col = 0 }
| None -> bind env n scrutinee) | None -> (
match qualified_variant env scrutinee n with
| Some full -> variant_hint env pos n full
| None -> bind env n scrutinee))
| P_ctor { modl; name; args; pos } -> ( | P_ctor { modl; name; args; pos } -> (
let name = match modl with Some a -> a ^ "." ^ name | None -> name in let full = match modl with Some a -> a ^ "." ^ name | None -> name in
match Hashtbl.find_opt env.ctors name with match Hashtbl.find_opt env.ctors full with
| Some enum -> check_ctor env scrutinee enum name args pos | Some enum -> check_ctor env scrutinee enum full args pos
| None -> List.iter (check_pattern env T.TUnknown) args) | None -> (
match (modl, qualified_variant env scrutinee name) with
| None, Some q -> variant_hint env pos name q
| _ -> List.iter (check_pattern env T.TUnknown) args))
(* 한정되지 않은 이름이 match 대상 열거형의 variant를 가리키는가.
가져온 열거형만 본다 (variant 이름에 점이 있다) — 이 모듈의 열거형이면
env.ctors에서 이미 찾았을 것이기 때문이다.
이 진단이 없으면 인자 없는 variant를 한정 없이 적었을 때 그것이 바인딩이
되고, 그 팔이 전부를 잡아 **다음 팔이 도달 불가**라는 엉뚱한 자리에
오류가 난다. 틀린 줄을 가리키는 진단은 없느니만 못하다. *)
and qualified_variant env scrutinee n =
match T.resolve scrutinee with
| T.TCon (enum, _) -> (
match Hashtbl.find_opt env.enums enum with
| Some (_, variants) ->
List.find_opt
(fun (v, _) ->
match String.rindex_opt v '.' with
| Some i -> String.sub v (i + 1) (String.length v - i - 1) = n
| None -> false)
variants
|> Option.map fst
| None -> None)
| _ -> None
(* env.ctors에 "Alias.Name" 꼴로 들어 있는 생성자 중 이름이 n인 것.
이 모듈의 생성자면 위에서 이미 찾았으므로 여기 오지 않는다. *)
and imported_ctor env n =
Hashtbl.fold
(fun k _ acc ->
match acc with
| Some _ -> acc
| None -> (
match String.rindex_opt k '.' with
| Some i when String.sub k (i + 1) (String.length k - i - 1) = n ->
Some k
| _ -> None))
env.ctors None
and variant_hint env pos n full =
err env pos
(Printf.sprintf "%s은(는) 가져온 열거형의 variant입니다 — %s로 적으십시오 (한정 없는 이름은 바인딩입니다)"
n full)
and check_ctor env scrutinee enum name args pos = and check_ctor env scrutinee enum name args pos =
match Hashtbl.find_opt env.enums enum with match Hashtbl.find_opt env.enums enum with
@@ -640,6 +759,17 @@ and infer_block env (b : block) : T.t =
go b.stmts go b.stmts
and check_stmt env = function and check_stmt env = function
(* 꼬리가 아닌 자리의 식은 값을 남기면 안 된다. 남긴 값은 버려지는데,
그 값이 Result면 실패가 조용히 사라진다 — 철학 1과 정면으로 부딪힌다.
일부러 버리려면 let _ = 로 적는다. 버린다는 사실이 코드에 보여야 한다. *)
| S_expr e -> (
let t = infer env e in
match T.resolve t with
| T.TUnit | T.TUnknown | T.TNever -> ()
| other ->
err env (expr_pos e)
(Printf.sprintf "이 식이 남기는 %s이(가) 버려집니다 (일부러 버리려면 let _ = 로 적으십시오)"
(T.show other)))
| S_let { pat; ty; value; pos; _ } -> | S_let { pat; ty; value; pos; _ } ->
let declared = Option.map (conv env []) ty in let declared = Option.map (conv env []) ty in
let got = let got =
@@ -662,7 +792,6 @@ and check_stmt env = function
let p = infer env place in let p = infer env place in
let v = infer env value in let v = infer env value in
if not (T.unify p v) then mismatch env pos p v "대입" if not (T.unify p v) then mismatch env pos p v "대입"
| S_expr e -> ignore (infer env e)
(* ------------------------------------------------------------------ *) (* ------------------------------------------------------------------ *)
(* 모듈 *) (* 모듈 *)
@@ -776,6 +905,18 @@ let check ?(imports : item list = []) (m : modul) : error list =
errors = []; errors = [];
} }
in in
(* 내장 열거형. 이것이 없으면 Option/Result의 패턴이 생성자로 인식되지
않고 변수 바인딩이 되어, match 안에서 타입 정보가 통째로 사라진다.
사용자 정의가 덮을 수 있도록 먼저 넣는다. *)
Hashtbl.replace env.enums "Option"
([ "a" ], [ ("Some", [ T.TVar "a" ]); ("None", []) ]);
Hashtbl.replace env.enums "Result"
([ "a"; "e" ], [ ("Ok", [ T.TVar "a" ]); ("Err", [ T.TVar "e" ]) ]);
List.iter
(fun (v, e) -> Hashtbl.replace env.ctors v e)
[
("Some", "Option"); ("None", "Option"); ("Ok", "Result"); ("Err", "Result");
];
(* 1차: 타입과 생성자 이름부터 (선언 순서에 의존하지 않는다) *) (* 1차: 타입과 생성자 이름부터 (선언 순서에 의존하지 않는다) *)
List.iter List.iter
(fun it -> (fun it ->
@@ -823,6 +964,27 @@ let check ?(imports : item list = []) (m : modul) : error list =
let want = conv env [] ty in let want = conv env [] ty in
let got = infer env value in let got = infer env value in
if not (T.unify want got) then mismatch env pos want got "상수의 값" if not (T.unify want got) then mismatch env pos want got "상수의 값"
(* 테스트는 파라미터 없고 effect 없는 함수와 같다. 검사도 같다 —
일반 코드와 다른 규칙을 주면 테스트만 통과하는 코드가 생긴다 *)
| I_test { name; body; pos } ->
env.locals <- [];
push env;
env.ret <- T.TUnit;
env.performed <- [];
env.saw_unknown <- false;
let got = infer_block env body in
if not (T.unify T.TUnit got) then
mismatch env pos T.TUnit got (Printf.sprintf "테스트 \"%s\"의 본문" name);
List.iter
(fun (a, apos) ->
err env apos
(Printf.sprintf
"테스트는 effect를 수행할 수 없습니다 (%s) — 테스트는 capability를 받지 않습니다"
(T.atom_show a)))
(List.rev (T.eff_resolve (List.map fst env.performed))
|> List.map (fun a -> (a, pos)));
env.performed <- [];
pop env
| _ -> ()) | _ -> ())
m.items; m.items;
List.sort List.sort
+21 -5
View File
@@ -10,13 +10,19 @@
type t = type t =
| TUnknown | TUnknown
(* 값을 내지 않는 타입. crash의 타입이고 어떤 자리에도 놓일 수 있다.
TUnknown과 다르다 — TUnknown은 "모른다"이고 TNever는 "돌아오지
않는다"이다. 둘 다 무엇과도 맞지만 생기는 이유가 다르다. *)
| TNever
| TInt | TInt
| TBool | TBool
| TString | TString
| TUnit | TUnit
| TVar of string | TVar of string
| TCon of string * t list | TCon of string * t list
| TFn of { affine : bool; params : t list; eff : eff; ret : t } (* params의 bool은 소유권이다 (own이면 true). affinity와 다른 축이다 —
affinity는 타입의 성질이고 소유권은 이 자리가 값을 가져가는가다. *)
| TFn of { affine : bool; params : (bool * t) list; eff : eff; ret : t }
| TMeta of meta ref | TMeta of meta ref
and meta = Unbound of int | Bound of t and meta = Unbound of int | Bound of t
@@ -97,6 +103,7 @@ let eff_missing ~declared ~performed =
let rec show t = let rec show t =
match resolve t with match resolve t with
| TUnknown -> "?" | TUnknown -> "?"
| TNever -> "Never"
| TInt -> "Int" | TInt -> "Int"
| TBool -> "Bool" | TBool -> "Bool"
| TString -> "String" | TString -> "String"
@@ -106,7 +113,8 @@ let rec show t =
| TCon (n, args) -> n ^ "[" ^ String.concat ", " (List.map show args) ^ "]" | TCon (n, args) -> n ^ "[" ^ String.concat ", " (List.map show args) ^ "]"
| TFn { affine; params; eff; ret } -> ( | TFn { affine; params; eff; ret } -> (
(if affine then "affine fn(" else "fn(") (if affine then "affine fn(" else "fn(")
^ String.concat ", " (List.map show params) ^ String.concat ", "
(List.map (fun (o, t) -> (if o then "own " else "") ^ show t) params)
^ ")" ^ ")"
^ (match eff_resolve eff with [] -> "" | e -> " effects " ^ eff_show e) ^ (match eff_resolve eff with [] -> "" | e -> " effects " ^ eff_show e)
^ match resolve ret with TUnit -> "" | r -> " -> " ^ show r) ^ match resolve ret with TUnit -> "" | r -> " -> " ^ show r)
@@ -117,7 +125,8 @@ let rec occurs r t =
match resolve t with match resolve t with
| TMeta r' -> r == r' | TMeta r' -> r == r'
| TCon (_, args) -> List.exists (occurs r) args | TCon (_, args) -> List.exists (occurs r) args
| TFn { params; ret; _ } -> List.exists (occurs r) params || occurs r ret | TFn { params; ret; _ } ->
List.exists (fun (_, t) -> occurs r t) params || occurs r ret
| _ -> false | _ -> false
(* 결정 위치의 해소: 파라미터의 effect 자리에 단독으로 선 미지수만 묶는다. (* 결정 위치의 해소: 파라미터의 effect 자리에 단독으로 선 미지수만 묶는다.
@@ -136,6 +145,8 @@ let unify_eff a b =
let rec unify a b = let rec unify a b =
match (resolve a, resolve b) with match (resolve a, resolve b) with
| TUnknown, _ | _, TUnknown -> true | TUnknown, _ | _, TUnknown -> true
(* Never는 어떤 타입 자리에도 놓인다. crash가 match 팔에 설 수 있는 이유다 *)
| TNever, _ | _, TNever -> true
| TMeta r, TMeta r' when r == r' -> true | TMeta r, TMeta r' when r == r' -> true
| TMeta r, t | t, TMeta r -> | TMeta r, t | t, TMeta r ->
if occurs r t then false if occurs r t then false
@@ -150,8 +161,13 @@ let rec unify a b =
(* affinity는 타입 동등성의 일부가 아니다. 값이 affine인지는 무엇을 (* affinity는 타입 동등성의 일부가 아니다. 값이 affine인지는 무엇을
capture했는지로 정해지는 substructural 성질이고, move/affinity 검사가 capture했는지로 정해지는 substructural 성질이고, move/affinity 검사가
소유한다. 여기서 섞으면 두 검사가 서로의 결론을 앞질러 버린다. *) 소유한다. 여기서 섞으면 두 검사가 서로의 결론을 앞질러 버린다. *)
(* 소유권은 타입 동등성의 일부다. 빌리는 클로저를 소유 자리에 넘기는
것은 안전하지만 그 반대는 아니고, 방향을 다루려면 부분 타입이
필요하다. 우리에겐 없으므로 정확히 일치를 요구한다. *)
List.length f.params = List.length g.params List.length f.params = List.length g.params
&& List.for_all2 unify f.params g.params && List.for_all2
(fun (o1, t1) (o2, t2) -> o1 = o2 && unify t1 t2)
f.params g.params
&& unify_eff f.eff g.eff && unify f.ret g.ret && unify_eff f.eff g.eff && unify f.ret g.ret
| _ -> false | _ -> false
@@ -164,7 +180,7 @@ let rec subst tenv eenv t =
TFn TFn
{ {
affine = f.affine; affine = f.affine;
params = List.map (subst tenv eenv) f.params; params = List.map (fun (o, t) -> (o, subst tenv eenv t)) f.params;
eff = subst_eff eenv f.eff; eff = subst_eff eenv f.eff;
ret = subst tenv eenv f.ret; ret = subst tenv eenv f.ret;
} }
+2
View File
@@ -3,6 +3,8 @@
// 파라미터 위치의 effect 절은 `effects <변수>` 또는 `effects {리터럴}`만 가능하다. // 파라미터 위치의 effect 절은 `effects <변수>` 또는 `effects {리터럴}`만 가능하다.
// 합집합은 결과 위치에서만 쓸 수 있다 — 문법이 그렇게 생겼다. // 합집합은 결과 위치에서만 쓸 수 있다 — 문법이 그렇게 생겼다.
import "cool.dev/std/string" as String
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,
+14 -1
View File
@@ -2,6 +2,19 @@
// //
// variant 추가가 downstream exhaustive match를 깨뜨리는 경로. // variant 추가가 downstream exhaustive match를 깨뜨리는 경로.
import "cool.dev/std/string" as String
import "cool.dev/std/int" as Int
// 거절 코드. 예전에는 어디에도 없는 이름이었고 Code.show가 조용히
// 통과했다 — 이름 해소가 점 왼쪽을 검사하지 않아서다.
pub copyable struct Code {
value: Int,
}
pub fn show_code(c: Code) -> String {
Int.show(c.value)
}
pub enum PayError { pub enum PayError {
Declined(Code), Declined(Code),
Network(Retryable), Network(Retryable),
@@ -24,7 +37,7 @@ pub fn is_retryable(e: PayError) -> Bool {
pub fn describe(e: PayError) -> String { pub fn describe(e: PayError) -> String {
match e { match e {
Declined(code) => String.concat("declined: ", Code.show(code)), Declined(code) => String.concat("declined: ", show_code(code)),
Network(_) => "network", Network(_) => "network",
Fraud => "fraud", Fraud => "fraud",
} }
+10 -1
View File
@@ -2,6 +2,15 @@
// //
// affinity는 타입 속성, own/무표기는 파라미터 위치 속성. 둘은 직교한다. // affinity는 타입 속성, own/무표기는 파라미터 위치 속성. 둘은 직교한다.
import "cool.dev/std/string" as String
// 예전에는 File이 어디에도 없는 이름이었고 File.close(f)가 조용히
// 통과했다. 이제 선언한다 — 그리고 own self가 있으므로 닫는 일은
// 핸들 자신의 메서드다.
pub capability File {
fn close(own self) effects {File.close}
}
// 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) }
@@ -9,7 +18,7 @@ pub fn make_formatter(prefix: String) -> fn(String) -> String {
// affine 값을 소유해 가므로 own. 결과 클로저는 자동으로 affine fn이 된다. // affine 값을 소유해 가므로 own. 결과 클로저는 자동으로 affine fn이 된다.
pub fn deferred_close(own f: File) -> affine fn() effects {File.close} { pub fn deferred_close(own f: File) -> affine fn() effects {File.close} {
fn() { File.close(f) } fn() { f.close() }
} }
// affine fn을 호출 후 소비하려면 소유해야 한다. // affine fn을 호출 후 소비하려면 소유해야 한다.
+166
View File
@@ -0,0 +1,166 @@
// 설정 파일 파서.
//
// 이 파일은 검사기를 시험하려고 쓴 것이 아니라 일을 하려고 쓴 것이다.
// 그것이 이 파일의 목적이다 — 언어가 실제로 쓸 만한지는 이런 코드에서만
// 드러난다.
import "cool.dev/std/list" as List
import "cool.dev/std/string" as String
import "cool.dev/std/int" as Int
import "cool.dev/std/option" as Option
import "cool.dev/std/result" as Result
import "cool.dev/std/test" as Test
// 설정 값. 타입이 셋뿐이므로 열거형이 맞다.
pub enum Value {
Text(String),
Number(Int),
Flag(Bool),
}
pub copyable struct Entry {
line: Int,
key: String,
value: Value,
}
// 오류에 줄 번호가 있어야 사람이 고칠 수 있다.
pub copyable struct Problem {
line: Int,
message: String,
}
pub copyable struct Config {
entries: List[Entry],
problems: List[Problem],
}
// 값 하나를 읽는다. 따옴표도 타입 표기도 없다 — 모양으로 정한다.
// true/false는 Flag, 정수로 읽히면 Number, 나머지는 Text.
pub fn parse_value(raw: String) -> Value {
if raw == "true" {
Flag(true)
} else if raw == "false" {
Flag(false)
} else {
match Int.parse(raw) {
Ok(n) => Number(n),
Err(_) => Text(raw),
}
}
}
pub fn show_value(v: Value) -> String {
match v {
Text(s) => String.concat("\"", String.concat(s, "\"")),
Number(n) => Int.show(n),
Flag(b) => if b { "true" } else { "false" },
}
}
pub fn type_name(v: Value) -> String {
match v {
Text(_) => "text",
Number(_) => "number",
Flag(_) => "flag",
}
}
// 한 줄을 읽는다. 빈 줄과 주석은 값이 없고, 그것은 오류가 아니다.
// 그래서 Result가 아니라 Option[Result[...]]가 필요해 보이지만, 그러면
// 부르는 쪽이 두 겹을 벗겨야 한다. Config 하나에 둘 다 모으는 편이 낫다.
pub fn parse_line(cfg: Config, no: Int, raw: String) -> Config {
let line = String.trim(raw)
let parts = String.split(line, "=")
if String.is_empty(line) {
cfg
} else if String.starts_with(line, "#") {
cfg
} else if List.len(parts) != 2 {
add_problem(cfg, no, String.concat("= 가 하나여야 합니다: ", line))
} else {
let key = String.trim(Option.unwrap_or(List.nth(parts, 0), ""))
let val = String.trim(Option.unwrap_or(List.nth(parts, 1), ""))
if String.is_empty(key) {
add_problem(cfg, no, "이름이 비어 있습니다")
} else {
add_entry(cfg, Entry { line: no, key: key, value: parse_value(val) })
}
}
}
pub fn add_entry(cfg: Config, e: Entry) -> Config {
Config {
entries: List.push(cfg.entries, e),
problems: cfg.problems,
}
}
pub fn add_problem(cfg: Config, no: Int, msg: String) -> Config {
Config {
entries: cfg.entries,
problems: List.push(cfg.problems, Problem { line: no, message: msg }),
}
}
pub fn parse(text: String) -> Config {
let lines = List.enumerate(String.split(text, "\n"))
let empty = Config { entries: [], problems: [] }
List.fold(lines, empty, fn(own cfg, l) {
parse_line(cfg, l.i + 1, l.value)
})
}
// 이름으로 찾는다. 없으면 Err — 못 찾은 것은 오류지 빈 값이 아니다.
pub fn lookup(cfg: Config, key: String) -> Result[Value, String] {
let hit = List.filter(cfg.entries, fn(e) { e.key == key })
Result.map(
Option.ok_or(List.first(hit), String.concat("설정에 없습니다: ", key)),
fn(e) { e.value },
)
}
pub fn get_int(cfg: Config, key: String) -> Result[Int, String] {
match lookup(cfg, key)? {
Number(n) => Ok(n),
Text(_) => Err(String.concat(key, "은(는) 정수가 아닙니다")),
Flag(_) => Err(String.concat(key, "은(는) 정수가 아닙니다")),
}
}
pub fn get_flag(cfg: Config, key: String) -> Result[Bool, String] {
match lookup(cfg, key)? {
Flag(b) => Ok(b),
Text(_) => Err(String.concat(key, "은(는) 참거짓이 아닙니다")),
Number(_) => Err(String.concat(key, "은(는) 참거짓이 아닙니다")),
}
}
// ------------------------------------------------------------------
// 테스트. 일반 코드와 같은 검사를 받고, capability를 받지 않으므로
// effect-free임이 증명된다 — 파일도 시계도 못 건드린다.
// ------------------------------------------------------------------
test "값의 타입은 모양으로 정한다" {
Test.assert(type_name(parse_value("true")) == "flag")
Test.assert(type_name(parse_value("42")) == "number")
Test.assert(type_name(parse_value("hello")) == "text")
}
test "빈 줄과 주석은 항목이 아니다" {
let cfg = parse("\n# 주석\n\n")
Test.assert(List.is_empty(cfg.entries))
Test.assert(List.is_empty(cfg.problems))
}
test "= 가 하나가 아니면 문제로 기록한다" {
let cfg = parse("a = 1\nb = c = d\n")
Test.assert(List.len(cfg.entries) == 1)
Test.assert(List.len(cfg.problems) == 1)
}
test "없는 이름을 찾으면 Err다" {
let cfg = parse("a = 1\n")
Test.assert(!Result.is_ok(get_int(cfg, "없음")))
Test.assert(Result.is_ok(get_int(cfg, "a")))
}
+11
View File
@@ -0,0 +1,11 @@
# 예제 설정
name = coollang
threads = 4
port = 8080
verbose = true
greeting = hello world
# 아래 두 줄은 일부러 틀렸다
= 42
broken = a = b
+96
View File
@@ -0,0 +1,96 @@
// 설정 파일 리포트 도구.
//
// 파일을 읽고, 파싱하고, 문제를 보고하고, 요약을 낸다.
// 권한은 셋뿐이다 — 읽기(File), 인자(Args), 출력(Console). 그 밖의 일은
// 이 프로그램이 할 수 없다. 시그니처에 적힌 것이 이 프로그램의 전부다.
import "config" as Cfg
import "cool.dev/std/list" as List
import "cool.dev/std/string" as String
import "cool.dev/std/int" as Int
import "cool.dev/std/option" as Option
pub capability Console {
fn print(s: String) effects {Console.print}
}
pub capability File {
fn read(path: String) effects {File.read} -> Result[String, String]
}
pub capability Args {
fn all() effects {Args.all} -> List[String]
}
pub fn report(c: Console, cfg: Cfg.Config) effects {Console.print} {
c.print("설정")
List.each(cfg.entries, fn(e) {
c.print(String.join("", [
" ", e.key, " = ", Cfg.show_value(e.value),
" (", Cfg.type_name(e.value), ")",
]))
})
c.print("")
c.print(String.join("", [
"항목 ", Int.show(List.len(cfg.entries)),
"개, 문제 ", Int.show(List.len(cfg.problems)), "개",
]))
if List.is_empty(cfg.problems) {
c.print("문제 없음")
} else {
c.print("")
c.print("문제")
List.each(cfg.problems, fn(p) {
c.print(String.join("", [ " ", Int.show(p.line), "행: ", p.message ]))
})
}
}
// 타입이 있는 조회. 없거나 타입이 다르면 Err이고, 둘 다 사람이 읽을 수 있다.
pub fn check_known(c: Console, cfg: Cfg.Config) effects {Console.print} {
c.print("")
c.print("알려진 설정")
show_int(c, cfg, "threads")
show_int(c, cfg, "port")
show_flag(c, cfg, "verbose")
}
pub fn show_int(c: Console, cfg: Cfg.Config, key: String) effects {Console.print} {
match Cfg.get_int(cfg, key) {
Ok(n) => c.print(String.join("", [ " ", key, " = ", Int.show(n) ])),
Err(m) => c.print(String.concat(" ", m)),
}
}
pub fn show_flag(c: Console, cfg: Cfg.Config, key: String) effects {Console.print} {
match Cfg.get_flag(cfg, key) {
Ok(b) => c.print(String.join("", [
" ", key, " = ", if b { "true" } else { "false" },
])),
Err(m) => c.print(String.concat(" ", m)),
}
}
pub fn first_arg(xs: List[String]) -> Result[String, String] {
Option.ok_or(List.first(xs), "설정 파일 경로가 필요합니다")
}
pub fn main(c: Console, f: File, a: Args)
effects {Console.print, File.read, Args.all} {
match run(c, f, a) {
Ok(_) => unit,
Err(m) => c.print(String.concat("오류: ", m)),
}
}
pub fn run(c: Console, f: File, a: Args)
effects {Console.print, File.read, Args.all} -> Result[Unit, String] {
let path = first_arg(a.all())?
let text = f.read(path)?
let cfg = Cfg.parse(text)
report(c, cfg)
check_known(c, cfg)
Ok(unit)
}
+3
View File
@@ -2,3 +2,6 @@
pub fn show(n: Int) -> String pub fn show(n: Int) -> String
pub fn abs(n: Int) -> Int pub fn abs(n: Int) -> Int
// 실패할 수 있으므로 Result다. 예외도, 0을 돌려주는 관례도 없다.
pub fn parse(s: String) -> Result[Int, String]
+44 -2
View File
@@ -2,14 +2,43 @@
// //
// 본문이 없다. 런타임이 구현하고, 이 파일은 그 계약을 말한다. // 본문이 없다. 런타임이 구현하고, 이 파일은 그 계약을 말한다.
// 그래서 이 파일은 구현이 아니라 시험대다 — effect 다형성이 실제로 쓸 만한지가 // 그래서 이 파일은 구현이 아니라 시험대다 — effect 다형성이 실제로 쓸 만한지가
// 여기서 결정된다. each map이 effect 변수 하나로 표현되지 않으면 규칙이 // each, map, fold, filter에서 결정된다. 규칙이 틀렸으면 여기서 드러난다.
// 틀린 것이고, 그건 v1로 미룰 수 없는 발견이다.
// //
// e는 파라미터의 effect 슬롯에 홀로 나타난다 (결정 위치). 호출 지점에서 // e는 파라미터의 effect 슬롯에 홀로 나타난다 (결정 위치). 호출 지점에서
// 인자의 시그니처를 읽어 묶인다 — 추론이 아니라 읽기다. // 인자의 시그니처를 읽어 묶인다 — 추론이 아니라 읽기다.
pub fn len[a](xs: List[a]) -> Int pub fn len[a](xs: List[a]) -> Int
pub fn is_empty[a](xs: List[a]) -> Bool
// 인덱싱 연산자가 언어에 없다. n번째를 꺼내는 일은 이름 있는 함수가 하고,
// 없을 수 있다는 사실은 Option이 말한다 — 범위를 벗어나면 예외도 기본값도
// 아니고 None이다.
pub fn first[a](xs: List[a]) -> Option[a]
pub fn nth[a](xs: List[a], i: Int) -> Option[a]
// 첫 원소를 뺀 나머지. 비어 있으면 빈 리스트다 — 없는 것을 빼는 일은
// 오류가 아니라 아무 일도 아니다. first와 짝이고, 둘은 서로 어긋날 수 없다.
pub fn rest[a](xs: List[a]) -> List[a]
// 번호를 붙인 값. fold에 인덱스를 넣는 대신 이것 하나를 둔다 —
// fold_indexed를 만들면 map_indexed, each_indexed가 따라오고 그것이
// "한 개념 한 방식"을 깨는 방향이다. enumerate는 기존 각 함수와 조합된다.
pub copyable struct Indexed[a] {
i: Int,
value: a,
}
pub fn enumerate[a](xs: List[a]) -> List[Indexed[a]]
// 뒤에 하나 붙인 새 리스트. 제자리 수정이 아니다.
pub fn push[a](xs: List[a], x: a) -> List[a]
pub fn concat[a](xs: List[a], ys: List[a]) -> List[a]
pub fn reverse[a](xs: List[a]) -> List[a]
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,
@@ -19,3 +48,16 @@ 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 filter[a, e: effects](
xs: List[a],
keep: fn(a) effects e -> Bool,
) effects e -> List[a]
// 누적자는 매 단계 소비되고 새것으로 바뀐다. own이 그 사실을 말한다 —
// 빌림으로 적으면 affine 값을 fold로 실어나를 수 없다.
pub fn fold[a, acc, e: effects](
xs: List[a],
own init: acc,
f: fn(own acc, a) effects e -> acc,
) effects e -> acc
+15
View File
@@ -0,0 +1,15 @@
// 표준 라이브러리: Option.
//
// 타입 이름이 그 타입에 딸린 함수의 이름공간이다 — Option.map.
pub fn is_some[a](o: Option[a]) -> Bool
pub fn map[a, b, e: effects](
o: Option[a],
f: fn(a) effects e -> b,
) effects e -> Option[b]
pub fn unwrap_or[a](o: Option[a], fallback: a) -> a
// None을 오류로 바꾼다. "없음"과 "왜 없는지"는 다른 정보다.
pub fn ok_or[a, err](o: Option[a], e: err) -> Result[a, err]
+15
View File
@@ -0,0 +1,15 @@
// 표준 라이브러리: Result.
pub fn is_ok[a, err](r: Result[a, err]) -> Bool
pub fn map[a, b, err, e: effects](
r: Result[a, err],
f: fn(a) effects e -> b,
) effects e -> Result[b, err]
pub fn map_err[a, err, err2, e: effects](
r: Result[a, err],
f: fn(err) effects e -> err2,
) effects e -> Result[a, err2]
pub fn unwrap_or[a, err](r: Result[a, err], fallback: a) -> a
+14
View File
@@ -1,4 +1,18 @@
// 표준 라이브러리: 문자열. // 표준 라이브러리: 문자열.
//
// 인덱싱 연산자가 언어에 없다. 문자열을 자르는 일은 이름 있는 함수가 한다 —
// 한 개념 한 방식.
pub fn len(s: String) -> Int pub fn len(s: String) -> Int
pub fn is_empty(s: String) -> Bool
pub fn concat(a: String, b: String) -> String pub fn concat(a: String, b: String) -> String
// 구분자로 자른다. 구분자가 없으면 원본 하나짜리 리스트.
pub fn split(s: String, sep: String) -> List[String]
// split의 반대. concat이 2항이라 여러 조각을 이으면 중첩이 깊어진다.
pub fn join(sep: String, parts: List[String]) -> String
pub fn trim(s: String) -> String
pub fn starts_with(s: String, prefix: String) -> Bool
pub fn contains(s: String, needle: String) -> Bool
+14
View File
@@ -0,0 +1,14 @@
// 표준 라이브러리: 테스트.
//
// assert는 특별한 것이 아니라 crash 위의 설탕이다. 그래서 본문이 있다 —
// std에서 실제 coollang으로 쓰인 첫 함수다.
//
// 실패 메시지에 값이 안 나오는 것은 의도다. `assert(a == b)`에서 a와 b를
// 보여주려면 표현식 텍스트를 잡아야 하고 그건 매크로다. 테스트 이름과
// 위치가 어느 것이 깨졌는지 말해준다.
pub fn assert(c: Bool) {
if !c {
crash("assert 실패")
}
}
+7 -2
View File
@@ -1,6 +1,11 @@
(test (test
(name test_coollang) (name test_coollang)
(libraries coollang) (libraries coollang unix)
(deps (deps
(glob_files %{workspace_root}/samples/*.cool) (glob_files %{workspace_root}/samples/*.cool)
(glob_files %{workspace_root}/std/*.cool))) (glob_files %{workspace_root}/std/*.cool)
(glob_files %{workspace_root}/docs/*.ebnf)
(glob_files_rec %{workspace_root}/dogfoods/**)
(glob_files %{workspace_root}/samples/app/*)
(glob_files %{workspace_root}/samples/modules/*.cool)
(glob_files %{workspace_root}/samples/run/*.cool)))
+882 -13
View File
@@ -180,11 +180,6 @@ let () =
check "버전 비어있지 않음" (String.length Version.string > 0); check "버전 비어있지 않음" (String.length Version.string > 0);
match Driver.check [] with Error [ _ ] -> () | _ -> check "빈 입력에는 오류" false match Driver.check [] with Error [ _ ] -> () | _ -> check "빈 입력에는 오류" false
let () =
if !failures = 0 then print_endline "ok"
else (
Printf.printf "%d개 실패\n" !failures;
exit 1)
(* --- ASI 함정: effects 절이 줄 끝에 오면 } 뒤에 NEWLINE이 삽입된다. (* --- ASI 함정: effects 절이 줄 끝에 오면 } 뒤에 NEWLINE이 삽입된다.
렉서는 이대로 두고 문법이 { NEWLINE }으로 흡수한다 (grammar.ebnf). --- *) 렉서는 이대로 두고 문법이 { NEWLINE }으로 흡수한다 (grammar.ebnf). --- *)
@@ -327,8 +322,9 @@ let () =
(parse_err "fn f(\n a: Int\n)" = Some "다중 줄 목록에는 후행 콤마가 필요합니다"); (parse_err "fn f(\n a: Int\n)" = Some "다중 줄 목록에는 후행 콤마가 필요합니다");
check "if 머리의 struct 리터럴" check "if 머리의 struct 리터럴"
(parse_err "fn f() {\n if H { a: 1 } {\n b\n }\n}" <> None); (parse_err "fn f() {\n if H { a: 1 } {\n b\n }\n}" <> None);
check "대입 왼쪽 검사" (* 대입 왼쪽 제약은 파서가 아니라 이름 해소가 본다. 구문으로 가르면
(parse_err "fn f() {\n g() = 1\n}" = Some "대입 왼쪽에는 변수나 필드만 올 수 있습니다"); ident 하나로 대입과 식이 갈리지 않아 문법이 LL(1)이 아니게 된다 *)
check "대입 왼쪽은 파서가 보지 않는다" (parse_err "fn f() {\n g() = 1\n}" = None);
check "한 줄에 문 두 개" (parse_err "fn f() {\n a b\n}" = Some "문 끝에 줄바꿈이 필요합니다") check "한 줄에 문 두 개" (parse_err "fn f() {\n a b\n}" = Some "문 끝에 줄바꿈이 필요합니다")
(* --- 괄호 안에서는 struct 리터럴이 다시 허용된다 --- *) (* --- 괄호 안에서는 struct 리터럴이 다시 허용된다 --- *)
@@ -392,6 +388,8 @@ let has_err src frag =
let () = let () =
check "중복 정의" (has_err "fn f()\nfn f()" "두 번 정의"); check "중복 정의" (has_err "fn f()\nfn f()" "두 번 정의");
(* 파서에서 옮겨온 검사. 구문으로 가르면 문법이 LL(1)이 아니게 된다 *)
check "대입 왼쪽" (has_err "fn f() {\n g() = 1\n}" "대입 왼쪽에는 변수나 필드만");
check "중복 파라미터" (has_err "fn f(a: Int, a: Int)" "파라미터 a"); check "중복 파라미터" (has_err "fn f(a: Int, a: Int)" "파라미터 a");
check "중복 제네릭" (has_err "fn f[a, a]()" "제네릭 파라미터 a"); check "중복 제네릭" (has_err "fn f[a, a]()" "제네릭 파라미터 a");
check "선언되지 않은 effect 변수" (has_err "fn f() effects e" "선언되지 않은 effect 변수 e"); check "선언되지 않은 effect 변수" (has_err "fn f() effects e" "선언되지 않은 effect 변수 e");
@@ -1029,9 +1027,11 @@ let console =
let outputs ?(use = []) src expected = let outputs ?(use = []) src expected =
match run_src (imports use ^ console ^ src) with match run_src (imports use ^ console ^ src) with
| Ok out -> out = expected | out, None -> out = expected
| Error e -> | out, Some e ->
Printf.printf " (실행 오류: %s)\n" (Session.string_of_error e); Printf.printf " (실행 오류: %s / 그때까지 출력: %S)\n"
(Session.string_of_error e)
out;
false false
let () = let () =
@@ -1110,7 +1110,7 @@ let () =
"a\nb\nafter\n"); "a\nb\nafter\n");
(* 권한은 런타임에서만 온다. main이 선언하지 않으면 존재하지 않는다. *) (* 권한은 런타임에서만 온다. main이 선언하지 않으면 존재하지 않는다. *)
check "선언하지 않은 capability는 실행 시점에도 없다" check "선언하지 않은 capability는 실행 시점에도 없다"
(match run_src "pub fn main() { }" with Ok "" -> true | _ -> false); (match run_src "pub fn main() { }" with "", None -> true | _ -> false);
check "런타임이 모르는 capability는 거절한다" check "런타임이 모르는 capability는 거절한다"
(match (match
run_src run_src
@@ -1119,8 +1119,28 @@ let () =
}\n\n\ }\n\n\
pub fn main(d: Db) effects {Db.read} -> Int { d.read() }" pub fn main(d: Db) effects {Db.read} -> Int { d.read() }"
with with
| Error e -> has_sub e.message "제공하지 않습니다" | _, Some e -> has_sub e.message "제공하지 않습니다"
| Ok _ -> false) | _, None -> false)
(* 실패해도 그때까지의 출력은 남는다. print는 실제로 일어난 effect이고,
일어난 일을 안 보여주면 "어디까지 갔나"를 알 수 없다. *)
let () =
match
run_src
(imports [ "Int" ] ^ console
^ "pub fn risky(a: Int, b: Int) -> Int { a / b }\n\n\
pub fn main(c: Console) effects {Console.print} {\n\
\ c.print(\"\")\n\
\ c.print(Int.show(risky(10, 0)))\n\
\ c.print(\"\")\n\
}")
with
| out, Some e ->
check "0으로 나누면 실행 시점 오류다" (has_sub e.message "0으로 나눌 수 없습니다");
check "실패해도 그 전 출력은 남는다" (out = "\n")
| _, None ->
check "0으로 나누면 실행 시점 오류다" false;
check "실패해도 그 전 출력은 남는다" false
(* 표준 라이브러리 시그니처가 실제 검사에 쓰이는지. (* 표준 라이브러리 시그니처가 실제 검사에 쓰이는지.
@@ -1232,3 +1252,852 @@ let () =
in in
List.exists (fun m -> has_sub m "가져왔지만 쓰지 않습니다") ms List.exists (fun m -> has_sub m "가져왔지만 쓰지 않습니다") ms
&& List.exists (fun m -> has_sub m "String이(가) 필요한데 Int") ms) && List.exists (fun m -> has_sub m "String이(가) 필요한데 Int") ms)
(* ------------------------------------------------------------------ *)
(* 실제 프로그램 *)
(* *)
(* samples/app은 검사기를 시험하려고 쓴 것이 아니라 일을 하려고 쓴 것이다. *)
(* 언어가 쓸 만한지는 이런 코드에서만 드러난다. *)
(* ------------------------------------------------------------------ *)
let () =
let st = Session.create ~root:"../samples/app" ~std:"../std" () in
match
Session.run
~args:[ "../samples/app/example.conf" ]
st "../samples/app/main.cool"
with
| _, Some e ->
Printf.printf " (실행 오류: %s)\n" (Session.string_of_error e);
check "app: 설정 리포트가 돈다" false
| out, None ->
check "app: 항목과 문제를 센다" (has_sub out "항목 5개, 문제 2개");
check "app: 값의 타입을 모양으로 정한다"
(has_sub out "threads = 4 (number)"
&& has_sub out "verbose = true (flag)"
&& has_sub out "name = \"coollang\" (text)");
check "app: 문제에 줄 번호가 붙는다"
(has_sub out "10행: 이름이 비어 있습니다" && has_sub out "11행: = 가 하나여야 합니다");
check "app: 타입 있는 조회" (has_sub out "port = 8080")
let () =
let st = Session.create ~root:"../samples/app" ~std:"../std" () in
match Session.run st "../samples/app/main.cool" with
| out, None -> check "app: 인자가 없으면 말해준다" (has_sub out "경로가 필요합니다")
| _, Some _ -> check "app: 인자가 없으면 말해준다" false
let () =
let st = Session.create ~root:"../samples/app" ~std:"../std" () in
match Session.run ~args:[ "/없는/파일.conf" ] st "../samples/app/main.cool" with
| out, None -> check "app: 없는 파일을 Err로 돌려준다" (has_sub out "오류: ")
| _, Some _ -> check "app: 없는 파일을 Err로 돌려준다" false
(* std 선언과 런타임 구현이 어긋나면 검사는 통과하고 실행이 죽는다.
v0에서 둘은 다른 파일에 있으므로 일치는 테스트가 지킨다 (friction F7). *)
let () =
let dir = "../std" in
let declared =
Sys.readdir dir |> Array.to_list
|> List.filter (fun f -> Filename.check_suffix f ".cool")
|> List.concat_map (fun f ->
let m = ref "" in
(match Driver.ast (Filename.concat dir f) with
| Error _ -> ()
| Ok _ -> m := Filename.remove_extension f);
match Driver.ast (Filename.concat dir f) with
| Error _ -> []
| Ok ast ->
List.filter_map
(function
(* 본문이 있으면 coollang으로 구현된 것이다 (std/test.cool의
assert). 런타임 구현이 필요한 것은 본문 없는 선언뿐이다 *)
| Ast.I_fn { decl; _ } when decl.fn_body = None ->
Some (!m ^ "." ^ decl.fn_name)
| _ -> None)
ast.Ast.items)
in
check "std에 선언이 있다" (List.length declared > 20);
List.iter
(fun name ->
check
(Printf.sprintf "std 선언 %s에 런타임 구현이 있다" name)
(List.mem name Interp.implemented))
declared;
List.iter
(fun name ->
check
(Printf.sprintf "런타임 구현 %s에 std 선언이 있다" name)
(List.mem name declared))
Interp.implemented
(* ------------------------------------------------------------------ *)
(* 문법 파일 *)
(* *)
(* docs/grammar.ebnf는 이제 문서가 아니라 기계가 읽는 소스다. 여기서 *)
(* 검사하는 것 셋: *)
(* 1. 기계가 읽을 수 있는가 *)
(* 2. 정의되지 않은 이름이 토큰 부류뿐인가 *)
(* 3. LL(1)인가 — 문법 첫머리의 주장이 여기서 검증된다 *)
(* ------------------------------------------------------------------ *)
let () =
let src =
let ic = open_in_bin "../docs/grammar.ebnf" in
let n = in_channel_length ic in
let s = really_input_string ic n in
close_in ic;
s
in
match Ebnf.parse_result src with
| Error e ->
Printf.printf " (문법 파일 %d행: %s)\n" e.line e.msg;
check "grammar.ebnf를 읽을 수 있다" false
| Ok g ->
check "grammar.ebnf를 읽을 수 있다" true;
check "프로덕션이 충분히 있다" (List.length g > 50);
(* 어휘 층의 이름만 정의 없이 참조될 수 있다 *)
let allowed = [ "NEWLINE"; "char"; "digit"; "letter" ] in
let undef = Ebnf.undefined g in
List.iter
(fun n ->
check
(Printf.sprintf "grammar.ebnf: %s는 정의되었거나 토큰 부류다" n)
(List.mem n allowed))
undef;
check "grammar.ebnf에 죽은 프로덕션이 없다" (Ebnf.unreachable g ~start:"module" = []);
let g = Ebnf.expand g in
let tokens = allowed @ [ "ident"; "int_lit"; "string_lit" ] in
let real =
List.filter
(fun (c : Ebnf.conflict) -> not c.c_greedy)
(Ebnf.conflicts ~tokens ~greedy:[ "NEWLINE" ] g)
in
List.iter
(fun (c : Ebnf.conflict) ->
Printf.printf " (LL(1) 충돌 %s:%d [%s] %s — %s)\n" c.c_rule c.c_line
c.c_kind
(String.concat " " c.c_tokens)
c.c_detail)
real;
check "grammar.ebnf는 LL(1)이다" (real = [])
(* 문법 문서의 어휘 절은 코드에서 생성된다. 두 곳에 손으로 적힌 목록은
어긋난다 — 이 프로젝트가 그것으로 한 번 데었다. *)
let () =
let src =
let ic = open_in_bin "../docs/grammar.ebnf" in
let n = in_channel_length ic in
let s = really_input_string ic n in
close_in ic;
s
in
let block = Lexical_doc.render () in
check "문법 문서의 어휘 절이 token.ml과 일치한다" (has_sub src block);
if not (has_sub src block) then begin
print_endline " 현재 코드가 만드는 블록:";
print_endline block
end
(* ------------------------------------------------------------------ *)
(* 문법과 파서의 대조 *)
(* *)
(* 문법 파일에서 직접 읽는 인식기와 손으로 쓴 파서에 같은 토큰 열을 주고 *)
(* 판정이 같은지 본다. 갈리면 둘 중 하나가 틀린 것이고 빌드가 깨진다. *)
(* 이것이 설명서와 구현이 어긋나지 않게 하는 유일한 장치다 — *)
(* "문서를 잘 관리하자"는 이미 실패했다. *)
(* ------------------------------------------------------------------ *)
let () =
let read f =
let ic = open_in_bin f in
let n = in_channel_length ic in
let s = really_input_string ic n in
close_in ic;
s
in
let g =
match Ebnf.parse_result (read "../docs/grammar.ebnf") with
| Ok g -> g
| Error e -> failwith (Printf.sprintf "문법 %d행: %s" e.line e.msg)
in
let tokens =
[ "ident"; "int_lit"; "string_lit"; "NEWLINE"; "char"; "digit"; "letter" ]
in
let files =
List.concat_map
(fun dir ->
try
Sys.readdir dir |> Array.to_list
|> List.filter (fun f -> Filename.check_suffix f ".cool")
|> List.sort compare
|> List.map (Filename.concat dir)
with Sys_error _ -> [])
[
"../samples";
"../samples/modules";
"../samples/run";
"../samples/app";
"../std";
]
in
check "대조할 파일이 있다" (List.length files > 15);
List.iter
(fun f ->
match Lexer.lex_result (read f) with
| Error _ -> ()
| Ok toks ->
let hand = Result.is_ok (Parser.parse_result toks) in
let spec = Result.is_ok (Recognize.check ~tokens g toks) in
if hand <> spec then
Printf.printf " (%s: 파서 %s / 문법 %s)\n" f
(if hand then "받음" else "거부")
(if spec then "받음" else "거부");
check (Filename.basename f ^ ": 문법과 파서의 판정이 같다") (hand = spec))
files
(* 문법이 약속한 것을 파서가 실제로 읽는가.
저장소의 .cool 파일만으로는 사람이 쓴 코드만 훑는다 — 아무도 안 밟은
구석은 드러나지 않는다. 문법에서 문장을 만들어 파서에 먹인다.
커버리지를 같이 재는 이유: 안 밟은 규칙은 시험되지 않은 규칙이다. *)
let () =
let read f =
let ic = open_in_bin f in
let n = in_channel_length ic in
let s = really_input_string ic n in
close_in ic;
s
in
match Ebnf.parse_result (read "../docs/grammar.ebnf") with
| Error _ -> ()
| Ok g ->
let tokens =
[
"ident"; "int_lit"; "string_lit"; "NEWLINE"; "char"; "digit"; "letter";
]
in
let visited = Hashtbl.create 128 in
let bad = ref 0 in
let shown = ref 0 in
for i = 1 to 2500 do
Random.init i;
let toks = Ebnf_gen.sentence ~tokens ~visited ~max_depth:18 g in
match Parser.parse_result toks with
| Ok _ -> ()
| Error e ->
incr bad;
if !shown < 3 then begin
incr shown;
Printf.printf " (씨앗 %d: 문법은 만들었는데 파서가 거부 — %s)\n" i e.msg
end
done;
check "문법이 만든 문장을 파서가 전부 받는다" (!bad = 0);
let lexical =
[
"ident";
"int_lit";
"string_lit";
"str_char";
"escape";
"bool_lit";
"literal";
]
in
let all =
Ebnf.expand g
|> List.map (fun (r : Ebnf.rule) -> r.name)
|> List.sort_uniq compare
|> List.filter (fun r -> not (List.mem r lexical))
in
let unvisited = List.filter (fun r -> not (Hashtbl.mem visited r)) all in
if unvisited <> [] then
Printf.printf " (밟지 않은 프로덕션: %s)\n" (String.concat " " unvisited);
check "생성이 모든 프로덕션을 밟는다" (unvisited = [])
(* ------------------------------------------------------------------ *)
(* panic, Never, test *)
(* ------------------------------------------------------------------ *)
let () =
let dir = Filename.concat (Filename.get_temp_dir_name ()) "cool_crashtest" in
ignore (Sys.command (Printf.sprintf "mkdir -p %s" (Filename.quote dir)));
let f = Filename.concat dir "m.cool" in
let run src =
write f src;
let st = Session.create ~root:dir ~std:"../std" () in
Session.test st f
in
let src =
"import \"cool.dev/std/test\" as Test\n\n\
pub enum E {\n\
\ A,\n\
\ B,\n\
}\n\n\
pub fn name(e: E) -> String {\n\
\ match e {\n\
\ A => \"a\",\n\
\ B => crash(\"B는 아직\"),\n\
\ }\n\
}\n\n\
test \"통과\" {\n\
\ Test.assert(name(A) == \"a\")\n\
}\n\n\
test \"assert 실패\" {\n\
\ Test.assert(1 == 2)\n\
}\n\n\
test \"crash 실패\" {\n\
\ Test.assert(name(B) == \"b\")\n\
}"
in
match run src with
| Error es ->
List.iter
(fun (e : Session.error) -> Printf.printf " (%s)\n" e.message)
es;
check "crash/test 예제가 검사를 통과한다" false
| Ok rs ->
check "crash/test 예제가 검사를 통과한다" true;
check "테스트 셋이 잡힌다" (List.length rs = 3);
let failed =
List.filter (fun (r : Interp.test_result) -> r.t_failure <> None) rs
in
check "실패는 둘이다" (List.length failed = 2);
(* Never가 match 팔에 서지 못하면 위 예제가 타입 검사를 통과하지 못한다 *)
check "assert 실패가 보고된다"
(List.exists
(fun (r : Interp.test_result) ->
r.t_name = "assert 실패"
&&
match r.t_failure with
| Some (_, m) -> has_sub m "assert 실패"
| None -> false)
rs);
check "crash 메시지가 그대로 보고된다"
(List.exists
(fun (r : Interp.test_result) ->
r.t_name = "crash 실패"
&&
match r.t_failure with
| Some (_, m) -> has_sub m "B는 아직"
| None -> false)
rs);
(* 하나가 죽어도 나머지는 돈다 — 격리는 런타임의 일이다 *)
check "실패해도 다른 테스트는 돈다"
(List.exists
(fun (r : Interp.test_result) ->
r.t_name = "통과" && r.t_failure = None)
rs)
(* 테스트는 effect를 수행할 수 없다 — capability를 받지 않으므로 증명된다 *)
let () =
let dir = Filename.concat (Filename.get_temp_dir_name ()) "cool_crashtest" in
let f = Filename.concat dir "e.cool" in
write f
"pub capability Db {\n\
\ fn read() effects {Db.read} -> Int\n\
}\n\n\
pub fn sneaky() effects {Db.read} -> Int\n\n\
test \"금지\" {\n\
\ let n = sneaky()\n\
}";
let st = Session.create ~root:dir ~std:"../std" () in
Session.load st f;
check "테스트는 effect를 수행할 수 없다"
(List.exists
(fun (e : Session.error) -> has_sub e.message "테스트는 effect를 수행할 수 없습니다")
(Session.errors st))
(* samples/app의 테스트가 실제로 돈다 *)
let () =
let st = Session.create ~root:"../samples/app" ~std:"../std" () in
match Session.test st "../samples/app/config.cool" with
| Error es ->
List.iter
(fun (e : Session.error) -> Printf.printf " (%s)\n" e.message)
es;
check "samples/app의 테스트가 돈다" false
| Ok rs ->
check "samples/app의 테스트가 돈다" (List.length rs >= 4);
check "samples/app의 테스트가 전부 통과한다"
(List.for_all (fun (r : Interp.test_result) -> r.t_failure = None) rs)
(* crash는 scope 트리를 타고 올라간다. 자식이 crash하면 그 scope는 실패하고,
scope 뒤의 코드는 실행되지 않는다.
v0는 태스크가 하나라 이 규칙이 축약된 형태로만 관측된다. 아래에서 "자식
셋"이 안 도는 것은 취소가 아니라 순차 실행의 부산물이다 — 취소는 아직
정하지 않았다. 고정하는 것은 전파이지 형제의 운명이 아니다. *)
let () =
match
run_src
(console
^ "pub fn main(c: Console, root: TaskScope)\n\
\ effects {Console.print, TaskScope.spawn} {\n\
\ c.print(\"scope 앞\")\n\
\ scope work = root {\n\
\ work.spawn(fn() { c.print(\"자식 하나\") })\n\
\ work.spawn(fn() { crash(\"자식 둘이 죽었다\") })\n\
\ }\n\
\ c.print(\"scope 뒤\")\n\
}")
with
| out, Some e ->
check "자식의 crash가 scope 밖으로 전파된다" (has_sub e.message "자식 둘이 죽었다");
check "crash 전의 출력은 남는다" (has_sub out "자식 하나");
check "scope 뒤는 실행되지 않는다" (not (has_sub out "scope 뒤"))
| _, None ->
check "자식의 crash가 scope 밖으로 전파된다" false;
check "crash 전의 출력은 남는다" false;
check "scope 뒤는 실행되지 않는다" false
(* ------------------------------------------------------------------ *)
(* dogfoods — 언어가 실제 문제를 표현하는가 *)
(* *)
(* samples가 검사기를 시험한다면 여기는 언어를 시험한다. 통과가 회귀 *)
(* 테스트가 된다 — 규칙을 바꿨을 때 어느 유스케이스가 깨지는지 보인다. *)
(* *)
(* 기준이 둘인 이유: check만 보면 공허하다. import가 조용히 해소되지 *)
(* 않으면 그 모듈의 이름이 전부 불투명해져 무엇이든 통과한다. *)
(* 실제로 첫 시도에서 그 일이 일어났다 (FINDINGS D1). *)
(* ------------------------------------------------------------------ *)
let () =
let cases =
[
("../dogfoods/std-draft/fs.cool", []);
( "../dogfoods/atomic-update/careful.cool",
[ "../dogfoods/std-draft/fs.cool" ] );
("../dogfoods/std-draft/db.cool", []);
("../dogfoods/db-pool/careful.cool", [ "../dogfoods/std-draft/db.cool" ]);
("../dogfoods/lru-cache/values.cool", []);
( "../dogfoods/lru-cache/resources.cool",
[ "../dogfoods/std-draft/db.cool" ] );
("../dogfoods/std-draft/os.cool", []);
("../dogfoods/mini-shell/parse.cool", []);
( "../dogfoods/mini-shell/shell.cool",
[ "../dogfoods/std-draft/os.cool"; "../dogfoods/mini-shell/parse.cool" ]
);
("../dogfoods/std-draft/sup.cool", []);
( "../dogfoods/supervisor/workers.cool",
[ "../dogfoods/std-draft/sup.cool"; "../dogfoods/std-draft/db.cool" ] );
]
in
List.iter
(fun (path, deps) ->
let st = Session.create ~root:(Filename.dirname path) ~std:"../std" () in
Session.load st path;
let errs = Session.errors st in
List.iter
(fun (e : Session.error) ->
Printf.printf " (%s)\n" (Session.string_of_error e))
errs;
check (Filename.basename path ^ ": check 통과") (errs = []);
(* 의존 모듈이 실제로 로드됐는가 — 이게 없으면 위 줄이 공허하다.
경로 표기가 여러 가지라(../a/../b) 파일 이름으로 맞춘다 *)
let loaded =
Hashtbl.fold
(fun k _ acc -> Filename.basename k :: acc)
st.Session.modules []
in
List.iter
(fun d ->
check
(Filename.basename path ^ ": " ^ Filename.basename d
^ "이(가) 실제로 해소된다")
(List.mem (Filename.basename d) loaded))
deps)
cases
(* Mini Shell의 파싱은 순수하므로 실제로 돌릴 수 있다. 개밥 먹기 중
처음으로 "쓰기만 한 것"이 아니라 "돌아가는 것"이 나왔다. *)
let () =
let st = Session.create ~root:"../dogfoods/mini-shell" ~std:"../std" () in
match Session.test st "../dogfoods/mini-shell/parse.cool" with
| Error es ->
List.iter
(fun (e : Session.error) ->
Printf.printf " (%s)\n" (Session.string_of_error e))
es;
check "mini-shell 파서 테스트가 돈다" false
| Ok results ->
check "mini-shell 파서 테스트가 다섯 개다" (List.length results = 5);
check "mini-shell 파서 테스트가 전부 통과한다"
(List.for_all (fun (r : Interp.test_result) -> r.t_failure = None) results)
(* D3: 자원을 들고 ?를 쓰면 정리를 건너뛴다.
v0 정책은 여전히 "오용 금지, 누수 허용"이다 — 타입 층에서 막지 않는다.
다만 이제 lint가 그 자리를 가리킨다. 언어가 강제로 막을지는 `with`가
결정할 문제이고, 그 결정에는 "실제 코드에서 얼마나 자주 새는가"라는
측정이 필요하다. lint가 그 측정이다.
naive와 careful 쌍이 이 개밥 먹기의 산출물이므로, lint가 정확히 naive
쪽에서만 울리는지가 그 쌍이 뜻을 갖는 조건이다. *)
let () =
let leaks root path =
let st = Session.create ~root ~std:"../std" () in
Session.load st path;
List.filter
(fun (e : Session.error) ->
let m = e.message in
let frag = "정리되지 않습니다" in
let n = String.length frag in
let rec go i =
i + n <= String.length m && (String.sub m i n = frag || go (i + 1))
in
go 0)
(Session.errors st)
in
let a = leaks "../dogfoods/atomic-update" "../dogfoods/atomic-update/naive.cool" in
let b =
leaks "../dogfoods/atomic-update" "../dogfoods/atomic-update/careful.cool"
in
let c = leaks "../dogfoods/db-pool" "../dogfoods/db-pool/naive.cool" in
let d = leaks "../dogfoods/db-pool" "../dogfoods/db-pool/careful.cool" in
check "naive는 ?마다 새는 것을 가리킨다" (List.length a = 2);
check "careful은 조용하다 (파일)" (b = []);
check "naive는 두 층 모두 가리킨다 (풀)" (List.length c = 2);
check "careful은 조용하다 (풀)" (d = []);
(* 누수는 여전히 타입 층의 오류가 아니다 — lint 말고 다른 것이 없어야 한다*)
let st = Session.create ~root:"../dogfoods/atomic-update" ~std:"../std" () in
Session.load st "../dogfoods/atomic-update/naive.cool";
check "누수 말고는 아무 오류도 없다 (오용 금지, 누수 허용)"
(List.length (Session.errors st) = 2)
(* D10 회귀: Option/Result 패턴이 타입을 나른다.
전에는 내장 생성자가 typecheck의 ctors에 없어 None이 변수 바인딩이 되고
Ok(v)의 v가 TUnknown이었다 — 즉 핵심 오류 처리 수단의 패턴 매칭이 타입을
전혀 검사하지 않았다. *)
let () =
check "Ok(v)의 v가 타입을 갖는다"
(type_has
"fn f(r: Result[Int, String]) -> String {\n\
\ match r {\n\
\ Ok(v) => v,\n\
\ Err(e) => e,\n\
\ }\n\
}"
"String이(가) 필요한데 Int");
check "None은 변수 바인딩이 아니다"
(type_has
"fn f(o: Option[Int]) -> Int {\n\
\ match o {\n\
\ None => \"x\",\n\
\ Some(v) => v,\n\
\ }\n\
}"
"match 팔의 타입이 서로 다릅니다")
(* 보안 정리 (ii): safe code에서 capability는 복제되지 않는다.
struct 필드를 통한 구멍이 있었다 — 필드 접근이 빌린 값을 돌려주는데
Move 자리에서 그것을 검사하는 곳이 없었다. 빌린 wrapper에서도 됐다. *)
let () =
let src =
"capability Pay {\n\
\ fn charge(n: Int) effects {Pay.charge}\n\
}\n\n\
struct Wrapper {\n\
\ pay: Pay,\n\
\ label: String,\n\
}\n\n\
fn consume(own p: Pay) effects {Pay.charge}\n\n"
in
check "capability를 필드로 복제할 수 없다"
(move_has
(src
^ "fn duplicate(w: Wrapper) effects {Pay.charge} {\n\
\ consume(w.pay)\n\
\ consume(w.pay)\n\
}")
"부분 move가 없습니다");
check "소유한 struct에서도 필드를 꺼낼 수 없다"
(move_has
(src
^ "fn take(own w: Wrapper) effects {Pay.charge} {\n consume(w.pay)\n}")
"부분 move가 없습니다");
check "copyable 필드는 막지 않는다"
(move_errs (src ^ "fn label_of(w: Wrapper) -> String {\n w.label\n}") = [])
(* ================================================================== *)
(* D4 — 수신자를 소비하는 capability 메서드 (own self) *)
(* *)
(* 이것이 없으면 자원이 자기 정리를 스스로 선언할 수 없고, 정리에 늘 *)
(* 바깥 권한이 따라붙는다. 그러면 with가 자원만 보고 무엇을 불러야 *)
(* 할지 알 수 없다 (dogfoods/FINDINGS D9). *)
(* ================================================================== *)
let selfsrc =
"capability Lease {\n\
\ fn query(s: String) effects {Lease.query} -> Int\n\
\ fn release(own self) effects {Lease.release}\n\
}\n\n"
let () =
check "own self는 수신자를 소비한다 — 한 번은 된다"
(move_ok
(selfsrc
^ "fn once(own l: Lease) effects {Lease.release} {\n l.release()\n}"));
check "두 번 소비하면 잡힌다"
(move_has
(selfsrc
^ "fn twice(own l: Lease) effects {Lease.release} {\n\
\ l.release()\n\
\ l.release()\n\
}")
"이미 move되었습니다");
check "빌린 수신자는 소비할 수 없다"
(move_has
(selfsrc
^ "fn borrowed(l: Lease) effects {Lease.release} {\n l.release()\n}")
"빌린 값이라 release()로 소비할 수 없습니다");
check "빌리는 메서드는 몇 번이든 부를 수 있다"
(move_ok
(selfsrc
^ "fn twice_query(l: Lease) effects {Lease.query} -> Int {\n\
\ let a = l.query(\"x\")\n\
\ l.query(\"y\")\n\
}"));
(* 중첩 수신자. 예전에는 시그니처를 못 찾아 조용히 빌림이 됐다 —
그러면 필드에 담긴 자원을 두 번 소비할 수 있었다. *)
check "struct 필드에 담긴 자원도 소비할 수 없다"
(move_has
(selfsrc
^ "struct Held { lease: Lease }\n\
fn f(h: Held) effects {Lease.release} {\n h.lease.release()\n}")
"부분 move가 없습니다");
(* 문법: own self는 capability 메서드의 첫 자리에만 존재한다 *)
check "일반 함수에는 self 자리가 없다"
(parse_err "fn f(own self) {}" = Some "self는 capability 메서드에서만 쓸 수 있습니다");
check "own 없는 self는 적을 말이 없다"
(parse_err "capability C {\n fn m(self)\n}"
= Some "self는 own self로만 적습니다 (수신자를 빌리는 것이 기본이라 적을 말이 없습니다)");
check "self는 첫 자리에만 온다"
(parse_err "capability C {\n fn m(x: Int, own self)\n}"
= Some "self는 capability 메서드의 첫 파라미터로만 올 수 있습니다")
(* 수신자를 소비하는지는 표면이다 — 바뀌면 호출하는 쪽의 move 검사 결과가
바뀌므로 interface hash에 들어가야 한다 *)
let () =
let h src = (Iface.of_module (parse_ok src)).Iface.hash in
check "own self가 interface hash를 바꾼다"
(h "pub capability C {\n fn m(own self)\n}"
<> h "pub capability C {\n fn m()\n}")
(* 보안 정리 (ii), 두 번째 구멍: 열거형 생성자가 인자를 소비하지 않았다.
리스트 리터럴과 struct 리터럴은 처음부터 Move 자리였는데 생성자만
일반 호출로 취급돼 빠져 있었다. Some(f)를 두 번 쓰면 복제됐다.
LRU 개밥 먹기가 affine 값을 열거형으로 실어나르는 방식을 쓰므로
이 자리가 뚫려 있으면 그 우회 자체가 무의미하다. *)
let () =
let src =
"capability Fd {\n\
\ fn close(own self) effects {Fd.close}\n\
}\n\
enum Maybe { Just(Fd), Nothing }\n\n"
in
check "사용자 열거형 생성자가 인자를 소비한다"
(move_has
(src
^ "fn dup(own f: Fd) -> Maybe {\n let a = Just(f)\n Just(f)\n}")
"이미 move되었습니다");
check "내장 Option 생성자도 소비한다"
(move_has
(src
^ "fn dup(own f: Fd) -> Option[Fd] {\n let a = Some(f)\n Some(f)\n}")
"이미 move되었습니다");
check "내장 Result 생성자도 소비한다"
(move_has
(src
^ "fn dup(own f: Fd) -> Result[Fd, Int] {\n let a = Ok(f)\n Ok(f)\n}")
"이미 move되었습니다");
check "한 번만 넣는 것은 된다"
(move_ok (src ^ "fn wrap(own f: Fd) -> Maybe {\n Just(f)\n}"));
check "빌린 값은 열거형에 넣을 수 없다"
(move_has
(src ^ "fn wrap(f: Fd) -> Maybe {\n Just(f)\n}")
"빌린 값이라 열거형에 넣을 수 없습니다");
check "copyable 값은 자유롭게 넣는다"
(move_ok (src ^ "fn n(x: Int) -> Option[Int] {\n let a = Some(x)\n Some(x)\n}"))
(* 점 왼쪽이 아무것도 아니면 그 이름은 없는 것이다.
예전에는 바깥 참조로 넘어가 오른쪽이 통째로 불투명해졌다 — D1과 같은
병이다. Nonsense.whatever(1)이 조용히 통과했다. *)
let () =
let msgs src =
List.map (fun (e : Resolve.error) -> e.msg) (snd (Resolve.resolve (parse_ok src)))
in
let contains m frag =
let n = String.length frag in
let rec go i =
i + n <= String.length m && (String.sub m i n = frag || go (i + 1))
in
go 0
in
let has src frag = List.exists (fun m -> contains m frag) (msgs src) in
check "가져오지 않은 이름에 점을 찍을 수 없다"
(has "fn f() -> Bool {\n Nonsense.whatever(1)\n}" "가져온 모듈도 지역 값도 아닙니다");
check "내장 타입은 모듈이 아니다"
(has "fn f(s: String) -> Int {\n String.len(s)\n}" "내장 타입이지 모듈이 아닙니다");
check "가져온 별칭은 통과한다"
(not
(has
"import \"cool.dev/std/string\" as String\nfn f(s: String) -> Int {\n String.len(s)\n}"
"모듈"));
(* capability 값 없이 메서드를 부르는 것은 타입 검사의 일이다.
여기서 막으면 그쪽의 더 정확한 진단에 닿지 못한다 *)
check "이 모듈의 선언은 통과시킨다 (진단은 타입 검사가 한다)"
(not
(has
"capability Db {\n fn read(k: Int) -> Int\n}\nfn f() -> Int {\n Db.read(1)\n}"
"가져온 모듈도"))
(* 보안 정리 (ii), 세 번째 구멍: 소유한 열거형을 두 번 분해할 수 있었다.
match가 scrutinee를 늘 빌려서, 조각을 owned로 꺼내 놓고도 원본이
소비되지 않았다. 파이프 하나에서 같은 fd를 두 번 꺼낼 수 있다는 뜻이다. *)
let () =
let src =
"capability Fd {\n\
\ fn close(own self) effects {Fd.close}\n\
}\n\
enum Pipe { Ends(Fd, Fd) }\n\
fn take(own f: Fd) effects {Fd.close} {\n\
\ f.close()\n\
}\n\n"
in
let twice =
"fn dup(own p: Pipe) effects {Fd.close} {\n\
\ match p {\n\
\ Ends(a, b) => {\n\
\ take(a)\n\
\ take(b)\n\
\ },\n\
\ }\n\
\ match p {\n\
\ Ends(a, b) => {\n\
\ take(a)\n\
\ take(b)\n\
\ },\n\
\ }\n\
}"
in
check "소유한 열거형을 두 번 분해할 수 없다"
(move_has (src ^ twice) "이미 move되었습니다");
check "한 번 분해해 꺼내는 것은 된다"
(move_ok
(src
^ "fn once(own p: Pipe) effects {Fd.close} {\n\
\ match p {\n\
\ Ends(a, b) => {\n\
\ take(a)\n\
\ take(b)\n\
\ },\n\
\ }\n\
}"));
check "빌린 열거형에서는 꺼낼 수 없다"
(move_has
(src
^ "fn borrowed(p: Pipe) effects {Fd.close} {\n\
\ match p {\n\
\ Ends(a, b) => take(a),\n\
\ }\n\
}")
"빌린 값이라");
(* 이름을 묶지 않는 패턴은 아무것도 꺼내지 않으므로 소비하지 않는다 *)
check "이름을 묶지 않는 분해는 소비하지 않는다"
(move_ok
(src
^ "fn peek(own p: Pipe) effects {Fd.close} {\n\
\ match p {\n\
\ Ends(_, _) => 1,\n\
\ }\n\
\ match p {\n\
\ Ends(a, b) => {\n\
\ take(a)\n\
\ take(b)\n\
\ },\n\
\ }\n\
}"))
(* 가져온 모듈의 struct를 만들 수 없었다. 타입은 쓸 수 있는데 값은 못
만드는 상태였고, 개밥 넷이 전부 같은 모듈 안에서만 struct를 만들어
한 번도 안 걸렸다.
실제로 해소까지 되는지는 supervisor/workers.cool이 Sup.Policy를 만들면서
확인한다 (위 dogfoods 목록). 여기서는 문법만 고정한다. *)
let () =
let parses src = Result.is_ok (Parser.parse_result (Result.get_ok (Lexer.lex_result src))) in
check "Alias.Name { ... } 를 파서가 받는다"
(parses "fn f() -> P.P {\n P.P { n: 1 }\n}");
(* no_struct 문맥에서는 "{" 가 언제나 블록이다. 여기서 점을 먹으면
if a.b { } 의 블록이 struct 리터럴로 읽힌다 *)
check "if의 머리에서는 Alias.Name 뒤의 { 가 블록이다"
(parses "fn f(a: Int) -> Int {\n if A.b { 1 } else { 2 }\n}");
check "match의 머리에서도 그렇다"
(parses "fn f(a: Int) -> Int {\n match A.b {\n _ => 1,\n }\n}")
(* 가져온 열거형의 이름을 한정 없이 쓰면 조용히 통과했다.
- 패턴에서는 바인딩이 되어 그 팔이 전부를 잡고, 오류가 다음 팔에 났다
- 식에서는 모르는 이름이 되어 move 검사가 생성자인 줄 몰랐다.
그래서 affine 값을 두 번 감쌀 수 있었다 (보안 정리 ii)
Mini Shell에 다섯 군데 있었고 파이프 처리 전체가 검사되지 않고 있었다. *)
let () =
let dep =
"pub capability Fd {\n\
\ fn close(own self) effects {Fd.close}\n\
}\n\
pub enum Box { Wrap(Fd), Empty }\n"
in
let msgs src =
let dir = Filename.temp_file "cool" "" in
Sys.remove dir;
Unix.mkdir dir 0o755;
let w n c =
let oc = open_out (Filename.concat dir n) in
output_string oc c;
close_out oc
in
w "k.cool" dep;
w "j.cool" src;
let st = Session.create ~root:dir ~std:"../std" () in
Session.load st (Filename.concat dir "j.cool");
List.map (fun (e : Session.error) -> e.message) (Session.errors st)
in
let has src frag =
List.exists
(fun m ->
let n = String.length frag in
let rec go i =
i + n <= String.length m && (String.sub m i n = frag || go (i + 1))
in
go 0)
(msgs src)
in
check "한정 없는 가져온 생성자를 식에서 거절한다"
(has
"import \"./k\" as K\nfn f(own x: K.Fd) -> K.Box {\n Wrap(x)\n}"
"K.Wrap로 적으십시오");
check "한정 없는 가져온 variant를 패턴에서 거절한다"
(has
"import \"./k\" as K\nfn f(b: K.Box) -> Int {\n\
\ match b {\n Empty => 0,\n K.Wrap(_) => 1,\n }\n}"
"K.Empty로 적으십시오");
check "한정하면 통과하고 이중 소비가 잡힌다"
(has
"import \"./k\" as K\nfn f(own x: K.Fd) -> K.Box {\n\
\ let a = K.Wrap(x)\n K.Wrap(x)\n}"
"이미 move되었습니다");
check "한정한 한 번은 통과한다"
(msgs "import \"./k\" as K\nfn f(own x: K.Fd) -> K.Box {\n K.Wrap(x)\n}" = [])
(* ================================================================== *)
(* 요약. 반드시 파일의 맨 끝이어야 한다. *)
(* *)
(* 예전에는 이 블록이 파일 184줄에 있었다. 최상위 let ()은 파일 순서대로 *)
(* 돌므로 그 뒤에 쓴 1500줄의 검사는 FAIL을 찍고도 종료 코드에 잡히지 *)
(* 않았다 — 즉 실패한 테스트를 커밋할 수 있었다. 빠른 검증 루프가 *)
(* 목적인 저장소에서 검증이 조용히 통과하는 것이 가장 나쁜 결함이다. *)
(* ================================================================== *)
let () =
if !failures = 0 then print_endline "ok"
else (
Printf.printf "%d개 실패\n" !failures;
exit 1)
+3
View File
@@ -0,0 +1,3 @@
(executable
(name ebnf_tool)
(libraries coollang))
+218
View File
@@ -0,0 +1,218 @@
(* 문법 파일을 읽어 기계가 소비할 수 있는지 확인하는 도구. *)
let read file =
let ic = open_in_bin file in
let n = in_channel_length ic in
let s = really_input_string ic n in
close_in ic;
s
(* 문법과 손 파서를 같은 파일에 돌려 판정이 갈리는지 본다 *)
let compare_files files =
let g =
match Coollang.Ebnf.parse_result (read "docs/grammar.ebnf") with
| Ok g -> g
| Error e -> failwith (Printf.sprintf "문법 %d행: %s" e.line e.msg)
in
let tokens =
[ "ident"; "int_lit"; "string_lit"; "NEWLINE"; "char"; "digit"; "letter" ]
in
let bad = ref 0 in
List.iter
(fun f ->
match Coollang.Lexer.lex_result (read f) with
| Error e ->
Printf.printf " 렉서 오류 %s:%d:%d %s\n" f e.pos.line e.pos.col e.msg
| Ok toks -> (
let hand =
match Coollang.Parser.parse_result toks with
| Ok _ -> None
| Error e -> Some (e.pos, e.msg)
in
let spec =
match Coollang.Recognize.check ~tokens g toks with
| Ok () -> None
| Error e ->
Some (e.pos, Printf.sprintf "%s이(가) 필요한데 %s" e.expected e.got)
in
match (hand, spec) with
| None, None -> ()
| Some _, Some _ -> ()
| None, Some (p, m) ->
incr bad;
Printf.printf "갈림 %s: 파서는 받고 문법은 거부 (%d:%d %s)\n" f
p.Coollang.Token.line p.Coollang.Token.col m
| Some (p, m), None ->
incr bad;
Printf.printf "갈림 %s: 문법은 받고 파서는 거부 (%d:%d %s)\n" f
p.Coollang.Token.line p.Coollang.Token.col m))
files;
Printf.printf "\n파일 %d개 중 판정이 갈린 것 %d개\n" (List.length files) !bad;
if !bad > 0 then exit 1
(* 문법에서 문장을 만들어 손 파서가 받는지 본다 *)
let fuzz n =
let g =
match Coollang.Ebnf.parse_result (read "docs/grammar.ebnf") with
| Ok g -> g
| Error e -> failwith (Printf.sprintf "문법 %d행: %s" e.line e.msg)
in
let tokens =
[ "ident"; "int_lit"; "string_lit"; "NEWLINE"; "char"; "digit"; "letter" ]
in
let bad = ref 0 in
let visited = Hashtbl.create 128 in
for i = 1 to n do
Random.init i;
let toks = Coollang.Ebnf_gen.sentence ~tokens ~visited ~max_depth:18 g in
match Coollang.Parser.parse_result toks with
| Ok _ -> ()
| Error e ->
incr bad;
if !bad <= 5 then begin
Printf.printf "\n[%d] 문법은 만들었는데 파서가 거부: %s\n " i e.msg;
List.iter
(fun (t : Coollang.Token.t) ->
match t.kind with
| Coollang.Token.Eof -> ()
| Coollang.Token.Newline -> print_string "\\n "
| k -> Printf.printf "%s " (Coollang.Token.show_kind k))
toks;
print_newline ()
end
done;
Printf.printf "\n문장 %d개 중 파서가 거부한 것 %d개\n" n !bad;
(* 어휘 층은 파서 문법에서 도달할 수 없다 — 분모에서 뺀다 *)
let lexical =
[
"ident";
"int_lit";
"string_lit";
"str_char";
"escape";
"bool_lit";
"literal";
]
in
let all =
Coollang.Ebnf.expand g
|> List.map (fun (r : Coollang.Ebnf.rule) -> r.name)
|> List.sort_uniq compare
|> List.filter (fun r -> not (List.mem r lexical))
in
let unvisited = List.filter (fun r -> not (Hashtbl.mem visited r)) all in
Printf.printf "프로덕션 %d개 중 %d개를 밟았다\n" (List.length all)
(List.length all - List.length unvisited);
if unvisited <> [] then
Printf.printf "밟지 않은 것: %s\n" (String.concat " " unvisited);
if !bad > 0 then exit 1
let () =
if Array.length Sys.argv > 1 && Sys.argv.(1) = "--dump" then (
let g =
match Coollang.Ebnf.parse_result (read "docs/grammar.ebnf") with
| Ok g -> g
| Error e -> failwith (string_of_int e.line)
in
let tokens =
[ "ident"; "int_lit"; "string_lit"; "NEWLINE"; "char"; "digit"; "letter" ]
in
for i = 1 to 8 do
Random.init i;
let toks = Coollang.Ebnf_gen.sentence ~tokens g in
Printf.printf "[%d] (%d토큰) " i (List.length toks);
List.iter
(fun (t : Coollang.Token.t) ->
match t.kind with
| Coollang.Token.Eof -> ()
| Coollang.Token.Newline -> print_string "\\n "
| k -> Printf.printf "%s " (Coollang.Token.show_kind k))
toks;
print_newline ()
done;
exit 0);
if Array.length Sys.argv > 1 && Sys.argv.(1) = "--fuzz" then (
fuzz (if Array.length Sys.argv > 2 then int_of_string Sys.argv.(2) else 200);
exit 0);
if Array.length Sys.argv > 2 && Sys.argv.(1) = "--compare" then (
compare_files
(Array.to_list (Array.sub Sys.argv 2 (Array.length Sys.argv - 2)));
exit 0);
if Array.length Sys.argv > 1 && Sys.argv.(1) = "--lexical" then (
print_string (Coollang.Lexical_doc.render ());
print_newline ();
exit 0);
let file =
if Array.length Sys.argv > 1 then Sys.argv.(1) else "docs/grammar.ebnf"
in
match Coollang.Ebnf.parse_result (read file) with
| Error e -> Printf.printf "%s:%d: %s\n" file e.line e.msg
| Ok g ->
Printf.printf "프로덕션 %d개\n" (List.length g);
Printf.printf "\n정의되지 않은 채 참조된 이름:\n";
List.iter (fun n -> Printf.printf " %s\n" n) (Coollang.Ebnf.undefined g);
Printf.printf "\n어디서도 참조되지 않는 프로덕션 (시작 기호 module 제외):\n";
List.iter
(fun n -> Printf.printf " %s\n" n)
(Coollang.Ebnf.unreachable g ~start:"module");
(* 문법이 쓰는 단말 전부. 렉서가 만드는 토큰과 대조하기 위한 것. *)
let terms = Hashtbl.create 64 in
let rec walk : Coollang.Ebnf.expr -> unit = function
| Term s -> Hashtbl.replace terms s ()
| Ref _ | RefArg _ -> ()
| Seq xs | Alt xs -> List.iter walk xs
| Opt e | Rep e -> walk e
| Except (a, b) ->
walk a;
walk b
in
List.iter (fun (r : Coollang.Ebnf.rule) -> walk r.body) g;
let ts =
Hashtbl.fold (fun k () acc -> k :: acc) terms [] |> List.sort compare
in
Printf.printf "\n문법이 쓰는 단말 %d개:\n %s\n" (List.length ts)
(String.concat " " ts);
(* 어휘 절의 이름은 파서 층에서 단말이다 *)
let tokens =
[
"ident"; "int_lit"; "string_lit"; "NEWLINE"; "letter"; "digit"; "char";
]
in
let g = Coollang.Ebnf.expand g in
let a = Coollang.Ebnf.analyze ~tokens g in
Printf.printf "\n비어도 되는(nullable) 프로덕션:\n %s\n"
(String.concat " "
(List.filter_map
(fun (r : Coollang.Ebnf.rule) ->
if Coollang.Ebnf.nullable a r.name then Some r.name else None)
g));
Printf.printf "\nFIRST 표본:\n";
List.iter
(fun n ->
Printf.printf " %-14s %s\n" n
(String.concat " "
(Coollang.Ebnf.SS.elements (Coollang.Ebnf.first a n))))
[ "decl"; "stmt"; "primary"; "type"; "pattern"; "item" ];
let cs = Coollang.Ebnf.conflicts ~tokens ~greedy:[ "NEWLINE" ] g in
let real =
List.filter (fun (c : Coollang.Ebnf.conflict) -> not c.c_greedy) cs
in
let soft =
List.filter (fun (c : Coollang.Ebnf.conflict) -> c.c_greedy) cs
in
Printf.printf "\n== LL(1) 충돌: 진짜 %d건, greedy로 해소 %d건 ==\n"
(List.length real) (List.length soft);
List.iter
(fun (c : Coollang.Ebnf.conflict) ->
Printf.printf "\n%s (%s:%d) [%s]\n 겹치는 토큰: %s\n %s\n" c.c_rule file
c.c_line c.c_kind
(String.concat " " c.c_tokens)
c.c_detail)
real;
if soft <> [] then begin
Printf.printf "\n-- greedy로 해소되는 것 --\n";
List.iter
(fun (c : Coollang.Ebnf.conflict) ->
Printf.printf " %s:%d %s [%s]\n" file c.c_line c.c_rule
(String.concat " " c.c_tokens))
soft
end